Rails API Not Found Routes

Let's take a side street into error handling. URLs are complicated and sometimes users make requests to resource that don't exist - yikes! When this happens an application should surface an HTTP 404 error indicating the request was made to a location that does not exist.

Usually this page has a unicorn, bee, animal or something on it to provide delight and nudge the user to a real location of the application. However, because we are both hardcore and building a headless Rails API we choose grade A 100% pure JavaScript Object Notation ladies and gentlemen, disco sh-...

Early 2001 Johnny Depp references aside, let's add a new method to the basic Response concern of our application.

module Response
  extend ActiveSupport::Concern
  def not_found_response(message = "not found")
    render json: { message: message }, status: :not_found ❶  

As you can see we've created a new method called not_found ❶ that renders a Hash object containing a message as JSON with the matching HTTP status code. Because these methods are part of the Response concern and are "mixed in" to our ApplicationController, controllers which inherit from this base will be able to access these methods.

Notice that we use the symbol :not_found instead of specifying a 404 code. This symbol is a provided to us through Rack::Utils and maps directly to the correct HTTP status code 404. Another reason to love Ruby gems.

Now, everybody makes mistakes and sometimes a requested endpoint does not exist. To avoid user turmoil let's add some additional handling to inform Rails of how we'd like to deal with these situations. Specifically, open config/application.rb file and add the following to the Application class:

class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
  config.load_defaults 5.2
  config.exceptions_app = routes ❶  
  config.api_only = true

Here we are overriding the application that is called when an error needs to be shown. By default Rails provides a basic application in ActionDispatch::PublicExceptions.new(Rails.public_path) but instead of using the default page, we inform Rails that we would like to take the fate of our application's error handling into our own hands using our own application routes ❶ (I too like to live dangerously). Thankfully Rails obliges and wishes us good luck.

Our adventure does not end here. To round out the journey we need to create a catch all route in the config/routes.rb file that maps unknown URL requests to our ApplicationController's not_found action.

Rails.application.routes.draw do
  match "*unmatched", to: "application#not_found", via: :all ❶
  # your other routes

Inside the routes codeblock, the match method can be used to constrain particular URLs to a subset of HTTP verbs using the via  keyword argument. Remember that match is just a method and in ruby methods are parenthesis () optional (eso es muy picante!).

If that throws you off feel free to add parenthesis but just remember there is nothing special going on here: we are passing a string and various other argument that gets splatted ( *args)  by the method call's signature.

# Matches a URL pattern to one or more routes.
#   match 'path' => 'controller#action', via: :patch
#   match 'path', to: 'controller#action', via: :post
#   match 'path', 'otherpath', on: :member, via: :get
def match(path, *rest, &block)

Understanding this, have a look again at our work. We use match to wildcard match any route requested which is not defined in our application routes directing the traffic to our new not_found method in the ApplicationController. Let's see if our efforts bear fruit.

Start your server (bundle exec rails server) and making a bogus request to the server:

curl -v http://localhost:3000/ok_
> GET /ok_ HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.64.1
> Accept: */*
< HTTP/1.1 404 Not Found
# ….

{"message":"not found"}
* Closing connection 0

Nice work! Our side alley adventure might not be as profound as defeating the Gorgon Sisters in battle, but it is another item we can cross off our list.