Introduction

A Ruby on Rails web developer? Have you taken an iterative (agile) approach to web development yet? If you haven't, it's about time to get started.

This tutorial will show you exactly, how to build an RESTful API from scratch using Rails 6. As with most of my tutorials, we will also follow basic test driven development (TDD) principles, writing tests before writing any code. We will learn how to build the API, version it, and later add both non-breaking changes (things that don't require a new version) as well as breaking changes. Lastly, we will also learn how to create an API client and cover advanced topics like signing your API requests.

Ready to get going? Let's get started with what an API is.

What is API and what are we building?

An API is an application programming interface. As web developers, you will know that to utilize some Google services like search or trends data, we get our app to connect to a relevant Google API.

The API we will be building is a human resource app that is geared towards helping a company manage its user relationships. The API we will be creating provides the ability to track and store relevant information about a user. In other words, our app will track the user's contact information, incoming as well as outgoing communications, and a lot more.

Before we can start building our API, we need to figure out what the business requirements are. Generally speaking, for an app like this, we want it to...

Allow access to clients

With an API, we can enable access to multiple clients that can store data and interact with our API. A client may have one or more users that can each add their own data and read/write to their client's stored data.

User data storage

We wish to track a user's first name, last name, current address, current phone number, and current email address. We should have a way to store what type of address, phone number, and email address it is. In addition, we want to store things like previous addresses, phone numbers, and email addresses as well.

Audit history via log

Our API should be able to log every transaction occurred during usage of the API to store historical changes for audit purposes.

Communications

Our API should provide a mechanism to take notes on communications with the user.

Administrative access and control

Our API should have a way to add and remove clients, users, and all of their data. There should be a way to either flag a record to delete or completely delete a record, allowing more than one delete option for your clients.


What models should we create?

The next thing we need to do is, to look at creating models based on what we want in the API.

  • Client - Possible database fields: first_name, middle_name, last_name.
  • API Key - Possible database fields: client_id, title, and key. API keys represent the users of our system. The client_id stores the client's id. In order to fulfill the Administrative API part, we will make our client_id nullable. When the client_id is set to null, it is assumed that the API key belongs to an admin. The title column allows us to name each API key and the key column will be where the api key is stored.
  • User - Possible database fields: client_id, first_name and last_name. The User model stores information about the user. By looking at the requirements above, we know that a user has multiple addresses, phone numbers, and email addresses. This rules out storing them in the users table. However, we can still store the name of the user in the first_name and last_name fields.
  • Address - Possible database fields: user_id , address1, address2, city, county, state, zip_code, country, address_type, primary. The Address model gives us a way to manage each address that belong to each user. Those fields are just the standard address fields that are commonly used.(Feel free to modify them per your local address structure).The address type as you can see, allows us to determine what type of housing or neighborhood the property is located in. Lastly, we have a boolean field called primary that allows admins to determine whether or not its the default address for a user.
  • Email - Possible database fields: user_id, address, email_type, primary. The Email model allows us to manage the user's email addresses. We will store the email address in the address field, the type of email address in the email_type field, and whether the email is the user's primary contact address or not in the primary field.


Now that we've covered all of the possible models, let's look at the controllers and actions that our API will require.

  • Clients - used to manipulate clients. Only accessible via admins.
  • index - example: GET /clients - Returns a list of the clients.
  • create - example: POST /clients - Creates a new client.
  • update - example: PATCH /clients/1 - Updates an existing client.
  • destroy - example: DELETE /clients/1 - Deletes an existing client.
  • API Keys - The API Keys controller is used to manipulate API keys. Clients can manipulate API keys for themselves, administrators can manipulate API keys for anyone.
  • index - example: GET /api_keys - Returns a list of the clients.
  • create - example: POST /api_keys - Creates a new API key.
  • update - example: PATCH /api_keys/1 - Updates an existing API key.
  • destroy - example: DELETE /api_keys/1 - Deletes an existing API key.
  • Users - The users controller is used to manipulate user data. Clients can only manipulate their own data. Admins have no access.
  • index - example: GET /users - Returns a list of the users for the client.
  • create - example: POST /users - Creates a new user for the client.
  • update - example: PATCH /users/1 - Updates an existing user.
  • destroy - example: DELETE /users/1 - Deletes an existing user.
  • Addresses - The addresses controller is used to manipulate addresses for a user. Address requests are nested under the users controller.
  • index - example: GET /users/1/addresses - Returns the addresses for the specified user.
  • create - example: POST /users/1/addresses - Creates a new address for the user.
  • update - example: PATCH /users/1/addresses/1 - Updates an existing address for the user.
  • destroy - example: DELETE /users/1/addresses/1 - Deletes the specified addresss from the user.
  • Emails - The emails controller is used to manipulate emails for a user. Email requests are nested under the users controller.
  • index - example: GET /users/1/emails - Returns the emails for the specified user.
  • create - example: POST /users/1/emails - Creates a new email for the user.
  • update - example: PATCH /users/1/emails/1 - Updates an existing email for the user.
  • destroy - example: DELETE /users/1/emails/1 - Deletes the specified email from the user.
  • Communications - The communications controller is used to manipulate communications for a user. communication requests are nested under the users controller.
  • index - example: GET /users/1/communications - Returns the communications for the specified user.
  • create - example: POST /users/1/communications - Creates a new communication for the user.
  • update - example: PATCH /users/1/communications/1 - Updates an existing communication for the user.
  • destroy - example: DELETE /users/1/communications/1 - Deletes the specified communication from the user.

Once we have those controllers taken care of, we are ready to get started with creating a Rails app.

Rails Application Setup


To create a rails app, make sure you have Ruby and Ruby on Rails installed.

If you have the packages installed, you can go ahead to your terminal and type

rails new MyAPIapp --api 

Once you have that in there, press enter and that will create a folder called MyAPIapp with your Rails API files.


Now let's switch to our APIExample project by using the cd command.

cd MyAPIapp

Now that we've created a new project, we need to add a few gems to our Gemfile. The gems we will be using in this example are listed below.

  • rails-controller-testing - The rails-controller-testing gem provides some extra methods to make writing controller tests easier.
  • database_cleaner - The database_cleaner gem keeps our database clean during test runs.
  • rspec-rails - RSpec is a popular Ruby testing framework. The rspec-rails gem provides Rails specific functionality enhancements.
  • factory_girl_rails - The factory_girl_rails gem is a Rails wrapper for the popular FactoryGirl gem. FactoryGirl provides a way for us to create Factories that can be used to generate data for our tests as well as our development environment.

Now that we know what these gems do, let's add them to our Gemfile. Open up your Gemfile and modify it so that it looks like the code listed below.

source 'https://rubygems.org'
   
  git_source(:github) do 
    |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") "https://github.com/#{repo_name}.git" 
  end 
  
  gem 'rails', '~> 6.0.1'
  
  gem 'sqlite3'
  gem 'puma' group :development, :test do
    gem 'byebug', platform: :mri
    gem 'listen'
    gem 'spring'
    gem 'spring-watcher-listen'
    gem 'rspec-rails'
    gem 'factory_girl_rails'
    gem 'rails-controller-testing'
    gem 'database_cleaner'
  end 



When you're ready, let's run bundle to install the gems.

bundle install

Next we need to configure RSpec. Run the command below to add configuration files for RSpec.

rails g rspec:install 

This will create the spec folder along with a few configuration files. Now if you look at the folder, you will see a test folder that we can delete because it is no longer used.

rm -rf test/ 

Now that we have RSpec installed, we need to configure our other gems. First, create a folder called support in the spec/ folder. The command below will do this for you.

mkdir spec/support 

Now we need to make a two changes to our spec/support/rails_helper.rb file. The first change we need to make involves telling Ruby to load all *.rb files in the spec/support folder when the spec/support/rails_helper.rb file is loaded. This is important because we can add custom files to spec/support and have them get included when RSpec is initialized. An example of what this code looks like is below.

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

The next thing we need to do is change the config.use_transactional_fixtures setting to false. This tells Rails not to use database transactions when running each of our tests.

We are making this change because DatabaseCleaner will manage our database cleaning strategy, so we want Ruby on Rails to stay out of the way.

Below is the final spec/rails_helper.rb file with both changes made and all of the comments removed for easier reading.


spec/rails_helper.rb:

ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) abort("The Rails environment is running in production mode!") if Rails.env.production? require 'spec_helper' require 'rspec/rails' Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = false config.infer_spec_type_from_file_location! config.filter_rails_from_backtrace! end 

