Making a REST API with Ruby
Building a simple yet thorough REST API with Ruby on Rails
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