Redis For A Flat URL Hierarchy

Dan Croak

In the URL structure of a new app, I want the routes to have a flat hierarchy similar to Quora’s:

  • A user: http://quora.com/Jason-Morrison-1
  • A topic: http://quora.com/Ruby-on-Rails
  • A question: http://quora.com/How-do-you-install-vim-color-scheme-in-OS-X

I’ve heard this might be good for SEO and a few objects in this new app should share the top-level namespace.

Keys and values

URLs are to resources as keys are to values. Maybe this is a job for a key-value store like Redis.

Set up

Getting Redis on OS X:

brew install redis

In Gemfile:

gem 'redis'

In config/initializers/redis.rb:

REDIS = Redis.connect(url: ENV['REDISTOGO_URL'])

In config/environments/development.rb:

ENV['REDISTOGO_URL'] ||= 'redis://localhost:6379'

That works with the “Redis To Go” Heroku add-on in staging/production:

heroku addons:add redistogo

Set the data

Each time any object I want to share this namespace is saved, I’ll notify Redis. For example, users:

class User < ActiveRecord::Base
  validates :handle, presence: true, uniqueness: true

  before_save do
    REDIS.set handle, 'User'
  end
end

I’m going to set their handle as the key and the value as the object type.

Rack middleware for the request

When a new request comes in, let’s have Redis find the key and if a record is found, mutate the route for my Rack endpoint.

In config.ru:

require ::File.expand_path('../config/environment',  __FILE__)
use RedisRouter
run MyRails::Application

In app/middleware/redis_router.rb:

class RedisRouter
  def initialize(app)
    @app = app
  end

  def call(env)
    intended_resource = env['REQUEST_PATH'].gsub('/', '')

    if type = REDIS.get(intended_resource)
      new_route = "/#{type.underscore.pluralize}/#{intended_resource}"
      env["REQUEST_PATH"] = env["PATH_INFO"] = env["REQUEST_URI"] = new_route
    end

    @app.call(env)
  end
end

If a record isn’t found, Rack will pass through normally.

Let Rails finish the request

In config/routes.rb:

MyRails::Application.routes.draw do
  get '/users/:handle', to: 'users#show'
  get '/places/:handle', to: 'places#show'
  get '/lists/:handle', to: 'lists#show'
end

In app/controllers/users_controller.rb:

class UsersController < ApplicationController
  def show
    @user = User.find_by_handle!(params[:handle])
  end
end

Would you do it this way

This approach introduces a dependency on Redis and makes two requests to two separate databases. We could have had one SQL query that queried multiple tables.

I like the idea of separating the concern of determining which type of object we want. That query should be fast and we can avoid messing with database indexes on our SQL database for this one case. We can separate this concern and if necessary, scale this component of the application independently.