Ruby
Article

Build APIs with Napa

By Devdatta Kane

APIs are one of the hottest topics in web development. With the increasing number of SPAs (Single Page Applications) and the microservice craze, APIs will become even more popular. As Ruby on Rails developers, we have an amazing framework at our disposal, very capable of building APIs. Though Rails is powerful, it is more complex as well. Sometimes we need a simpler, more nimble framework, one that only does one thing and does it well. I think I have found such a framework. Allow me to introduce you to a framework that is specifically designed for building APIs: Napa.

Napa combines gems like Grape, Roar, and ActiveRecord, allowing us to build powerful APIs with minimal effort. In this tutorial, we will learn how to build an API with token authentication using Napa and Devise.

Installing Napa

Napa is available as a gem, that requires Ruby 2.0. We will need install that first using RVM or rbenv. Switch to Ruby 2.0 and install Napa:

gem install napa --no-ri --no-rdoc

Creating the Project

First, create the project using Napa’s generator:

napa new contact-service -d=pg

By default, Napa will generate a MySQL configured project. The -d=pg switch to tells Napa to generate a PostgreSQL configured project.

Napa will create a project directory named contact-service and generate all the necessary directories and files. The project is structured as follows:

contact-service
- app
  - apis
  - models
  - representers
- config
- db
- lib
- log
- spec
- Gemfile
- Rakefile

The structure is quite similar to a Rails application, but a little bit simpler. Most of the application’s code goes into the app directory. Our APIs will be in the (wait for it..) apis directory. Models will go into models directory, just like Rails. One of the newer bits here is the representers directory, which will hold the Roar representers. Representers convert the models into the desired JSON output. Other directories serve the same purpose as they do in a Rails app.

Switch to the project directory and install all gems:

cd contact-service
bundle install

After the gems are installed, we will add the database details in .env. First, create the database:

rake db:create

Let’s move on to the next step.

Creating the API

Time for the good stuff, creating the actual API. For that we will first require a model. Create a Contact model with some basic attributes such as name, email and phone number:

napa generate model Contact name:string email:string phone:string

Migrate the database:

rake db:migrate

Ok, now let’s create an API endpoint:

napa generate api contact

Napa will generate the API class – app/apis/contacts_api.rb and the representer class – app/representers/contact_representer.rb. The contacts_api.rb class should look as follows:

class ContactsApi < Grape::API
  desc 'Get a list of contacts'
  params do
    optional :ids, type: Array, desc: 'Array of contact ids'
  end
  get do
    contacts = params[:ids] ? Contact.where(id: params[:ids]) : Contact.all
    represent contacts, with: ContactRepresenter
  end

  desc 'Create a contact'
  params do
  end
  post do
    contact = Contact.create!(permitted_params)
    represent contact, with: ContactRepresenter
  end

  params do
    requires :id, desc: 'ID of the contact'
  end
  route_param :id do
    desc 'Get an contact'
    get do
      contact = Contact.find(params[:id])
      represent contact, with: ContactRepresenter
    end

    desc 'Update a contact'
    params do
    end
    put do
      # fetch contact record and update attributes.  exceptions caught in app.rb
      contact = Contact.find(params[:id])
      contact.update_attributes!(permitted_params)
      represent contact, with: ContactRepresenter
    end
  end
end

By default, Napa provides a blank declaration of parameters that should be accepted by the API. This is provided for securing the API against attacks. We will update the params blocks of the post and put methods and set the allowed parameters:

...
desc 'Create an contact'
params do
  optional :name, type: String, desc: 'The Name of the contact'
  optional :phone, type: String, desc: 'The Phone of the contact'
  optional :email, type: String, desc: 'The Email Address of the contact'
end
...

...
desc 'Update a contact'
params do
  optional :name, type: String, desc: 'The Name of the contact'
  optional :phone, type: String, desc: 'The Phone of the contact'
  optional :email, type: String, desc: 'The Email Address of the contact'
end
...

Now, we will update the contact_representer.rb class. Add the Contact model fields in the class that should be part of the JSON responses. Update the contact_representer.rb class, as follows:

class ContactRepresenter < Napa::Representer
  property :id, type: String
  property :name
  property :phone
  property :email
end

The last remaining part is to add the API to application_api.rb, which mounts the API:

class ApplicationApi < Grape::API
  format :json
  extend Napa::GrapeExtenders

  mount ContactsApi => '/contacts'

  add_swagger_documentation
end

Firing Some Requests

Our API is almost ready. Let’s test it using curl. First, start the development server:

napa server

Now, open another terminal so we can fire requests at the API. First, create a contact:

curl -X POST -d name="Devdatta Kane" -d email="kane.devdatta@gmail.com" -d phone="25451512544" http://localhost:9393/contacts

Napa’s response is always wrapped in a data element:

{
  "data": {
    "object_type": "contact",
    "id": "1",
    "name": "Devdatta Kane",
    "email": "kane.devdatta@gmail.com",
    "phone": "25451512544"
  }
}

Now that we have created a contact, let’s try to fetch all contacts:

curl -X GET http://localhost:9393/contacts

The response will be an array of elements.

{
  "data": [
    {
      "object_type": "contact",
      "id": "1",
      "name": "Devdatta Kane",
      "email": "kane.devdatta@gmail.com",
      "phone": "25451512544"
    }
  ]
}

We can fetch a single contact by passing the contact’s ID:

curl -X GET http://localhost:9393/contacts/1

Which responds with:

{
  "data": {
    "object_type": "contact",
    "id": "1",
    "name": "Devdatta Kane",
    "email": "kane.devdatta@gmail.com",
    "phone": "25451512544"
  }
}

