I recently built an API with Sinatra and ran into a recurring challenge when dealing with resource-specific routes (like /objects/:id
). The first thing I had to handle in each of those routes was whether or not a record for both the resource type and id existed. If it didn't, I wanted to send back a JSON response with some meaningful error message letting the API consumer know that they asked for a certain kind of resource with an ID that didn't exist.
My first pass looked something like this:
get '/objects/:id' do |id|
object = Object.find_by_id(id)
if object.nil?
status 404
json(errors: "Object with an ID of #{id} does not exist")
else
json object
end
end
put '/objects/:id' do |id|
object = Object.find_by_id(id)
if object.nil?
status 404
json(errors: "Object with an ID of #{id} does not exist")
else
if object.update_attributes(params[:object])
json object
else
json(errors: object.errors)
end
end
end
Seems ok, but there would be a lot of duplication if I had these if
/else
statements in every resource-specific route. Lately, I've looked for common if
/else
conditionals like this as an opportunity for method abstraction, particularly with the use of blocks and yield
. The following methods are an example of this kind of abstraction:
def ensure_resource_exists(resource_type, id)
resource = resource_type.find_by_id(id)
if resource.nil?
status 404
json(errors: "#{resource_type} with an ID of #{id} does not exist")
else
yield resource if block_given?
end
end
Then the initial example would look something like:
get '/objects/:id' do |id|
ensure_resource_exists(Object, id) do |obj|
json obj
end
end
put '/objects/:id' do |id|
ensure_resource_exists(Object, id) do |obj|
if obj.update_attributes(params[:object])
json obj
else
json(errors: obj.errors)
end
end
end
It hides away the distracting error case handling and gives us a readable, declarative method body. Next time you find yourself dealing with repetitive error cases, use blocks like this for great justice!