Making a REST API with Ruby

Making a REST API with Ruby

Building a simple yet thorough REST API with Ruby on Rails

ยท

8 min read

Hello everyone! This TinyApp is called SuperServer - it's an API that serves food data.

A quick refresher on what an API does: An API is an application that speaks to another application. Most current APIs (as of 2021) will "serve" data in JSON. JSON is understood by all sorts of languages, so it's perfect to send data between a front-end and a back-end.

So what's REST? It's Representational State Transfer - but knowing what it stands for doesn't help us much. There are more details on this in the Further Readings section at the bottom of this post, but for our purposes, know that our REST API can Create, Read, Update, and Delete records in a database when it listens to an HTTP request.

In this post, we're going to create a REST API that serves some food data. This way, anyone can consume our API and get food data from our database. I couldn't help but food theme this TinyApp since we're working on the server-side, and it's "serving" food as data. I'm funny, I know.

Get on the Rails ๐Ÿ›ค

This TinyApp uses Ruby on Rails. I won't spend much time here, but here's a quick lowdown on why I'm using Rails for this TinyApp:

  • It's expressive. Even if you don't have Ruby knowledge, you'll be able to understand this TinyApp conceptually.
  • Rails is opinionated and removes a lot of configuration headache that happens with other frameworks / languages (I'm looking at you, NodeJS).
  • The TinyApp isn't about the exact code used, it's about the concepts around building an API, and Rails abstracts this brilliantly.

With the backdrop set, let's start building SuperServer!

Create the Rails App for API only ๐ŸŽจ

Normally when we create a Ruby on Rails application, it assumes that we want to build a full stack application with a front-end and a back-end. Since we only want to serve data, we don't need all the middleware that comes with an application that renders a view layer.

To keep our bundle size focused and without any bloat, let's make our Rails API with the following command. (This assumes you have Ruby and Rails installed on your computer).

$ rails new superserver --api

If we cd into this directory from the terminal, we'll need to bundle up the assets, create our database locally, and start the Rails server with the following three commands:

$ bundle install
$ bundle exec rake db:create
$ bundle exec rails serve

MVC ๐Ÿ•บ

Ruby on Rails (and much of web development) uses the Model View Controller approach to building applications. Let's break it down briefly:

  • model -> defines the data structure. In this TinyApp, we're storing and retrieving food data.
  • view -> typically this is the layer that displays something to a web page. Since this TinyApp is an API, we're just rendering JSON. Therefore, there's no view layer.
  • controller -> this is the "brain" of an MVC application. The controller is responsible for business logic - it uses models and tells the view what to show. In this case, our controller handles what food data gets served up as JSON.

Make a Food Model ๐ŸŒฎ

Well, since we're serving food data, we want to create a model that represents Food in our application. This will allow us to query and interact with Food objects. What kind of data will be attached to a Food? Let's make it simple.

A Food will have a name, the spice_level of the dish, and the cuisine of the dish.

In the models folder, we'll create a new file: food.rb. The Food class inherits from ApplicationRecord which is a Rails class. By inheriting ApplicationRecord, we're telling Rails that this a data model.

class Food < ApplicationRecord
  validates :name, presence: true
  validates :spice_level, numericality: { only_integer: true }
  validates :cuisine, presence: true
end

We're adding "model-level validations" to this model to make sure that the application uses the right kind of data when it makes a Food object. You can read more about validations in the Further Reading link, but these ones translate to the following in English:

  • Let's validate our name and make sure that it's present when we create a new Food.
  • Let's make sure that a spice_level exists when we create a new Food, ensuring that it's a number, specifically, an integer.
  • Let's validate that the food's cuisine is present when we create a new Food.

With these validations, we'll get rich error messages if anything goes wrong in creating a new Food object.

Make a Food Migration ๐Ÿฅž

While we have a Food model that our Rails application can understand, we don't actually have a table in our database that knows about foods. The purpose of database migrations is to instruct our database with what table to create.

Luckily, we don't have to do much from scratch. With Rails, we can generate a migration from the terminal:

bundle exec rails generate migration create-food

In the db/migrations folder we'll find a migration file. Let's add all the fields of a food record:

class CreateFood < ActiveRecord::Migration[6.1]
  def change
    create_table :foods do |t|
      t.string :name, null: false
      t.integer :spice_level, null: false
      t.string :cuisine, null: false
      t.timestamps
    end
  end
end

Translated, we're defining:

  • A string called "name" which cannot be null
  • An integer called "spice_level" which cannot be null
  • A string called "cuisine" which cannot be null
  • Timestamps (created_at and updated_at)

To run this migration and create the foods table in our local database, we run:

$ bundle exec rake db:migrate

Make a Food Controller for the API ๐ŸŸ

Super. Now that we have our model set up, and we have an actual foods table in our local database, we can set up the actual functionality of our API. The business logic lives in our controller file. Since we're making this an API, it's best practice to put our controller in a folder called api and then, incase there's another version of our API down the road, a subfolder called v1 for "version 1".

In the api/v1/ folder, we'll make a foods_controller.rb, and it looks like this:

class Api::V1::FoodsController < ApplicationController
  def index
    render json: Food.all
  end

  def show
    @food = Food.find(params[:id])
    render json: @food
  end
end

Similar to the ApplicationRecord that we inherited from when we made our Food model, we're inheriting from ApplicationController.

Then, we have a couple Ruby methods. The first is called index and the second is show. In Rails, these two methods are part of conventions.

What they say is: "When you visit the api/v1/foods path, do whatever is in def index. When you visit the api/v1/foods/{food_id} path, do whatever is in def show.

In index we're rendering some JSON, and returning Food.all. Rails (or more accurately, ActiveRecord, the ORM that Rails uses) runs some SQL under the hood which returns all of our foods as JSON.

In show we're rendering one specific food, based on the id that's present in the url. This id is captured in url parameters. So if we were getting data from api/v1/1 then we would get the JSON data of the food with the id of 1.

Edit Routes ๐Ÿš

In order to have this controller be accessible when the path is requested, we have to tell Rails that it exists. To do that, we'll visit routes.rb. This is where our application's routes are set up.

To do this, we'll create a namespace that tells Rails to look for the controller within the api and v1 folders:

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :foods, only: [:index, :show]
    end
  end
end

We then grab our foods as a resource, and only look for the index and show methods in the controller.

Now, if we start our server and visit localhost:3000/api/v1/foods - we'll see, in JSON, a list of foods. If we visit localhost:3000/api/v1/2 we'll see, in JSON, a single food that has the ID of 2.

Seed Data ๐ŸŒฑ

Just a minute... if we were to go to our localhost:3000/api/v1/foods page, we'd find it totally empty of any foods. That's because we haven't added any data to our database!

There are multiple ways to add data to the database, but the method that we'll take is by editing our seed data. If we head to the seeds.rb file, we can create a bunch of foods to add to the database.

Food.destroy_all

Food.create(name: "Banana Bread", spice_level: 0, cuisine: "American")
Food.create(name: "Fish Taco", spice_level: 4, cuisine: "Mexican")
Food.create(name: "Chicken Tikka", spice_level: 9, cuisine: "Indian")
Food.create(name: "Samosa", spice_level: 8, cuisine: "Indian")
Food.create(name: "Pad Thai", spice_level: 5, cuisine: "Thai")
Food.create(name: "Green Curry", spice_level: 7, cuisine: "Thai")

We first destroy all the data so that the database is clean when we re-seed it. Then we create multiple food objects with all the required fields: name, spice_level, and cuisine. This will add all of these foods directly to the SQL database.

To run this file and seed the local database we head back to our terminal:

$ bundle exec rake db:seed

That's a basic API using Ruby on Rails! This blog post touches on the basics of creating this API with some generous assumptions. There are so many other details to dig into, even with the limited functionality we have in this app. There are other features we can add, tests, and more. Since I didn't want this post's scope to get too out of hand, we'll stop here for now.

Look out for a lot more fun with TinyApp backend apps in the future!

Until next time,

~ Saalik

Further Reading

ย