Test Driving a Rails API - Part One
Tags: rails, restapi, ruby, postgres
Published: 2024-03-12 03:32:25

## Part 1: Setup the Environment and Create a Rails Application

### Intro to REST APIs

In this tutorial, I will show you how to create an [API-only application with Ruby on Rails](https://guides.rubyonrails.org/api_app.html). While Rails is often used to create monolith web applications that serve server-rendered HTML pages, its versatility and rich feature set make it a great choice for building API applications.

The demand for highly interactive browser applications has prompted a surge in JavaScript front-end frameworks. These frameworks allow developers to shift a significant portion of application logic to the client side, using a RESTful API for interactions with the back-end application server. A web application where JavaScript renders the UI in the browser, which is highly interactive and communicates with the server through an API, is often called a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application).

In this tutorial, we’ll build a [REST](https://en.wikipedia.org/wiki/REST) API, sometimes called a web API or JSON API. An [API](https://en.wikipedia.org/wiki/API), or application programming interface, is a way for two applications or pieces of software to communicate with each other.

REST APIs allow a client application, typically a SPA running in a web browser, to communicate with the backend application server, which handles the application’s data storage and persistent state.

The client makes [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) requests to fetch data from and save data back to the server. The payload of those requests will contain representations of the application’s data. Those representations will often be formatted as JSON, as will be the case here.

Architecting a web application with an interactive client side alongside a REST API presents both advantages and challenges. A primary benefit is that it allows you to use the same API to build any number of clients: web, mobile, desktop, etc. Additionally, this approach facilitates a clear separation of concerns, decoupling the UI from the data storage and authentication concerns. 

Combining the API and client-side components within a single codebase is feasible, albeit with its own set of considerations. However, in this tutorial, we'll develop the API within its own distinct codebase. Subsequent articles will delve into constructing front-end applications that consume the API using various frameworks, providing a comprehensive understanding of both ends of the development stack.

In 2024, there are a myriad of options for building a REST API, and Rails remains high on my list. Rails’ extensive feature set and great developer experience allow you to get up and running with an API very quickly.

### About the API

The API we’re about to build will allow us to manage events on a Calendar. Through this API, we’ll be able to create, read, update, and delete calendar events. It’s a [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) API, but the complexities of calendar events will introduce some additional logic to make things a little more interesting. We’ll use tests to guide us as we develop our API.

Clone [the repository](https://github.com/jmflannery/cal_api) yourself and follow along.

### Install prerequisites

Before we can create the app, we need to install Ruby, the Rails gem, and the PostgreSQL database.

### Install Ruby and Rails

Let’s get started. I prefer to manage my Ruby installations on my development machine with [chruby](https://github.com/postmodern/chruby) paired with [ruby-install](https://github.com/postmodern/ruby-install).  Another outstanding set of tools is [rbenv](https://github.com/rbenv/rbenv) with [ruby-build](https://github.com/rbenv/ruby-build). I highly recommend installing Ruby with one of those two sets of tools. Follow the instructions on their project’s READMEs. For this article, I’ll be running Ruby (MRI) v3.3.0.

```bash
$ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-darwin23]
```

With Ruby installed, you can now manage your [RubyGems](https://rubygems.org/) with the `gem` command. Install the Rails gem or update if you already have it.

```bash
$ gem install rails
```

or

```bash
$ gem update rails
```

For this article, I’ll be running Rails v7.1.3:

```bash
$ rails -v
Rails 7.1.3
```

### Install PostgreSQL

A running Rails application needs a database to connect to. You may already have your database of choice installed, but if not, I recommend [PostgreSQL](https://www.postgresql.org/), or Postgres for short. On a Mac, probably the easiest way to install it is with [Posrgres.app](https://postgresapp.com/). Another option, the one I prefer, is to use [Homebrew](https://brew.sh/). With Homebrew installed, this command will install PostgreSQL version 16 along with [libpq](https://www.postgresql.org/docs/current/libpq.html):

```bash
$ brew install libpq postgresql@16
```

When the install finishes, it will display several Caveats to the `postgresql@16` install, a few of which are of interest to us; first:

```bash
If you need to have postgresql@16 first in your PATH, run:
  echo 'export PATH="/usr/local/opt/postgresql@16/bin:$PATH"' >> ~/.zshrc
```

While not required for a Rails app to connect Postgres, it is nice to have Postgres’ bin directory in your path because it contains several useful executables for interacting with Postgres, including `postgres`, `psql`, `createdb`, `createuser`, and `pg_dump`.

Add the Postgres bin directory to your path by adding this line to the shell configuration file that loads on shell startup:

```bash
export PATH=/usr/local/opt/postgresql@16/bin:$PATH
```

Or you can do that with the following command in your terminal:

```bash
$ echo 'export PATH=/usr/local/opt/postgresql@16/bin:$PATH' >> ~/.bashrc
```

Then, reload the file so that it takes effect.

```bash
$ source ~/.bashrc
```

The other, more important caveat is:

```bash
To start postgresql@16 now and restart at login:
  brew services start postgresql@16
Or, if you don't want/need a background service you can just run:
  LC_ALL="C" /usr/local/opt/postgresql@16/bin/postgres -D /usr/local/var/postgresql@16
```

I prefer to manage running Postgres with `brew services`. The following command will start up Postgres in the background now and automatically every time your Mac starts up.

```bash
$ brew services start postgresql@16
```

This way, you mostly won’t have to worry about starting and stopping Postgres, it will always be running in the background. However, you can always restart, start, and stop Postgres with the following `brew services` commands:

```bash
$ brew services restart postgresql@16

$ brew services stop postgresql@16

$ brew services start postgresql@16

$ brew services list
Name          Status  User File
postgresql@15 started jack ~/Library/LaunchAgents/homebrew.mxcl.postgresql@16.plist
```

### PostgreSQL databases and roles

With Postgres installed, we can now create any number of databases. A database named `postgres` was created for us. Later, we’ll use Rails’ built-in task to create our application’s development and test databases.

Before Rails can begin interacting with Postgres, we need to specify the database user through which it can access the database. When we installed Postgres, a user (called a role in Postgres) with superuser privileges was created for us, with the same name as our current operating system user. My OS username is `jack`. Therefore, I now have a Postgres role named `jack`. This can be our app’s database user. Optionally, you can set a password for your user. To do so, launch [psql](https://www.postgresql.org/docs/current/app-psql.html), the command-line interface (CLI) tool for interacting with Postgres:

```bash
$ psql -d postgres
```

Because we did not specify one, `psql` will attempt to connect as a role under the same name as your OS user. You should have such a role, so there is no need to specify it. `psql` will also attempt to connect to a database with the same name as your OS user. In this case, we do not have such a database, so we must pass the `-d` flag to connect to the database named `postgres`.

At the prompt, use the `\password` command to set a new password:

```bash
postgres=# \password
Enter new password for user "jack": 
Enter it again: 
postgres=# \q
```

Use `\q` to quit `psql`.

Alternatively, you could create a dedicated Postgres role for each application you’re developing. But on your development machine, sharing the same Postgres role for all apps under development is fine.

### Configure bundler for the pg gem

If you installed Postgres via Homebrew, you need to configure bundler so that when it installs the pg gem, it knows where to find the [pg_config](https://www.postgresql.org/docs/current/app-pgconfig.html) executable, which is installed as part of Postgres. The [pg gem](https://github.com/ged/ruby-pg) is the Ruby interface to Postgres and requires `pg_config` during installation. We can use this command to configure `bundler` so that it can find it and successfully install `pg`.

```bash
$ bundle config set build.pg --with-pg-config=/usr/local/opt/postgresql@16/bin/pg_config
```

### Generate the application

This command will generate a new Rails API-only application named `cal_api` using PostgreSQL as the database.

```bash
$ rails new cal_api --api --database=postgresql
```

With that, our app is generated, all dependent gems are installed, and a new git repository is initialized. You can now `cd` into the app’s directory.

### Configure the database connection

Rails needs to know how to connect to your database and expects to find connection information in the `config/database.yml` file. When the app was generated with the `--database=postgresql` flag, a `database.yml` file tailored for Postgres was created for us. It already contains everything we need to get started; we don’t actually need to specify a database user because Postgres will automatically connect as the Postgres role with the same name as our OS user. However, in the name of making the app more configurable, I prefer to set all of the database config values explicitly. Open `config/database.yml`, and I recommend reading through and understanding the comments; some valuable information is within. Then replace all of it’s contents with the following:

```yaml
default: &default
  encoding: unicode
  adapter: <%= ENV['DB_ADAPTER'] %>
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  host: <%= ENV.fetch('DB_HOST') { 'localhost' } %>
  port: <%= ENV['DB_PORT'] %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>

development:
  <<: *default
  database: cal_api_development

test:
  <<: *default
  database: cal_api_test

production:
  <<: *default
  database: cal_api_production

```

As you can see, there are almost no actual values; the values will come from the environment. Rails will automatically process the ERB when it reads the file. We can keep this in the git repo because it doesn’t expose private information.

### Install dotenv

Storing environment variables for a Rails app can be problematic. The [dotenv](https://github.com/bkeepers/dotenv) gem will automatically, when Rails boots, load environment variables from `.env` files into the Rails `ENV`. This is a great way to store private information that varies per developer or deployment environment, such as your development database configuration. Rails Encrypted Credentials is a great way to store private information, like API keys, etc, but I wouldn’t use it for storing my local development environment’s database information. The Encrypted Credentials file is checked into the git repository and would, therefore, be shared by all developers on the project. `dotenv` allows each developer or deployment environment to store their own information in `.env` files that are ignored by git.

To install `dotenv` add it to your `Gemfile`'s `development` and `test` groups.

```ruby
group :development, :test do
  gem 'dotenv'
end
```

Install the gem:

```bash
$ bundle install
```

A common strategy is to simply store all environment variables in a `.env` file that is kept out of the git repository. That works fine, however, I like to follow the [advice on the dotenv readme](https://github.com/bkeepers/dotenv?tab=readme-ov-file#should-i-commit-my-env-file) and commit the `.env` file to the repository. We’ll take advantage of the [precedence dotenv follows when loading .env.* files](https://github.com/bkeepers/dotenv?tab=readme-ov-file#customizing-rails) and have each developer maintain a `.env.local` file, which we will keep out of the repository. The `.env` file will contain all of the variable names without any values and will serve as an example. Any new developer coming onto the project will make a copy of `.env` named `.env.local` and fill in their own specific database configuration. Rails boots, the values in `.env.local` will take precedence over those in `.env`.

First, because by default `dotenv` will not load the `.env.local` file in the test environment, add the following line of code to your `config/application.rb` file:

```ruby
Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test"
```

 Be sure to add the new line right under the line `Bundler.require(*Rails.groups)` near the top of the file like this:

```ruby
# config/application.rb

Bundler.require(*Rails.groups)

# Add this line to load .env.local in test
Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test"
```

Next, create a file named `.env` at the top level of the project and add the following:

```bash
DB_ADAPTER=
DB_USERNAME=
DB_PASSWORD=
DB_PORT=
```

Then make a copy of `.env` called `.env.local` with your values filled in:

```bash
DB_ADAPTER=postgresql
DB_USERNAME=jack
DB_PASSWORD=secret
DB_PORT=5432
```

These four variables must be defined, otherwise Rails will not have a complete database configuration. It's worth it to make these variables required and have Rails fail early on startup if they are not defined. Create the file `config/initializers/dotenv.rb` and add the following contents:

```ruby
if Rails.env.development? || Rails.env.test?
  Dotenv.require_keys("DB_ADAPTER", "DB_USERNAME", "DB_PASSWORD", "DB_PORT")
end
```

This way, if the variables are not defined, Rails will fail with a clear error message.

Ensure that `/.env.*` is listed in your `.gitignore` file. The base generated `.gitignore` file should have the entry `/.env*`, which will cause git to ignore `.env`. 

Open your `.gitignore` file and change the line

```bash
/.env*
```

to

```bash
/.env.*
```

With that, git will now see the `.env` file, and you can commit it to the repository.

### Create the Databases

If your database is configured properly, you can use the Rails built-in task to create the application's databases. Run the following command in your terminal:

```bash
$ rails db:create
```

That command should have created the development and test databases, as named in the `database.yml` file.

### Wrapping up Part One

There’s a lot to think about and understand when building an API with Ruby on Rails. Our app is all setup, and now we’re ready to get to the real meat and potatoes of the project. I’m going to break this post up into two parts, otherwise it’d be far too long. In Part Two, we’ll get to building our models, designing the database schema, and building out the API endpoints. We’ll be writing tests along the way and using them as a guide to drive our development. See you in Part Two.