API Versioning

Implementing API versioning on our Rails API

A Quick Recap

In the previous article we created a login and register endpoint.. We can now implement API versioning for our Rails API.

Feel free to refer to the part 6 branch of the GitHub repository if needed.

This is the 6th installment of my Developing A Cross-Platform iOS & Android Social Media App series.

What Is API Versioning And Why Do We Need It?

Our current API currently has the endpoints /users/login and /users/register. Imagine we are in a scenario where we have an active userbase and we are updating our mobile app from 1.2.3 to 2.0.0. This is signifies some form of a major change. In 2.0.0 users are now asked to enter their name on the registration screen, in addition to an email and a password. In this scenario, our 1.2.3 mobile app's registration field only has an email and password field. How can we release 2.0.0 without breaking 1.2.3?

The cleanest approach is API versioning. That means 1.2.3 will point to the /v1/ version of the api and 2.0.0 will point to the /v2/ version of the api.

So if a user tries registering using 1.2.3, the app would point to /v1/users/register and 2.0.0 would point to /v2/users/register. This will prevent potential bugs and conflicts when deploying a critical change.

Note: Some would argue this is overkill for a small project with no userbase. It's easy to implement, keeps urls clean, and is a good habit for a developer in the industy. If you do it right the first time, so you won't have to worry about it in the future.

Versioning The Controller

The first step is to move the controller into a directory called v1. This means that app/controllers/users_controller.rb becomes app/controllers/v1/users_controller.rb. You can create the directory and move the file any way you'd like but here is how you can do it in the terminal:

$ mkdir app/controllers/v1
$ mv app/controllers/users_controller.rb app/controllers/v1/users_controller.rb

mkdir: makes the directory in the path provided

mv: moves the file provided to the new path. In this case moving from the controllers folder to the controllers/v1 folder.

mv can also be used to rename files: mv file.rb new_name.rb

We need to update our users controller, so we need to update this line:

class UsersController < ApplicationController

When Rails sees UsersController, it will look for the users_controller.rb file in controllers/. We need to tell Rails that this controller is now in a subdirectory in the controllers/ folder. All we would have to do it preprend UsersController with V1:: as such:

class V1::UsersController < ApplicationController

Note: ApplicationController's path is still controllers/application_controller.rb so we do not prepend V1:: to ApplicationController.

Updating The Routes

Our last step is to update the config/routes.rb file. Here is how it looks now:

Rails.application.routes.draw do
  post 'users/register', to: 'users#register'
  post 'users/login', to: 'users#login'
end

We need to tell Rails that we are now versioning our endpoints. This is fairly simple to implement using namespace:

Rails.application.routes.draw do
  namespace :v1 do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
  end
end

namespace: Tells rails that we are going to provide it with a prefix (:v1) to be applied to everything nested inside it.

:v1: tells Rails to look for V1::UsersController

do: everything nested under this namespace will be prepended with /v1/

We can now run rake routes to see our new routes:

$ rake routes
           Prefix Verb   URI Pattern                   Controller#Action
v1_users_register POST   /v1/users/register(.:format)  v1/users#register
   v1_users_login POST   /v1/users/login(.:format)     v1/users#login

Awesome, our routes are now updated to be prepended with v1/. Let's restart are server using ctrl + c and by running rails s. Once the server is up, lets test our new routes:

$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456","password_confirmation": "123456"}}' http://localhost:3000/v1/users/register | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   232    0   146  100    86    211    124 --:--:-- --:--:-- --:--:--   211
{
   "email" : "[email protected]",
   "id" : "2907763c-6180-48b1-84ba-231372a445d2",
   "created_at" : "2020-02-10T17:06:32.468Z",
   "updated_at" : "2020-02-10T17:06:32.468Z"
}
$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   198    0   146  100    52   1092    389 --:--:-- --:--:-- --:--:--  1097
{
   "email" : "[email protected]",
   "updated_at" : "2020-02-10T17:06:32.468Z",
   "created_at" : "2020-02-10T17:06:32.468Z",
   "id" : "2907763c-6180-48b1-84ba-231372a445d2"
}

