At work we have settled into a pattern of splitting our frontend and backend into standalone JS applications at the front and Ruby on Rails at the back. This means I've been writing a lot of Ruby on Rails applications where the only real interface is an API. I've avoided purpose-built gems, instead using a fairly standard Ruby on Rails stack and adopting a few general practices. I've compiled some of these into the following tips.

1. Use Jbuilder for JSON responses

Jbuilder is an official Ruby on Rails library for building JSON structures using a DSL. Add it to your Gemfile and you can write JSON responses using the full view stack Rails gives you. Jbuilder views look like this (taken from the Jbuilder README):

# app/views/message/show.json.jbuilder

json.content format_content(@message.content)
json.(@message, :created_at, :updated_at)

json.author do
  json.name @message.creator.name.familiar
  json.email_address @message.creator.email_address_with_name
  json.url url_for(@message.creator, format: :json)
end

if current_user.admin?
  json.visitors calculate_visitors(@message)
end

json.comments @message.comments, :content, :created_at

json.attachments @message.attachments do |attachment|
  json.filename attachment.filename
  json.url url_for(attachment)
end

This is particularly useful when your JSON responses contain a lot of control logic, or if you want to re-use partial structures across different API calls.

2. Write full-stack tests with real-world API calls

I'm not one for TDD. But adapting advice from DHH, when writing an API it pays to construct realistic requests (complete with whatever authentication you use) and parse the responses for expected output. This kind of test has many names; integration tests, request specs, or feature specs.

If you have a JSON API this helper is particularly useful when writing request specs:

def json
  @json ||= JSON.parse response.body
end

And then you can use it like this (with Rspec):

expect(json['status']).to eq("OK")

If your API integrates with third-party APIs, use webmock and/or vcr to help test with realistic responses.

If you need more Rspec helpers for writing API tests, check out rspec_api_helpers (thanks Ian!).

3. Compress large responses

Large API responses compress (gzip) very well because of how much re-use there is (e.g. XML tags or JSON labels in an array of objects), so you can achieve some massive speedups by turning it on. Compressing your API output is especially important if it is being consumed by mobile applications where throughput may be low.

You have a couple of choices when you want to compress output from your Ruby on Rails application, depending on your production environment:

  1. Use your reverse proxy (e.g. nginx, Apache)
  2. Use middleware like Rack::Deflater or heroku-deflater, particularly relevant if you are deploying to a PaaS like Heroku.

Assuming your API is data-based, you probably don't need to worry about using the fancier heroku-deflater to serve pre-compressed images, because you're probably not serving any. Even if you are, I go by the rule that CPU time is cheaper (faster) than bandwidth/throughput.

4. Use CORS for pain-free cross-domain API use

Forget JSONP, nowadays it's safe and accepted to use more sophisticated CORS and get all the benefits of full AJAX requests.

CORS is easy to enable in your app with rack-cors, another piece of middleware. It's important to insert rack-cors as early as possible in your middleware chain, so that it takes effect in the event of a raised exception or other middleware halting or modifying requests.

Here's a dangerous example configuration that allows all requests from anywhere. You should put this in your config/application.rb file or in one or more of your environment files.

  config.middleware.insert_before 0, Rack::Cors do
    allow do
      origins '*'
      resource '*',
        headers: :any,
        methods: [:get, :post, :put, :delete, :options]
    end
  end

You should consider a stricter CORS configuration than the one above. Design your API so that it only allows requests that you expect, from origins you expect.

5. Use Hawk for authentication

Always thoroughly evaluate all available security techniques to find ones suitable for your needs. Understand your threat model. Do not just take the advice of a random blog post on the Internet.

I spent some time evaluating different API authentication methods recently, and discovered (surprise!) that there is an overwhelming lack of standards. Most large APIs come up with their own authentication flows building on pre-existing standards.

  • Twitter uses a form of oAuth.
  • Amazon use custom HMAC signing that is sometimes adopted elsewhere (usually in ways that are subtly different, like in the ordering of payload fields).
  • Many places just use PSKs in query strings.
  • Many others use HTTP Basic Auth (which is basically just a PSK).
  • No one uses HTTP Digest.

Eventually I settled on Hawk. Hawk shares a lot with HTTP Digest and is ultimately a derivative of bits of oAuth (but done right). It also has a few optional features that you can implement to increase or decrease protection against various forms of attacks, depending on your requirements.

In essence, Hawk signs requests with a PSK but does not expose the PSK, and also helps protect against replay attacks, has the ability to verify server responses, the ability to verify request payloads, does not require any handshake requests, and is effective without TLS. It also has plenty of language implementations which is useful for an API.

I've deployed Hawk in mobile web applications, service-to-service APIs, and commercial partner APIs (where the partner was writing Java), and in all situations it has worked well.

However one disadvantage of Hawk is that it could potentially be a bit of a moving target. There isn't a formal specification and it's not that widely used.

Remember: do not just take the advice of a random blog post on the Internet when securing your software.


Good luck writing your API! :) Do you have any other tips & tricks you find yourself using regularly?