Generating a JS API from Rails Routes

AuthorMáximo Mussini
·4 min read

One idea I've always appreciated about the routing approach in Rails is path helpers, which are automatically generated from route definitions.

Some advantages of using path helpers are:

  • There's no need to take care of manually interpolating ids and parameters to build a String URL.
  • Any change to the route definition affects the helper, so if a route is removed or modified, the path helper is no longer available.
  • Code that uses an outdated path helper causes a runtime error, which can be detected by integration or unit tests, instead of rendering a path that would end up in a 404, like it would happen when using manual string interpolation.

Path Helpers in JS

Traditionally, frontend code in Rails replicates the path structure as defined in the backend, hardcoding the paths for every endpoint that needs to be accessed, and specifying HTTP verbs as needed (GET, POST, PATCH).

$.get('/search', { query })

This approach is very cumbersome, and it is possible to introduce typos, or make mistakes like choosing the wrong HTTP verb (PUT instead of PATCH, and so on).

We could avoid these problems if we had path helpers in JS.

While some tools that allowed to expose path helpers to JS already existed, they were created when sprockets was the way to compile frontend assets in Rails, long before the Webpack era.

I wanted a solution that provided an enjoyable front-end development experience. One that could leverage Webpack loaders and aliases, and was based on ES6 modules to allow tree-shaking, was easy to inspect, and where changes could be tracked under version control.

And that's why I created JsFromRoutes.

Shaping the Vision

From a developer's UX perspective, the goal was to simplify how to make requests to a Rails API.

This meant not thinking about paths, parameter interpolation, nor HTTP verbs. Just plain function calls and promises.

import UsersRequests from '@requests/UsersRequests'

const user = await UsersRequests.create({ name, email })

These functions would be automatically generated from the Rails route definitions. The generation process should be transparent, and happen automatically as routes are added and the application is reloaded.

For maintainability, as well as for tree-shaking purposes, it was desirable to do a simple mapping: one request object per controller.

import { request } from '@services/ApiService'

export default {
  search: options =>
    request('get', '/users/search', options),

  create: options =>
    request('post', '/users', options),

  destroy: options =>
    request('delete', '/users/:id', options),
}

Delegating the actual request to an existing service would allow to keep the generated code lean. Any changes or improvements could then be made on the service itself, without having to regenerate the code. For example, switching from axios to redaxios, or to the fetch API.

Generating JS from Rails Routes ⚙️

Although the routes DSL in Rails wasn't designed for inspection purposes, it does provide the necessary metadata: the name of the endpoints, the HTTP verbs, and the paths including named parameters.

By doing some slight processing to the information in Rails.application.routes, and combining that with an ERB template, we are able to generate the JS functions we need.

import { request } from '@services/ApiService'

export default {
<% routes.each_with_index do |route, index| %>
  <%= route.helper %>: options =>
    request('<%= route.verb %>', '<%= route.path %>', options),
<% end %>
}

Automating the Flow 🤖

Why trigger the generation manually? Let the machines do it 😃

By adding a hook to Rails' reload process in development, we can automatically generate files from routes when a route is added, modified, or removed.

ActiveSupport::Reloader.to_prepare { JsFromRoutes.generate! }

In order to optimize file generation, we split the generated JS files by controller, and add a cache key based on the routes to avoid rewriting the file if the route definition hasn't changed.

When the Webpack development server is running, this automatically triggers a new build, and our request method or path helper is ready to be used!

The Gem 💎

The flow described above is now available through the js_from_routes gem. The library provides additional configuration options, visit the readme for more information.

This approach has similar benefits to path helpers in Rails:

  • No need to manually specify the URL, preventing mistakes and saving development time.
  • If an action is renamed or removed, the helper ceases to exist, which causes an error that is easier to detect than a 404.
  • The HTTP verb is embedded in the helper, so it's transparent for the client code. Changing the verb in the route causes the JS code to be regenerated, no need to update the consumers!

Summary

js_from_routes allows to automatically generate path and API helpers from Rails route definitions, allowing you to save development effort and focus on the things that matter.

Since introducing this tool, we have saved a lot of time that we would have spent writing boilerplate code, avoided a lot of mistakes like typos and interpolation errors, and made the front-end development experience more enjoyable 😃