Try to update the contact’s details:

curl -X PUT -d email="dev@devdatta.com" http://localhost:9393/contacts/1

Napa returns the updated object:

{
  "data": {
    "object_type": "contact",
    "id": "1",
    "name": "Devdatta Kane",
    "email": "dev@devdatta.com",
    "phone": "25451512544"
  }
}

Securing the API

We have a fully working API. Yay! But it is completely insecure. BOO! We are allowing API access without any authentication. One of the most simple used authentication method for securing APIs is Token Authentication. We will implement simple token authentication in our API using Devise. We could implement authentication on our own as well, but using a battle tested library such as Devise has its advantages.

First, add Devise in our Gemfile:

gem 'devise'

And update the bundle:

bundle install

Devise is now installed, but needs a config file. In Rails, we can generate the file using a Devise generator, but that won’t work in Napa. So we will manually create the file in the config/initializers directory. Create a file named devise.rb in that directory and add the following:

Devise.setup do |config|
  config.secret_key = '6e059f15248c9c26e7208a4a1129029c13f4a0fcef629562c34a4b3f0b1bbcbb8ed1431728cdbc6fe6e6bc4ed0f90cc9fc701c962e63be107e0bfd021eb70f08'
  config.mailer_sender = 'a@example.com'
  require 'devise/orm/active_record'
  config.authentication_keys = [:email] 
  config.case_insensitive_keys = [:email ]
  config.strip_whitespace_keys = [:email ]
  config.skip_session_storage = [:http_auth]
  config.stretches = Rails.env.test? ? 1 : 10
  config.password_length = 8..128
end

Replace the config.secret_key with a new secret. Now we will create a User model, using Napa’s model generator.

napa generate model User

Napa will create the model class and migration as well. Open the migration class and add the following:

...
def change
  create_table :users do |t|
    t.string :email,              null: false, default: ""
    t.string :encrypted_password, null: false, default: ""
    t.string :authentication_token
    t.timestamps
  end
  add_index :users, :email, unique: true
  add_index :users, :authentication_token, :unique => true
end
...

Open the app/models/user.rb and add the following:

class User < ActiveRecord::Base
  devise :database_authenticatable

  before_save :ensure_authentication_token

  def ensure_authentication_token
    self.authentication_token ||= generate_authentication_token
  end

  private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.where(authentication_token: token).first
    end
  end
end

Now that we have our model and migration in place, migrate the database:

rake db:migrate

We will create a User using Napa console, which you can fire up with:

napa console

pry(main)> User.create!(email:'kane.devdatta@gmail.com', password:'abcdabcd', password_confirmation:'abcdabcd')

You should get the following output:


=> #<user id: 1, email: "kane.devdatta@gmail.com", encrypted_password: "$2a$10$vdRAL.CusK3IEBTopa1cau5WTLihyNDdssuAtU3otieq...", authentication_token: "yFjgn5cwsFAfKhvU1R_t", created_at: "2015-08-08 11:41:05", updated_at: "2015-08-08 11:41:05">
</user>

There is the user’s authentication_token which we will need later. Exit the console.

We have to add authentication support to our API. Update the app/apis/application_api.rb as follows:

class ApplicationApi < Grape::API
  format :json
  extend Napa::GrapeExtenders

  before do
    error!("401 Unauthorized", 401) unless authenticated?
  end

  helpers do
    def authenticated?
      return true if User.find_by_authentication_token(params[:access_token])
    end
  end

  mount ContactsApi => '/contacts'

  add_swagger_documentation
end

Now, start napa server again and test if teh authentication works properly:

curl -X GET http://localhost:9393/contacts

This should result in an error, since we have not passed the access token with the request:

{"error":{"code":"api_error","message":"401 Unauthorized"}}

Try the same request but with access token:

curl -X GET http://localhost:9393/contacts\?access_token\=yFjgn5cwsFAfKhvU1R_t

And you’ll receive a successful response:

{
  "data": {
    "object_type": "contact",
    "id": "1",
    "name": "Devdatta Kane",
    "email": "kane.devdatta@gmail.com",
    "phone": "25451512544"
  }
}

Wrap Up

Today we learned how to quickly build APIs with Napa. Napa offers extra functionality through middleware, such as logging and sensitive data scrubbing, which you can explore on Napa’s GitHub repository. Our authentication solution can also be improved using something like OAuth and Access Grants, but I leave that exercise to you.

Comments and feedback welcome, as always.

  • Fun Frenzy

    Thanks for the GREAT tutorial! Very easy to follow along. I’ll definitely be using this gem for my next SPA!

  • Navjeet Chabbewal

    Very useful article written in a very succinct way and to the point. Thanks a lot.

  • Sumit

    Thank you, this was very informative. How can this be integrated with omniauth, so that users could choose to log in with, say, Facebook or Twitter?

  • Tiago Cardoso

    Why this over plain Grape? Besides the generators a la rails, I don’t see a lot of advantages.

  • spullen09

    Tried out the tutorial.
    Couple of things I noticed:
    * Some of the generators are broken/missing on the latest version on rubygems (maybe it hasn’t been pushed yet?). I had to patch the library’s code in order for it to work.
    * Once I did get everything to work, making requests were super slow, slower than a stock rails (webrick with postgres) json endpoint

  • spullen09

    Tried out the tutorial.
    Couple of things I noticed:
    * Some of the generators are broken/missing on the latest version on rubygems (maybe it hasn’t been pushed yet?). I had to patch the library’s code in order for it to work.
    * Once I did get everything to work, making requests were super slow, slower than a stock rails (webrick with postgres) json endpoint

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.