Now let's configure support for FactoryGirl. Create a new file called factory_girl.rb inside spec/support and modify it so that it looks like the code listed below.

RSpec.configure do |config| 
  config.include FactoryGirl::Syntax::Methods
end 

The code above simply includes FactoryGirl's functionality in with RSpec. For example, this means we can run create(:client) for instance to create a new client.

Now we are ready to configure Database Cleaner. Create a new file in the spec/support folder called database_cleaner.rb and modify it so that it looks like the following code.

RSpec.configure do |config| 
  config.before(:suite) do 
    DatabaseCleaner.clean_with(:truncation)
  end
  config.before(:each) do 
    DatabaseCleaner.strategy = :transactionend
 config.before(:each) do 
   DatabaseCleaner.startend
 config.append_after(:each) do 
   DatabaseCleaner.clean 
 end
end 

What does this code do? Lines 2-4 tells Database Cleaner to wipe the database before running the test suite. Lines 6-9 configures Database Cleaner to use a transaction for each of our tests. This means no data actually gets committed to our test database. Lines 11-13 tells DatabaseCleaner to clean our database after each test. If we were building a regular Rails application, we would have more code that tells Database cleaner to have different behavior whenever integration tests are run.


Creating Client and API Key Models

The next thing we have to do is create our Client and ApiKey models. First let's create the client model. The client model has 1 column, name. Run the command below to create the Client model now.

