Custom slugs for Rails URLs done right

Written by gilani | Published 2017/07/14
Tech Story Tags: ruby-on-rails

TLDRvia the TL;DR App

I’ve seen a lot of examples of this online, and it took a while for me to get it right, so I decided to document it.

The two cases I’ve used this for:

  1. Vanity URLs, e.g. trying to open a user’s profile at /u/amingilani
  2. Obfuscated URLs, e.g. trying to make the URL difficult to guess such as/transactions/601585f7–0f4a-41e8-bd04-b2eb24262fb4

Both cases differ only by the fact that the slug is randomly generated in the latter.

Quick definitions:

  • Slug: part of the URL to identify the record
  • Primary key: a unique identifier for database records
  • UUID: Universal unique identifier, a 128 bit generated identifier that depending on the implementation is either guaranteed or is very likely to be unique.

Overview:

  1. We’ll add a unique slug column to our database table
  2. We’ll generate a unique slug on creation (only for the 2nd use-case)
  3. We’ll use the slugs in URLs

Adding a slug column to our database

We’ll need to ensure that our slug is unique, and always present. You can name it “username” or anything more semantic if it’s meant to be part of the model.

class AddSlugToRecommendations < ActiveRecord::Migration[5.1]def changeadd_column :transactions, :slug, :string, null: falseadd_index :transactions, :slug, unique: trueendend

Generating a unique slug on creation

We’ll use the securerandom library that ships with rails to generate a unique UUID. If the UUID (by whatever sorcery) already exists, a new one will be generated instead.

This is only important, if we’re trying to obfuscate our URLS. Ignore this step if you’re letting users set something like username

class Transaction < ApplicationRecordbefore_create :set_slug

private

def set_slugloop doself.slug = SecureRandom.uuidbreak unless Transaction.where(slug: slug).exists?endendend

Use the slug in URLS

This will have to be set in multiple places:

  1. use the :slug param in routes
  2. lookup records using the :slug param
  3. Override the Model#to_param method to return theslug attribute instead of id

Tell your routes to use the slug param:

Rails.application.routes.draw doresources :transactions, param: :slugend

Now your routes will look like this:

edit_transaction GET /transactions/:slug/edit(.:format)transaction GET /transactions/:slug(.:format)PATCH /transactions/:slug(.:format)PUT /transactions/:slug(.:format)DELETE /transactions/:slug(.:format)

For your controller to lookup records by the slug, stop using the id and use the slug param instead:

class TransactionsController < ApplicationControllerbefore_action :set_transaction, only: [:show, :edit, :destroy]

private

def set_transaction@transaction = Transaction.find_by slug: params[:slug]endend

Let’s override the Model#to_param method so that form_for @transaction works:

class Transaction < ApplicationRecord...def to_paramslugend...end

Why my way is the best

Unless you’re creating a distributed application with distributed database nodes that need a collision free method to generate primary keys, you don’t need randomly generated ids. You’re just looking for a quick and easy way to hack URLS, which is why this method is awesome.

Using a string as a primary key is significantly slower than an integer during lookups, so every Model#children call will be slower.

Using a slug for URL lookups means the slow performance will be limited to URL lookups, which is what you wanted in the first place.


Published by HackerNoon on 2017/07/14