![]() |
- 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
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
- 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
- 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
- class ApiProjectsController < ApiProjectRouteController
- #...
- end
- class ApiTodosController < ApiProjectRouteController
- #...
- end
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
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
- 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
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
- 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
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
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

No comments:
Post a Comment