rails g model Client name 

Now let's create our ApiKey model. The ApiKey model has 2 columns, name and key. Run the command below to create it now.

rails g model ApiKey client:references name key 

Now let's migrate the database. Run the command below to do this now.

rails db:migrate 

What's next? It's time set up our factories.


What is a factory you might ask? A factory is a piece of code that generates data for use in our tests. We can also use factories to ease the creation of seed data in our development environment. Factories become extremely useful as the application grows, since they can be used to keep business logic out of our tests. For example, instead of manually creating a client and then adding the API keys to it, you can simply call something like create(:client, :with_api_keys) and the code in your factory would take care of the rest.


First let's create the Client Factory. Open up the client factory in spec/factories/clients.rb and modify it so that it looks like the code below.

FactoryGirl.define do factory 
  :client do
    sequence(.:name) { |n| "Client #{n}" }
  end
end 

The sequence function listed above is a helper provided by FactoryGirl that automatically counts up as you create more and more models. So calling create(:client) the first time would yield a client with the name "Client 1". Calling create(:client) the second time would yield a client named "Client 2", and so on.

Great, now let's set up the factory for the ApiKey model . Open up the ApiKey factory in spec/factories/api_keys.rb and modify it so that it looks like the code below.

FactoryGirl.define do
  factory :api_key do
    association :client, strategy: :build sequence(:name) { |n| "User #{n}" }
    trait :admin do client { nil } endend
end 

Let's break this down:

Line 3 specifies that ApiKey has an association with Client. When you run create(:api_key) an associated client would get automatically created unless you override it. The strategy: build option tells FactoryGirl not to create the association unless we use the create method or call save on the model. The default behavior is for FactoryGirl to create the association in the database even when we call build. This is not something we want to happen.

The trait function at lines 6-8 specify variations on the default factory. In this instance, calling create(:api_key, :admin) would use the admin trait to set the client_id to nil, hence giving us an API key that has admin capabilities.

Now that we have our database and factories ready, it's time to start implementing our API. Our first task is to set up the relationships between our models and add validation. Let's start with the Client model. We know that the client has many API keys, in addition, we should make name a required field. Knowing this let's write some tests. What tests do we need? First, we need to test whether the factory we just created is valid. It's a good idea to test all of your factories and make sure they are valid. This becomes important as your application grows in complexity. We also need to write a test for the relationship with API keys. Finally, we need to write a test to check that a name is required. Let's add these tests now. Open up the Client model spec in spec/models/client.rb and modify it so that it looks like the code below.

spec/models/client_spec.rb:
   require 'rails_helper' RSpec.describe Client, type: :model do let(:client) { build_stubbed(:client) } it 'has valid factories' do expect(build_stubbed(:client)).to be_valid end it 'has many api keys' do subject = Client.reflect_on_association(:api_keys) expect(subject).to_not be_nil expect(subject.class_name).to eq('ApiKey') expect(subject.macro).to eq(:has_many) end describe 'validations' do it 'requires that a name be present' do client.name = nil expect(client).to_not be_valid end end end 
 

Let's break this down:

Line 4 uses a method called let. The let method defines a specific piece of code that can be used by your tests. By default, the code specified by let doesn't actually run until it is referenced in the test. Note things get reset after each test, so even if you did something like client.name = 'foo' in 1 test, it wouldn't affect the way the other tests work. The let function also has a sibling called let!. The only difference between the two is the let! code gets run right away before the test starts.


At line 4 you will also notice that we use a function called build_stubbed here. The build_stubbed function assigns an id to the object, but doesn't actually save object to the database. Functions like persisted? would then return true if called. As a result, using build_stubbed can make your tests run much faster. Note that any attempts to manipulate the database will fail, therefore, build_stubbed is only useful the functionality you are testing doesn't perform database operations.

The first test at lines 6-8 checks whether our factory is valid. It's a good idea to always test all your factories to make sure they are valid on creation. This prevents problems with the factories or your code when things change.

The second test at lines 10-15 verify that the relationship to the ApiKey model is set up properly. We include this test since it's a good idea to always test for proper model relationships. The reflect_on_association method returns information about a specific association in your model. Line 12 checks whether the method returned nil or not. A nil return value would indicate that the relationship does not exist. Line 13 checks that the relationship is tied to the 'ApiKey' model. Finally, line 14 tests for the type of relationship.

