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.