The Basic Setup
In this post, I’ll show how to go about setting up a versioned API, as well as a basic user authentication system for the API.
I will assume that you already have your Rails application created, and that you’re using Devise for user authentication. Don’t worry - if you’re not using Devise it’s pretty simple to adapt the information to your own authentication system.
Within the Rails ecosystem, there are many, many different methods and libraries for creating APIs. One such gem that we’ve had success with recently is rocket_pants.
The RocketPants gem has built-in support for things like paging, errors, caching, versioning, etc, and integrates with active_model_serializers.
When using RocketPants, your API controllers are segregated from your user-facing web controllers. This has some advantages and disadvantages.
Since they’re totally separate, it will be slightly more difficult to reuse logic between your web and API controllers. This shouldn’t be too big of a problem, because the controllers shouldn’t have much logic in the first place. Also, I find that mobile applications tend to have a different experience than the web app, and thus require different controllers/endpoints.
The advantage in separating the “Web” and API controllers is that the API controllers can be versioned and evolve separately, without effecting the web app.
The Base Controller
The first thing to do is add the rocket_pants gem and run bundle install:
After that’s done, we’ll go ahead and create a BaseController:
This is similar to the ApplicationController, as all of our API controllers will inherit from it. We’ll leave it empty until we build out the authentication system.
Also notice the path of this file: "app/controllers/api/v1". We’ll use this scheme so that we can easily separate controllers for different version of the API (when that time comes).
Authentication - The Sessions Controller
The sessions_controller#create endpoint will be responsible for accepting an email address and password from the mobile application, and returning an authentication token if the password is valid.
I’m going to post the code up front, and we’ll walk through it.
First of all, we’ll find the user for authentication by their email address. This is a method that Devise provides. After that, we’ll check if the user is found, and if they are, whether or not they have provided a valid password.
If the password checks out, we will generate an authentication token for the user (we have not implemented this method yet, but we will).
Once we’ve generated the token, we will use a method called “expose”, provided by RocketPants, that will expose the information about our user in the JSON response. In this case, we’ll send out the authentication token and user ID.
If the user is not found, or the password isn’t correct, we’ll use another method that RocketPants provides, “error!”, to raise an unauthenticated error.
Also note, in this example I used Strong Parameters. That’s mostly out of habit, it’s not really necessary here because we’re not doing any mass assignment.
Generating a User Token
Inside the User model, we’ll write a method to generate the authentication token.
Note: I haven’t included it here, but you’ll need to create a migration to add the authentication_token column to the users table. It can be a string type column.
Setting up the Routes
To get started, we can set our routes up like so:
This will end up generating a route that looks like this:
Trying it out
Now let’s try it out! We’ll use curl to simulate both an invalid and valid login attempt.
Locking down the API
Now that we’ve got a means of logging in, we can restrict access to the API for users who have not been authenticated.
Back in the basecontroller, we can add a couple beforefilters.
The first calls authenticate_user_from_token. This method looks at the token and user_id passed in, and decides if the token is valid. Here we use Devise.secure_compare() to compare the passed in token to the user’s actual token from the DB.
The next filter, require_authentication!, raises an unauthenticated error if no current_user was produced in the authenticate_user_from_token method.
Now, since we’re requiring authentication in the base controller, we’ll need to skip this before filter anywhere that we do NOT need authentication. One such example is the sessions_controller#create.
Adding the First Content Endpoint
With our authentication in place, we can add our first content endpoint. For my sample application, I’ve got a model called Organization. We’ll try out the API setup by creating an organizations#index endpoint.
The above code adds a simple controller index method that exposes all of the organizations, and a corresponding route.
Let’s test this again with curl:
We pass in the user_id and authentication token that we received from the above example calls to sessions#create. As you can see, RocketPants exposes a nice JSON format including the count.
Customizing the JSON Output
As I mentioned before, RocketPants integrates nicely with active_model_serializers. You can take a quick peek at their README to see what it’s all about.
Following along with the example, let’s assume that we don’t want to return the organization’s ID in the response above.
We’ll go ahead and add the activemodelserializers gem and then create an organization serializer. Within the serializer we specify that we only want to see the name and acronym fields.
RocketPants automatically picks up on this and the output is modified accordingly:
Let’s say we want to limit our response to 2 organizations per call.
We’ll simply add the Kaminiri gem, and paginate the organizations as usual. Again, RocketPants will automatically pick up on this, and paginate the response for us:
As you can see, appending the &page=2 param to the URL shows the 2nd page of results.
And Much More
This is by no means an exhaustive demonstration of RocketPants features. I merely demonstrated some of the basic ones. I encourage you to check out the README for more info.
Thanks for reading!
Hopefully this helped you get your API set up.
If you enjoyed this post, be sure to subscribe to the RSS feed to be informed of our new posts!