The final test at lines 18-21 checks that the name is required. If we run our client model spec now via bundle exec rspec spec/models/client.rb we'd end up with a failing test. Let's fix that. Open up your client model at app/models/client.rb and modify it so that it looks like the code listed below.

app/models/client.rb:
   class Client < ApplicationRecord validates :name, presence: true has_many :api_keys end 

If we re-run the client model spec now via bundle exec rspec spec/models/client.rb it passes! Let's move on.

The next thing we should work on is our ApiKey model, so let's talk about what we need for that. We know that our ApiKey model has 3 fields, client_id, name, and key. We also know that the client is optional while both name and key are required. We should also probably add a uniqueness validator for our API key. What else do we need? Well for starters, we need a way to generate an API key. For our key, we will generate a random 64 character string. We also need a way to tell whether the API key belongs to an admin or not. We could easily check if client_id is nil, but this could cause an issue if we decide to change the way we do authorization down the road. So let's create a method in our ApiKey model called admin? The admin? method will return true if the user is an admin or false if the user is not.

Now that we know what we need to do, let's start by writing some tests. Open up the ApiKey model spec in spec/models/api_key_spec.rb and modify it so that it looks like the code listed below.

spec/models/api_key.rb:
   require 'rails_helper' RSpec.describe ApiKey, type: :model do let(:api_key) { build(:api_key) } it 'has valid factories' do expect(build_stubbed(:api_key)).to be_valid expect(build_stubbed(:api_key, :admin)).to be_valid end it 'has a relationship with Client' do subject = ApiKey.reflect_on_association(:client) expect(subject).to_not be_nil expect(subject.class_name).to eq('Client') expect(subject.macro).to eq(:belongs_to) end describe 'validations' do it 'does not require a client' do api_key.client_id = nil expect(api_key).to be_valid end it 'requires a name to be present' do api_key.name = nil expect(api_key).to_not be_valid end it 'requires a key to be present' do api_key.key = nil expect(api_key).to_not be_valid end it 'requires a unique key' do api_key2 = create(:api_key, key: api_key.key) expect(api_key).to_not be_valid end end it 'generates an API key with a size of 64 characters' do expect(api_key.key.length).to eq(64) end describe '#admin?' do it 'returns true when the user has not been assigned a client' do api_key.client = nil expect(api_key.admin?).to be_truthy end it 'returns false when the user has been assigned a client' do api_key.client_id = 1 expect(api_key.admin?).to be_falsey end end end
 


Let's break this down:

The first test at lines 6-9 checks that our factories are valid. Notice that we check both the default as well as the variation provided by the admin trait. As mentioned above, it's a good idea to test all of our factories.

The next test at lines 11-16 check whether the ApiKey model has a relationship with Client. You'll notice this code is similar to the test we wrote for client, with the exception of the class name being 'Client' and the relationship being belongs_to

The test at lines 19-22 checks that the client is optional and not required.

The tests at lines 24-32 check that key and name are required.

The test at lines 33-37 checks that the API key is unique.

The test at lines 40-42 checks that our API key is 64 characters long.

Finally, the tests at lines 45-53 define the behavior of our admin? helper.

Now that we have added the tests, let's make them pass. Open up the ApiKey model at app/models/api_key.rb and modify it so that it looks like the code below.

app/models/api_key.rb:
   class ApiKey < ApplicationRecord belongs_to :client, required: false validates :key, :name, presence: true validates :key, uniqueness: true after_initialize do |api_key| api_key.key = SecureRandom.hex(32) if api_key.key.blank? end def admin? client_id.nil? end end
 

Let's break this down:

Line 2 defines the relationship with Client. Notice the inclusion of the required: false parameter. This is because Rails, starting with version 5, now requires the presence of a model in the relationship unless you tell it not to.

Line 3 contains the validations that make both key and name required fields.

Line 4 contains a validation that requires key to be unique.

Lines 6-8 define an ActiveRecord callback called after_initialize. The after_initialize callback runs when an instance of the model is instantiated. This happens whenever the model is either loaded from the database or created using .new or .create. Because of this, line 47 generates our API key, but only if it's blank. It does this using the hex method on the SecureRandom class. SecureRandom.hex generates a random hexadecimal string of the specified length. The actual length of the string is twice of what is specified because it is in hex format (00-FF * 32 in this case). The string in question always contains the numbers 0-9 and a-f. Other letters or symbols are not used.


Lines 10-12 define the admin helper function. If the client_id is set to nil, The admin? function returns true. Otherwise it returns false.

If you run your test suite now via rake or bundle exec rspec you should end up with 100% passing tests.


To be continued...