Duetcode

Implement user creation API endpoint

04 July 2020

Before starting to work on our business domain, we need to run rails db:create command to create development and test databases. While creating databases, make sure that you already installed PostgreSQL, and it’s running on your computer. (You can execute pg_isready command to check)

If you didn’t review the Instapaper service so far, I would suggest to register and use it a little bit before proceeding to read this chapter.

Create ‘User’ model

As we can see on Instapaper, the first thing that comes to mind is creating a User model and providing an authentication system. For the authentication, we have options like sorcery and clearance gems, but I prefer to use devise gem because it’s a battle-tested and well-known gem in the ruby community, which has the comprehensive feature set, and also it’s easy to use.

Let’s add devise gem to Gemfile, and run bundle install command.

Gemfile
# ...

# Flexible authentication solution for Rails based on Warden
gem 'devise', '~> 4.7'

# ...

After that, we need to execute the devise install command.

~/code/bookmarker (master) bundle exec rails g devise:install

  create  config/initializers/devise.rb
  create  config/locales/devise.en.yml

It creates configuration and localization files of the devise. We also need to add default_url_options for the development environment because we need to provide all the details to generate URLs as following for mailers.

<%= url_for(host: 'example.com', controller: 'welcome', action: 'greeting') %>

Instead of passing the URLs’ host value every time we build mailer views, we will set it once with default_url_options.

config/environments/development.rb
# frozen_string_literal: true

Rails.application.configure do
  # ...
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  # ...
end

(For now, we only set it for the development environment, but keep in mind that you need to set also for production environment when you deploy to the production.)

After that, we need to generate a User model by using the devise gem.

~/code/bookmarker (master) bundle exec rails g devise User

  invoke  active_record
  create    db/migrate/20200629151857_devise_create_users.rb
  create    app/models/user.rb
  invoke    rspec
  create      spec/models/user_spec.rb
  invoke      factory_bot
  create        spec/factories/users.rb
  insert    app/models/user.rb
   route  devise_for :users

This command will generate a database migration, which creates users table and also the user model along with spec and factory files.

Creating the User model with the devise also modifies routes.rb and adds devise_for :users line, which adds routes for User resource that provides sign in, sign out, update password, and CRUD operations with view files provided by the devise. But we will not use them since we don’t need view files, and we will use those functionalities behind our API endpoints, so we don’t want to expose devise routes directly.

We created our rubocop ruleset in the previous chapter, but we will iterate the ruleset when we see the points that we want to change along the course. After creating the User model by using devise gem, we have two offenses which we need to keep or remove if we don’t see any advantage of having them.

  • You may realize that the migration file generated by devise has rubocop offenses since its method length is more than seven lines, but it’s better to exclude migration files from this specific rubocop rule because the main point of having this rule is increasing code readability and DSL of the rails migrations is relatively easy to read (unless you intentionally mess it up), so I think we’re good to exclude them.
  • We also had other offenses from missing top-level class documentation cop, but personally, I don’t think it’s sustainable to maintain this ruleset. Besides that point, we should have a goal of naming classes carefully to make them easy to understand without any comment line.

After adjusting those rules, we should have a .rubocop.yml file looks like as following. (You can also remove Style/Documentation block from .rubocop_todo.yml file since we disabled it.)

.rubocop.yml
inherit_from: .rubocop_todo.yml

Metrics/MethodLength:
  Max: 7
  Exclude:
    - 'bin/bundle'
    - 'db/migrate/*'

Style/Documentation:
  Enabled: false

Now, we need to run the database migration created by the devise.

~/code/bookmarker (master) bundle exec rails db:migrate

== 20200629152903 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0156s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0116s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0025s
== 20200629152903 DeviseCreateUsers: migrated (0.0298s) =======================

Our user model is ready to use, but before using it, I would like to write specs for the user model. Some software engineers tend to use TDD, but I want to write specs after I build the intended behavior because I don’t want my software design to be affected by tests. (You can read DHH’s blog post about this topic to get more information.)

Write tests for the user model

