4.2.2 ProjectsController
- class ApiProjectsController < BaseApiController
- before_filter :find_project, only: [:show, :update]
- before_filter only: :create do
- unless @json.has_key?('project') && @json['project'].responds_to?(:[]) && @json['project']['name']
- render nothing: true, status: :bad_request
- end
- end
- before_filter only: :update do
- unless @json.has_key?('project')
- render nothing: true, status: :bad_request
- end
- end
- before_filter only: :create do
- @project = Project.find_by_name(@json['project']['name'])
- end
- def index
- render json: Project.where('owner_id = ?', @user.id)
- end
- def show
- render json: @project
- end
- def create
- if @project.present?
- render nothing: true, status: :conflict
- else
- @project = Project.new
- @project.assign_attributes(@json['project']
- if @project.save
- render json: @project
- else
- render nothing: true, status: :bad_request
- end
- end
- end
- def update
- @project.assign_attributes(@json['project'])
- if @project.save
- render json: @project
- else
- render nothing: true, status: :bad_request
- end
- end
- private
- def find_project
- @project = Project.find_by_name(params[:name])
- render nothing: true, status: :not_found unless @project.present? && @project.user == @user
- end
- end
The todos controller will look similar, even having to find a project in order to fulfill the RESTfulness requirement (one uniquely identifiable resource per endpoint).
4.2.2.1 Defensive Programming
Defensive programming is a software design principle that dictates that a piece of software should be designed to continue functioning in unforeseen circumstances. Because your API will be exposed to third-party developers, allowing them to submit arbitrary inputs, it is important to apply this practice in API design.
This is why
has_key? is used above rather than simple existence checking. If
@json['project'] came in as false, or null, a simple existence check would return false, even though we can not say with any degree of accuracy that no JSON request body will ever have a false node at a depth of one.
The more significant defensive choice was the inclusion of
responds_to?(:[]). This is due to the fact that, if the request body were say
{"project": "noteIAmNotAnObject"},
@json['project']['name'] would result in a server error. In the spirit of defensive programming, when building APIs, ask yourself, "what is the least expected, most random, or most malicious input a user can submit?" Then write code to handle it.
4.2.2.2 HTTP Status Codes
While it is tempting to simply return 200 (OK), 404 (Not Found), and 500 (Internal Server Error), HTTP status codes — very much like HTTP verbs — have well-defined meanings. For the benefit of other developers, use status codes that make sense. For example, if we have a uniqueness constraint on Project name, we should return a conflict status on creation attempt for name clashes to let the developers understand where they went wrong.
The exception to the rule is when an item exists but the API user does not have the right access privileges to act on it. While it might be tempting to return a 403 (Forbidden), this in and of itself provides an attacker with information about the existence of the item. Instead, return a 404 if the user cannot access the record, no matter what the reason.
4.2.2.3 Code DRY?
While the above does achieve the desired result, it is far from DRY. There are repeated JSON validations and a duplicate database read, once for read only on update, and once for update rejection on a non existing item for create. Additionally, any nested route, such as
/api/v1/projects/:name/todos will require the same find methods so as to uniquely identify the correct resource.
5 A better way
First, we'll extract functionality common to all API endpoints into the BaseApiController.
- def validate_json(condition)
- unless condition
- render nothing: true, status: :bad_request
- end
- end
- def update_values(ivar, attributes)
- instance_variable_get(ivar).assign_attributes(attributes)
- if instance_variable_get(ivar).save
- render nothing: true, status: :ok
- else
- render nothing: true, status: :bad_request
- end
- end
- def check_existence(ivar, object, finder)
- instance_variable_set(ivar, instance_eval(object+"."+finder))
- end
This turns the create and update methods into calls to update_values, while the Project JSON validations call validate_json:
- before_filter only: :create do |c|
- meth = c.method(:validate_json)
- meth.call (@json.has_key?('project') && @json['project'].responds_to?(:[]) && @json['project']['name'])
- end
- before_filter only: :update do |c|
- meth = c.method(:validate_json)
- meth.call (@json.has_key?('project'))
- end
- before_filter only: :create do |c|
- meth = c.method(:check_existence)
- meth.call(@project, "Project", "find_by_name(@json['project']['name'])"
- end
- def create
- if @project.present?
- render nothing: true, status: :conflict
- else
- @project = Project.new
- update_values :@project, @json['project']
- end
- end
The other, possibly more significant improvement is introducing a multi-tiered inheritance hierarchy into your controllers. A good way to break this out is based on route nesting. Todos are nested within Projects; therefore, any Todo will need to find a Project.
- class ApiProjectRouteController < BaseApiController
- private
- def find_project
- @project = Project.find_by_name(params[:name])
- render nothing: true, status: :not_found unless @project.present? && @project.user == @user
- end
- def find_todo
- @todo = #...
- end
- #other finders also go here.
- end
ApiProjectsController now needs to inherit from ApiProjectRouteController rather then BaseApiController.
- class ApiProjectsController < ApiProjectRouteController
- #...
- end
- class ApiTodosController < ApiProjectRouteController
- #...
- end
6 Custom behavior
Unfortunately, APIs do not exist in a vacuum. The system will have its own set of predefined behaviors, and on occasion, despite the best of intentions it will be necessary to expose behavior through the API that is not available otherwise, or may even clash with the application's existing behavior. A specific, fairly common example would be the relaxing of validations.
For the sake of clarity, let us walk through an example. Let's assume that our system's Project model has a priority column that is required.
- class Project < ActiveRecord::Base
- has_many :todos
- belongs_to :user
- validates :priority, presence: :true
- validates :name, presence: :true
- end
Now let us assume that a major customer would like to integrate an in-house solution that allows Project Managers to bulk create Project entries without priority, as priorities have not yet been decided. However, the regular UI users should not gain these increased privileges. How could this be achieved? In short, we need the model to be aware of the request source, and apply one set of validations in case the request came from the API, and another otherwise.
The first thing to do, is to add this concept at the controller level. Adding a method to the BaseApiController:
- before_filter :indicate_source
- def indicate_source
- @api = true
- end
The second thing to do is to allow the model to validate conditionally.
- class Project < ActiveRecord::Base
- after_initialize :set_ivars
- has_many :todos
- belongs_to :user
- validates :priority, presence: :true, if: lambda{ |model| model.instance_variable_get(:@strict_priority_validation) }
- validates :name, presence: :true
- private
- def set_ivars
- @strict_priority_validation = true
- end
- end
Now every time a model is instantiated it will set its validations to be strict, and apply the priority validation if strictness is true.
The final piece of the puzzle is sharing state between the controller and the model to override model level instance variables. To do this, we can define a PORO (Plain Old Ruby Object):
- class ProjectValidationPicker
- def pick_a_validation(project, api=false)
- if api
- project.instance_variable_set(:@strict_priority_validation, false)
- else
- project.instance_variable_set(:@strict_priority_validation, true)
- end
- end
- end
And for the final piece of the puzzle, this object needs to be used on update/create. Handily, we already have a function on BaseApiController to do just that. Lets change this method to pull in a validation picker if it exists and to set the api appropriately.
- def update_values(ivar, attributes)
- instance_variable_get(ivar).assign_attributes(attributes)
- validation_picker = check_validation_picker_existence(ivar)
- validation_picker.send(:new).pick_a_validation(instance_variable_get(ivar), @api) if validation_picker
- if instance_variable_get(ivar).save
- render nothing: true, status: :ok
- else
- render nothing: true, status: :bad_request
- end
- end
- def check_validation_picker_existence(ivar)
- Module.const_get (ivar.to_s[1..-1] + "_validation_picker").camelize rescue false
- end
7 Debugging
Unfortunately, no code is ever perfect the first time it is written. While a detailed analysis of either of these tools is outside the scope of this article, it is worthy of note that both the Unix curl command, and the Google Apps Postman REST client allow crafting of custom requests to arbitrary endpoints.
8 Testing
With the code above, we have created a flexible, DRY, RESTful API. It allows us to have behavior that is unique to the API, while maintaining the same Database Models. But how can this be tested? Since the bulk of the lifting is done at the controller level, and since every request is a stateless transaction, request level specs are the most obvious fit. The simplest way to achieve this is controller tests, which do not differ significantly from the usual model for controller endpoint tests. Fundamentally, we craft JSON requests, and send them to the endpoints. A simple example might look something like this:
- describe ApiProjectsController do
- before do
- @base_json = { api_token: @user.api_token }
- @project_json = {project: {priority: "4", name: "foo"}}
- @new_project_json = @base_json.merge(@project_json)
- end
- describe "actions" do
- describe "#create" do
- before do
- @request.env['RAW_POST_DATA'] = @new_project_json.to_json
- lambda do
- post :create
- end.should change(Project, :count).by(1)
- end
- it "returns ok" do
- expect(response.status).to eq(200)
- expect(Project.all.last.priority).to eq(@new_project_json[:priority])
- expect(Project.all.last.name).to eq(@new_project_json[:name])
- end
- end
- end
- end
9 Other approaches and considerations
It is worth mentioning that the code outlined above represents only one possible approach to building a RESTful API in a Rails app. In fact it has made two, relatively significant tradeoffs.
The first, is that every request requires its own authentication. It is possible to avoid making this sacrifice using session cookies which Devise can also generate. I would argue that this complicates third party integration as it requires the developer to keep track of the session cookie, passing it in with every request instead of the api_token, as well as handling session expiration. In addition, it leaves the system exposed to attack, if the cookie origin is not scrutinized sufficiently, or even if the API consumer stands up to go to the restroom.
The second is that the in-house UI does not by default hit the same API endpoints. One solution would be to have Devise generate and expire a secondary, UI-only auth token, which does not trigger the API validation (maintains @api as false in BaseApiController), this may be stored in another column, or a different datastore entirely. Non-persistent in-memory datastores like Redis are particularly well suited to this application.
Finally it is worth noting that while this demo simply returns the entire resource, it is best practice to use a serializer to limit the subset of keys returned to what it is safe and/or necessary to expose.
10 Summary
We have covered how to build a REST api in an existing Rails application from the ground up, how to expose the endpoints, how to route to them, and how to allow custom behavior. We have touched on testing and debugging. While the example was fairly trivial, it is my hope that you can use it as a template for building scalable, reusable APIs.
Written by Abraham Polishchuk
If you found this post interesting, follow and support us.
Suggest for you:
Ruby Scripting for Software Testers
Python, Ruby, Shell - Scripting for Beginner
Professional Rails Code Along