Implementing Friendly URLs with UUID-Based Models

Implementing Friendly URLs with UUID-Based Models

Β·

4 min read

I want to share a practical solution to a common challenge in Rails development I face it with almost every application I work with: combining UUID-based models with user-friendly URLs. If you've ever wondered how to maintain the security benefits of UUIDs while having clean, SEO-friendly URLs, you're in the right place!

The Challenge

Imagine you have a Rails app using UUIDs for your models (great choice for security and scalability!), but your URLs look like this:

https://yourapp.com/products/123e4567-e89b-12d3-a456-426614174000

Wouldn't it be nicer to have URLs like this?

https://yourapp.com/products/awesome-product-name

Let's make it happen! I'll walk you through the process step by step.

Prerequisites

Before we dive in, make sure you have:

  • A Rails application (I'm using Rails 7, but this works with Rails 5+ too)

  • PostgreSQL database (recommended for UUID support)

  • Models using UUID as primary keys

**

Step 1: Setting up your environment
**

First, let's add the friendly_id gem. It's a mature, well-maintained solution that makes our lives much easier, thanks Norman!

  • Add this to your Gemfile:
gem "friendly_id", "~> 5.5.0"  # Latest stable version as of 2024
  • Run these commands in your terminal:
bundle install
rails generate friendly_id  # Creates the friendly_id migration
rails db:migrate            # Sets up the friendly_id_slugs table
  • Now, let's add a slug column to your model. For example, if you have a Product model:
rails generate migration AddSlugToProducts slug:string:uniq
rails db:migrate

**

Step 2: Configuring our model
**

Here's where the magic happens. Lets see how to set up your model with different slug strategies:

class Product < ApplicationRecord
  # Step 1: Enable friendly_id
  extend FriendlyId

  # Step 2: Configure slug generation
  friendly_id :slug_candidates, use: [:slugged, :finders]

  private

  # Step 3: Define your slug candidates
  def slug_candidates
    [
      :name,                          # First try: just the name
      [:name, :category],             # If that's taken: name-category
      [:name, :category, :created_at] # Last resort: name-category-timestamp
    ]
  end

  # Step 4: Control when slugs should be regenerated
  def should_generate_new_friendly_id?
    name_changed? || category_changed? || super
  end
end

Let's break down what's happening here:

  1. extend FriendlyId adds the friendly_id functionality to your model

  2. :slugged enables slug generation

  3. :finders allows you to use find(params[:id]) with both slugs and UUIDs

  4. slug_candidates provides fallback options if your first choice is taken

**

Step 3: The magic rake task**

For a quick slug generation for existing records I used use rails console. Quick but, it is not the Rails way.
Here's a super helpful rake task I've created to manage your slugs. Create lib/tasks/friendly_id.rake:

namespace :friendly_id do
  desc 'Generate slugs for all your models'
  task generate_slugs: :environment do
    # Let's be informative about what we're doing
    puts "πŸš€ Starting slug generation..."

    # Replace Product with your model name
    Product.find_each do |record|
      print "Processing #{record.name}... "

      # Clear existing slug to force regeneration
      record.slug = nil
      if record.save(validate: false)
        puts "βœ… Created slug: #{record.slug}"
      else
        puts "❌ Failed"
      end
    end

    puts "\n✨ All done! Your URLs are now user-friendly!"
  end

  desc 'Check for any records missing slugs'
  task check_slugs: :environment do
    puts "πŸ” Checking for records without slugs..."

    records_without_slugs = Product.where(slug: nil)
    if records_without_slugs.any?
      puts "Found #{records_without_slugs.count} records needing slugs:"
      records_without_slugs.each do |record|
        puts "- #{record.name} (ID: #{record.id})"
      end
    else
      puts "πŸ‘ All records have slugs! You're good to go!"
    end
  end
end

Run these tasks in your terminal:

rake friendly_id:generate_slugs
rake friendly_id:check_slugs

Step 4: Implementation in your controllers

Update your controller to use friendly_id:

class ProductsController < ApplicationController
  def show
    # This will work with both slugs and UUIDs!
    @product = Product.friendly.find(params[:id])
  end
end

🌟 Keep in mind

  1. UUID compatibility

    • Your UUIDs are still there, working behind the scenes

    • Database relations still use UUIDs

    • URLs just look nicer now!

  2. URL generation

  3.  # In your views, nothing changes!
     <%= link_to product.name, product_path(product) %>
    
  4. Handling changes

    • Slugs automatically update when the source fields change

    • Old slugs can be preserved using the :history module

    • You can customize when slugs regenerate

  5. Troubleshooting
    Still seeing UUIDs in your URLs? Try these steps:

    1. Clear your browser cache

    2. Restart your Rails server

    3. Run rails friendly_id:generate_slugs

    4. Check your controller uses friendly.find

Common gotchas I stumbled upon and solutions

  1. Duplicate slugs

  2.  # Add a sequence for duplicates
     friendly_id :name, use: [:slugged, :sequence]
    
  3. Special characters

    • friendly_id handles most special characters well

    • You can customize with your own normalizer

  4. Performance

    • Slug lookups are indexed

    • UUID benefits remain for relationships

    • Best of both worlds! πŸŽ‰

**

Wrapping up**

You now have user-friendly URLs without sacrificing the benefits of UUIDs! Your URLs are:

  • SEO-friendly βœ…

  • Human-readable βœ…

  • Secure (UUIDs still used internally) βœ…

  • Easy to maintain βœ…

Have questions or run into issues? Drop a comment below! I'd love to help you implement this in your Rails app.

Happy coding! πŸš€ πŸ’»

Did you find this article valuable?

Support Ahmed Nadar by becoming a sponsor. Any amount is appreciated!

Β