Let’s open spec/models/user_spec.rb file, which is generated when we create the user model using the devise. Although we didn’t write validations for the user model, the devise gem automatically adds presence validation to both email and password fields. So we can add validation tests for the email and password fields of the user model.

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:email) }
    it { should validate_presence_of(:password) }
  end
end

Let’s run rspec to make sure both of the tests are green.

~/code/bookmarker (master) $ bundle exec rspec
..

Finished in 0.03387 seconds (files took 0.81182 seconds to load)
2 examples, 0 failures

Some developers might think about why we test devise internals, but we are actually testing our implementation of the devise rather than the devise gem itself. If any other developer changes the authentication library and somehow forgot to validate email and password, those tests will cover and warn the developer.

The second thought might be, “but we already don’t allow null values in database” that’s true, so it will not break our data integrity. Still, to be able to handle errors on the application layer, it’s better to validate them also on the application side, which is handled by the devise for now, but it might be different with another library.

How to design restful API?

With simple words, restful API consists of resources and interactions. We can call anything as a resource, and it’s represented by nouns like a user, project, etc. You can also define processes as a resource as long as you can describe them with nouns like CreatingBankAccount. Resources don’t have to be mapped with any data source. You can think like it’s a way of defining terms in your business domain.

On the other hand, we have interactions for resources that are represented by verbs, and they are approximately mapped with HTTP request methods like GET, POST, DELETE, PUT, PATCH, etc.

So we use both resources and HTTP methods to design restful API. If we have a User resource, our API endpoints might look like this:

GET       /users            # To get users list
POST      /users            # To create a new user
GET       /users/:id        # To get user information with specified ID
PATCH     /users/:id        # To update user information (partially) with specified ID
PUT       /users/:id        # To update user information with specified ID
DELETE    /users/:id        # To delete user with specified ID

I tried to explain how the restful API designed without diving into details so much, but if you want to gain more information, you can check blog post from Thoughtworks about the topic.

Create a BaseController for API

Since we already created our User model, we can start working on user creation endpoint. As a common convention of API controllers, let’s create api/v1 folder (api folder and v1 folder inside of api folder) inside the controllers’ folder and create api/v1/base_controller.rb extended by ApplicationController.

app/controllers/api/v1/base_controller.rb
# frozen_string_literal: true

class Api::V1::BaseController < ApplicationController
end

Rubocop default ruleset will ask you to use nested module/class definitions instead of compact style like Api::V1::BaseController. Even though it’s not a big deal, I want to use the compact style of the definition, so I disabled the rule in the rubocop file.

.rubocop.yml
inherit_from: .rubocop_todo.yml

# ...

Style/ClassAndModuleChildren:
  Enabled: false

So we created a base controller for our API version one. If you’re building a restful API, it’s better to version it in advance so that you can create different folders for the next versions like v2, v3. In addition to that, we created BaseController also because if we want to use the same abstraction (like authentication and error handling) from all controllers for API version one, we can place it to BaseController easily.

Create ‘POST api/v1/users’ endpoint

Before building the endpoint, let’s modify the routes file to have users resource with a create endpoint as following.

config/routes.rb
# frozen_string_literal: true

Rails.application.routes.draw do
  # ...

  namespace :api do
    namespace :v1 do
      resources :users, only: [:create]
    end
  end
end

Now we can build Api::V1::UsersController with a create action.

app/controllers/api/v1/users_controller.rb
# frozen_string_literal: true

class Api::V1::UsersController < Api::V1::BaseController
  def create
    @user = User.new(user_params)

    if @user.save
      return render json: @user, status: :created
    end

    render json: @user.errors, status: :unprocessable_entity
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

Even though we cover success and unprocessable entity cases in the create action, we will implement shared error handling logic for more generic errors like ActionController::BadRequest or ActiveRecord::RecordNotFound in the next chapters. In the following chapters, we will also implement serializers and renderer modules, which will provide a better abstraction for rendering resources and errors. Keep in mind that the action we wrote here will be refactored after we introduce the renderer module.

