![]() |
There comes a time in the development lifecycle of most web applications when a third-party integration becomes necessary. One of the simplest ways to do so is to expose a REST API for consumption. This article will walk you through a possible approach to designing and implementing a REST API in an intentionally simplistic task management web application, and will cover some best practices to ensure maintainability of the code.
2 Requirements, Assumptions
This article is going to assume some familiarity with the Rails framework, the Ruby programming language in general, and at least some familiarity with the Rails ecosystem by way of Devise, RSpec, and Capybara. When this does not significantly impact DRYness (DRY stands for "Don't Repeat Yourself," a mantra of software development), the more verbose syntax was intentionally chosen for readability.
2.1 The theoretical application
We will build out an API which corresponds to a task management application. It will contain a User model to represent users with access to the system, a Project model to represent projects, and a Todo model representing specific tasks to be done within a project. As such, a User will have many Projects, and a Project will have many Todos.
3 Theory. What makes an API RESTful?
- + Statelessness: Client state should not be stored on the server between requests. Said another way, each individual request should have no context of the requests that came before it.
- + Resource identification per request: Each request should uniquely identify a single resource. Said differently, each request that modifies the database should act on one and only one row of one and only one table.Requests that only fetch information should get zero or more rows from one table.
- + Representational state transfer: The resource endpoints should return representations of the resource as data, usually XML or JSON. The returned information should be sufficient for the client to uniquely identify and manipulate the database row(s) in question.
4. A basic REST API in Rails
4.1 Routes
Rails provides a fantastic tool for defining endpoints in the form of routes:
- ApiDemoApp::Application.routes.draw do
- scope '/api' do
- scope '/v1' do
- scope '/projects' do
- get '/' => 'api_projects#index'
- post '/' => 'api_projects#create'
- scope '/:name' do
- get '/' => 'api_projects#show'
- put '/' => 'api_projects#update'
- scope '/todos' do
- get '/' => 'api_todos#index'
- post '/' => 'api_todos#create'
- scope '/:todo_name' do
- get '/' => 'api_todos#show'
- put '/' => 'api_todos#update'
- end
- end
- end
- end
- end
- end
- end
While Rails will let you use PUT and POST more or less interchangeably, your API will be consumed by other developers. As such, it is best practice to go for the principle of least surprise – POST for create, PUT for update, PATCH for upsert (update and insert). Another reason to use the correct methods is that your application will be maintained by developers who are not you, possibly long after you are gone. Using methods which conform to standard practice (index, show, create, and update) ensures that this is a simpler task.
4.1.3 Versioning
Once your API is exposed, you need to assume that somebody is consuming it. Therefore, an existing API should never be modified, except for critical bugfixes. Rather than changing existing endpoints, expose a new version. One way to do this might be to create versioned controllers and routes (as above). With comprehensive test coverage, backwards compatibility can be ensured. A corollary of the above is that one should only expose an API that has gone through rigorous internal testing.
4.1.4 Route Parameters
While it is possible to identify routes based on the id column of the targeted resource, this does presume a higher degree of knowledge by the consuming application. A deliberate tradeoff — that needs to be made on a case by case basis — using unique database ids in the route chain allows users to access short routes, and simplifies resource lookup, while exposing internal database ids to the consumer and requiring the consumer to maintain a reference to ids on their end. Using public, but possibly not unique, identifiers like name reduces the amount of system internals that needs to be exposed, while allowing the client to easily lookup any data needed to make a request. The downfall is longer nested routes.
4.2 Controllers
4.2.1 BaseController and Authentication
A base API controller is useful to handle authentication and extract common API functionality. There are many possible schemes, but a common approach is to require reauthentication on a per-request level. This is probably the simplest way to ensure statelessness.
- class BaseApiController < ApplicationController
- before_filter :parse_request, :authenticate_user_from_token!
- private
- def authenticate_user_from_token!
- if !@json['api_token']
- render nothing: true, status: :unauthorized
- else
- @user = nil
- User.find_each do |u|
- if Devise.secure_compare(u.api_token, @json['api_token'])
- @user = u
- end
- end
- end
- end
- def parse_request
- @json = JSON.parse(request.body.read)
- end
- end
Devise.secure_compare helps avoid timing attacks. While the comparison algorithm used by Devise is not strictly speaking constant time as it uses newly allocated memory and is capable of invoking the Garbage Collector as a result, it is much nearer constant time then custom comparison routines. Similarly, the Users loop does not break, thus preventing an attacker from establishing api token validity based on response time.
Written by Abraham Polishchuk
If you found this post interesting, follow and support us.
Suggest for you:
The Complete Ruby on Rails Developer Course
Learn Ruby on Rails from Scratch
Ruby Scripting for Software Testers
Python, Ruby, Shell - Scripting for Beginner
Professional Rails Code Along

No comments:
Post a Comment