Note: Be sure to update the urls to include v1/

Using Routing Concerns

We still can prepare our routes.rb file to be cleaner in the future. We want to focus our code to be DRY (Don't Repeat Yourself). A routing concern allows us to define some routes within that concern and use those routes anywhere in the file. In this case we want a routing concern for our login and register routes and call that concern in the v1 namespace.

We need to add a concern to our routes.rb file:

Rails.application.routes.draw do
  concern :base_api do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
  end

  namespace :v1 do
    # ...
  end
end

concern: tells Rails that we are declaring a concern

:base_api: This is what you want the concern to be named as. A good self-doccumenting name would be :base_api but it could be anything like :report_endpoints

do: tells Rails that everything nested between the do and end should be outputted anywhere this concern is called.

post 'users/register': this line was just copied and pasted from namespace :v1 do. We don't need to adjust it.

We can now delete everything nested within namespace :v1 do and call the concern:

namespace :v1 do
  concerns :base_api
end

concerns: tells Rails we want to use a concern

:base_api: tells Rails which concern we want to use

Note: Since the concern is under the :v1 namespace, it will already know to look for v1/ controllers.

Before we test it, here is an example of why concerns are useful. Here is an example without API verisoning that has a v2 namespace with a new endpoint:

Rails.application.routes.draw do
  namespace :v1 do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
  end

  namespace :v2 do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
    get 'users/new-custom-endpoint', to: 'users#new_custom_endpoint'
  end
end

Here is an example of the routes.rb file above with API verisoning:

Rails.application.routes.draw do
  concern :base_api do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
  end

  namespace :v1 do
    concerns :base_api
  end

  namespace :v2 do
    concerns :base_api
    get 'users/new-custom-endpoint', to: 'users#new_custom_endpoint'
  end
end

When you compare the two, you can see that that the 2nd example is more readable. Imaging if we had 3 api versions with dozens of endpoints. It helps self-document the code because at one glance we know exactly what endpoints exist through all api versions. The code within the controllers may differ, but knowing if there is a [POST] /comments/ endpoint in v2 and not v1 is important.

Don't forget that you shouldn't have the v2 namespace above as that was just an example. Your config/routes.rb should look like:

Rails.application.routes.draw do
  concern :base_api do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
  end

  namespace :v1 do
    concerns :base_api
  end
end

Testing The Refactored Code

First, lets run rake routes again.

$ rake routes
           Prefix Verb   URI Pattern                  Controller#Action
v1_users_register POST   /v1/users/register(.:format) v1/users#register
   v1_users_login POST   /v1/users/login(.:format)    v1/users#login

Since /v1/ is being prepended, lets restart our server again with ctrl + c and rails s and run our curl commands again.

$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456","password_confirmation": "123456"}}' http://localhost:3000/v1/users/register | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   246    0   153  100    93    101     61  0:00:01  0:00:01 --:--:--   101
{
   "updated_at" : "2020-02-10T18:09:33.148Z",
   "id" : "50cc4c1f-aa26-4a99-a1e3-e345f5ba58ee",
   "created_at" : "2020-02-10T18:09:33.148Z",
   "email" : "[email protected]"
}
$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   212    0   153  100    59    998    385 --:--:-- --:--:-- --:--:--  1000
{
   "email" : "[email protected]",
   "updated_at" : "2020-02-10T18:09:33.148Z",
   "id" : "50cc4c1f-aa26-4a99-a1e3-e345f5ba58ee",
   "created_at" : "2020-02-10T18:09:33.148Z"
}

If everything works, you've successfully implementing API versioning!

What's Next?

By now you should have 2 important endpoints: login and register. But these need to be improved so that we won't have to pass login and password to every request. In the next section we will be implementing Json Web Tokens.