I prefer doing things The Rails Waytm whenever possible. Oftentimes when you are working with web requests, your controllers expect to respond to specific content formats. Formats that are outside of this expected format are generally handled within the controller layer. There’s nothing wrong with this approach BUT we can do better here by using request-based constraints in the Routing layer.
A simple JSON endpoint
Here’s a typical controller setup where we want to accomplish the following things:
- users#index endpoint
- return a relation of users in json format
- return a 200 success status
So this endpoint will only ever care about JSON responses. What happens if a client requests HTML?
The request succeeds and returns the jsonified user relation above. What should have been a failed request actually succeeds. Certainly not ideal but what if we use respond_to with a specific format?
Using respond_to to enforce format
The result here is more expected. The above raises a ActionController::UnknownFormat
when we make an HTML request to an endpoint that
expects only JSON. This makes sense because HTML isn’t a handled format and therefore the controller doesn’t know what to do.
We could stop here (and that would be a perfectly acceptable way of crafting the
controller action) but we’ve added 2 lines of code plus two blocks for what the original render json: users
does in one. So how can we have a simple controller while still enforcing format?
Request-based constraints for the routing layer
What is a Lambda?
A lambda is equivalent to an anonymous function in other languages. This means that you can specific logic within one without providing it a name.
By specifying the valid formats directly in our routes.rb file, we ensure that at
the routing layer that using an unsupported format will respond with a failed status. This is accomplishing by using a lambda to specify valid request formats using the following syntax lambda { |request| request.format == :json }
So let’s adjust our routes.rb file to add the new constraint and revert our UsersController to use the original render json
format:
Now when we make HTML requests to the endpoint the we recieve a routing error
that looks like: ActionController::RoutingError: No route matches [GET] "/users"
. Basically this is saying that the above route doesn’t even exist which is true; the HTML version of the above route isn’t defined.
In addition to using the simpler render syntax in the controller we no longer need to handle invalid request formats directly inside the controller.
Want to get even fancier? We can use the shorthand “stabby” lambda syntax to achieve the same results with:
So, now you’re thinking, “This is great and all, but I have 200 other json only endpoints. Am I going to need to repeat this pattern for each of them?”. Of course not! Rails doesn’t leave you hanging on this.
Advanced Constraints
For cases where we want to apply the same constraint to several routes, we can use a dedicated class. This class must respond to the #matches?
message. With this we have ourselves a highly reusable format constraint.
For the below example I’ve added a couple extra routes for demonstration purposes.
We’re still repeating ourselves a bit with the above example. Luckily, the constraints
syntax also has a block form which makes
it even easier to group your routes by JSON only endpoints.
I’ve also built the above to allow for arrays of formats to match against. This is useful for grouping routes that have multiple formats.
Now we’re grouping all of our JSON routes behind a formatting constraint. Nice!
Acknowledgments
Steve Grossi for the idea of using a dedicated constraint class
Dave Jones for pointing me at the concept wrapping routes within a constraint block.
Wrapping Up
This is just one type of routing constraint that Rails allows for. If you’d like to learn more about request-based constraints here’s the documentation on the subject.
What did you think about this method? Does it make controller code cleaner at the price of hiding formatting logic? Tell me about it below.
Join the conversation