Building a data API with Ruby on Rails 5: Part 1 — Setup and Routing.

This is the second part of my ruby on rails series. Part one can be found here: Ruby and RVM. The source code is available on Github. I can also be found on twitter @corey_s_, and on twitch at twitch.tv/jastora.

In this tutorial we will be starting to lay the foundation for our rails 5 API. We wont be building a traditional demo API, but something a little more useful. The idea behind this is to build an API that will allow us to log data from various applications that have access to our endpoint. This is useful if we want to see real time results from our logging in a mobile app, a game demo or even a web app. As we progress through the tutorials we will be adding various features such as authentication, JWT support, spec testing, some database tweaking and devops/CI/deployment to a cloud provider such as Heroku.

High level of our API’s purpose.

We can aggregate data from anything with an HTTP interface. We could aggregate crashes in a mobile app or even aggregate player data from a game we built so we could determine where players die a lot, where they give up, average level time, etc. The possibilities are endless.

Generating our code

First we will be setting up a new Ruby on Rails project, stripped down to the bare essentials needed for a JSON API. Since we are just using this strictly as an API, we will pass in the --api to the command line as follows:

> rails new data_analytics --api

This will bootstrap our base project configured for our API needs. It does a few things behind the scenes.

  1. Configures our app by limiting the amount of needed middleware.
  2. Our main ApplicationController will now inherit from ::API
  3. Adjusts the generators to skip generating views, helpers and assets.

Official notes can be found here: Link


Setup Check

Lets test basic project for a simple default API call.

All of our application routing can be found in ~/config/routes.rb. It will be empty for now, but this is where our application will know how to map HTTP verbs (GET, POST) and URL’s to our corresponding controllers and actions.

/config/routes.rb

Lets add a default route, that will act as our initial api route. We can use this as a health check endpoint, heartbeat, etc. Add the following code to routes.rb. The format goes ControllerName#Action.

Now lets add an action to the controller, called index , that will handle this request. In /controllers/application_controller.rb, lets add our action.

This is the basis of handling an HTTP request. Rails will use the url we defined in routes.rb, see that incoming request of localhost:3000, and because we defined our root as mapping to the index action of the application controller, Rails will route our request to execute that index action.

We can test this by using a cURL GET request:

> curl localhost:3000
{"status":200,"message":"Hello World"}

Or in Postman:

Victory! We have established a default entry point into our API, confirmed the server is responding to requests and return JSON.


Models

Now before we go scaffolding a bunch of models and controllers we need to think about what we need. Our goal is to have various web accessible applications log data into the system through our RESTful API. We will need to know what application they are representing and what fields are needed. A situation like this fits well into the NoSQL realm, but for brevity we will be using relational databases (Sqlite).

Lets start high level on what our domain models will look like. (Note: We are skipping authentication until later in the tutorial).

> rails generate model App
Running via Spring preloader in process 17580
invoke active_record
create db/migrate/20181002153146_create_apps.rb
create app/models/app.rb
invoke test_unit
create test/models/app_test.rb
create test/fixtures/apps.yml

Here we generate our top level class, App. I do not tell it to create any fields here on purpose. As we want to think about how to lay out our model. Rails is very good at doing a lot of behind the scenes work for us, put its good sometimes to manually set up some things so we can learn.

When we generate a model we also generate a corresponding migration file, which will define the schema for our ActiveRecord model. Open up the newly created migration and add the following.

Run the following command on the command line. This will tell the Rails engine to run our pending migrations and get out database up to speed.

> rails db:migrate
== 20181002153146 CreateApps: migrating =======================================
-- create_table(:apps)
-> 0.0015s
== 20181002153146 CreateApps: migrated (0.0016s) ==============================

When we view our sqlite database (using any sqlite db tool) we can see our schema has been updated with our new model, App. Its also important to note that Rails tracks are migrations in the schema_migrations table. This is a utility table that allows our application to keep track of all past migrations and when we need to run db:migrate again.

Datum Sqlite Explorer

Now lets tell our ruby model to ensure validation on our unique constraints! Add the following to our model:

app/models/app.rb

Great! Now we will work on building our initial controller and resource routes.


Controllers

Controllers are going to be the main handler from when we take our initial url and when we execute the corresponding code. Before we jump into generating our app_controller, lets take a look at what resource endpoints we will need to expose to work with our App.rb model.

Now lets generate our apps_controller. On the command line execute the following statement:

> rails g controller v1/apps
create app/controllers/v1/apps_controller.rb
invoke test_unit
create test/controllers/v1/apps_controller_test.rb

Here we generate a controller and a spec test file. We also namespace our controller under /v1/. This allows us to version our API. Now lets take a look at our new file, /controllers/v1/apps_controller.rb.

Excellent. Now that we have our main CRUD actions defined, we need to add it to our routes.rb file, so Rails knows how to handle the incoming request.

If we run the rake utility we can see all our current HTTP routes.

> rake routes
Prefix Verb URI Pattern Controller#Action
root GET / application#index {:format=>/json/}
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create

Here we can clearly see we have no exposed endpoints for a new app resource. So in /config/routes.rb. Add the following:

Now run our rake utility again to verify Rails as established our new routes:

> rake routes
Prefix Verb URI Pattern Controller#Action
root GET / application#index {:format=>/json/}
v1_apps GET /v1/apps(.:format) v1/apps#show
PATCH /v1/apps(.:format) v1/apps#update
PUT /v1/apps(.:format) v1/apps#update
DELETE /v1/apps(.:format) v1/apps#destroy
POST /v1/apps(.:format) v1/apps#create

Now, some quick tests. Open up Postman, or any HTTP helper, and lets issue a request to our create endpoint.

POST /v1/apps

Here we issue a POST request to our CREATE endpoint, with a json payload of our data. Our API issues a 201 response and returns the created payload, complete with our unique ID and timestamps.

If we try to issue the same request again, our validators will fire and provide a clean response back to us:

POST /v1/apps

Sweet victory! Next tutorial we will be writting some simple spec around our controllers to verify their status.