Again rubocop detected Style/IfUnlessModifier offense for the if block we didn’t write as a single line condition. We can refactor this line as return render json: @user, status: :created if @user.save. However, I prefer having options of using both multiline and single line conditions based on code readability instead of forcing myself to use single line conditions all the time. To do that, we can remove Style/IfUnlessModifier rule from .rubocop_todo.yml and disable it on rubocop.yml file as follows.

.rubocop.yml
inherit_from: .rubocop_todo.yml

# ...

Style/IfUnlessModifier:
  Enabled: false

Now we can send a post request to create any user. (If you want to send the request from the web client application using the browser, it will fail because we didn’t set up CORS yet. We will also implement CORS settings in the next chapters.)

curl --location --request POST 'http://localhost:3000/api/v1/users' \
--header 'Content-Type: application/json' \
--data-raw '{ 
  "user" : {
    "email": "user@duetcode.io",
    "password": "samplepassword"
  }
}'

And API response should look like this:

{
  "id": 1,
  "email": "user@duetcode.io",
  "created_at": "2020-06-13T11:58:54.203Z",
  "updated_at": "2020-06-13T11:58:54.203Z"
}

Before jumping into another topic, let’s write the tests for the users controller. You can generate the spec file with bundle exec rails g integration_test api/v1/users command.

spec/requests/api/v1/users_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Api::V1::Users', type: :request do
  describe 'POST /api/v1/users' do
    let(:user_params) do
      { email: 'user@duetcode.io', password: 'samplepassword' }
    end

    it 'create a new user' do
      post api_v1_users_path, params: { user: user_params }
      expected_body = { 'email' => 'user@duetcode.io' }

      expect(response).to have_http_status(:created)
      expect(JSON.parse(response.body)).to include(expected_body)
    end

    it 'returns unprocessable entity with errors' do
      user_params[:password] = nil
      post api_v1_users_path, params: { user: user_params }

      expected_error = { 'password' => ['can\'t be blank'] }

      expect(response).to have_http_status(:unprocessable_entity)
      expect(JSON.parse(response.body)).to eq(expected_error)
    end
  end
end

Here, we’re providing parameters with valid and invalid information and checking if the API returns correct HTTP status with expected user information or errors. As you can realize, we used JSON.parse(response.body) to match actual results with predefined expected data. It’s common to have similar tests, so we can extract the parsing body of the response part to the separate module (spec/support/json_helpers.rb) and use it from there.

spec/support/json_helpers.rb
# frozen_string_literal: true

module JsonHelpers
  def load_body(response)
    JSON.parse(response.body)
  end
end

Then we need to include json helpers in spec_helper.rb.

spec/spec_helper.rb
# frozen_string_literal: true

require_relative 'support/json_helpers'

RSpec.configure do |config|
  config.include JsonHelpers
  # ...
end

And now, we can refactor users spec and use load_body(response) instead of JSON.parse(response.body).

spec/spec_helper.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Api::V1::Users', type: :request do
  describe 'POST /api/v1/users' do
    let(:user_params) do
      { email: 'user@duetcode.io', password: 'samplepassword' }
    end

    it 'create a new user' do
      post api_v1_users_path, params: { user: user_params }
      expected_body = { 'email' => 'user@duetcode.io' }

      expect(response).to have_http_status(:created)
      expect(load_body(response)).to include(expected_body)
    end

    it 'returns unprocessable entity with errors' do
      user_params[:password] = nil
      post api_v1_users_path, params: { user: user_params }

      expected_error = { 'password' => ['can\'t be blank'] }

      expect(response).to have_http_status(:unprocessable_entity)
      expect(load_body(response)).to eq(expected_error)
    end
  end
end

Summary

In this chapter, we created a user model using devise gem and learned how to build an API endpoint for user creation. You can find the source code of bookmarker application on github. In the next chapter, we will introduce active_model_serializers for API endpoints. You can also find the previous chapter here.

Thanks for reading! I would love to hear your feedback about the chapter and the course in general. You can contact me by email or from twitter.