Creating our first API endpoint
Let’s create our first RESTful API endpoint which will be a CRUD endpoint for the user. Before doing that we need to make sure where our API code should go. I hope you are familiar with the rails MVC pattern and how Rails organizes its codebase. If not go ahead and check this out first.
API versioning
Initially, the version of our API maybe let’s say ‘V1’ and as our application grows it will have many versions for the API as well(v1, v2, v3….). The clients that use our API may not only use the latest version, what I mean by that is there may be some clients which may only use certain versions of the API let’s say v1 even though our latest version is v3. So we must keep in mind the versioning of our API.
Inside the controller folder of our app let’s make a new folder as api
and inside the api folder let’s create another folder as v1
. Now our folder structure looks something like this
So all the API we create will be inside this folder.
Creating User model
Let’s say our user consists of attributes like fullname, email, gender and password. Go to the terminal and type this to create a user model.
rails g model User fullname:string email:string password_digest:string gender:integer
This will create all the necessary files such as the migration file, user model file, and test cases files.
Let’s do rails db:migrate
in the terminal.
I also like to install a gem called annotate which is quite useful for reading the Active Record schema.
In the Gemfile add gem annotate
inside the group development and test and then bundle install
in the terminal. Now in the terminal, we can do
annotate --models --exclude tests,factories
If we go to the user.rb
model we can see the schema information on the top.
Creating Users Controller
Before creating any API’s controller let’s create a controller a base_controller.rb
first inside the api/v1
folder which will be inherited from the Application controller.
# frozen_string_literal: true
module Api
module V1
class BaseController < ApplicationController
end
end
end
Now again inside the folder api/v1
let’s create a User’s controller which will inherit from the Base controller.
So all the rules or code added in the base controller will apply to all our API’s controllers globally.
In the terminal enter this
rails generate controller api/v1/users index show create update delete
Our users_controller.rb
will look like this
# frozen_string_literal: true
module Api
module V1
class UsersController < BaseController
def index; end
def show; end
def create; end
def update; end
def delete; end
end
end
end
Using BCrypt for password encryption
If you have noticed above we have stored the password as a plain text in the databse which is not secure. We can use the bcrypt gem to secure the password.
Note: If we had used the devise
gem then we didn’t have to worry about this, as devise itself can handle this.
The first thing we need to do is uncomment gem 'bcrypt', '~> 3.1.7'
this guy in the gem file and then bundle install
.
In the User
model we then need to add has_secure_password
and also active model validation for password presence.
class User < ApplicationRecord
has_secure_password
validates :email, :fullname, :password_digest, presence: true
validates :email, uniqueness: true
end
Note: Your column name in users table must be password_digest
and not password
.
Now when ever a new user is created their password is saved as encrypted hash instead of plain text. Also one advantage of using bcrypt is that we can easily authenticate the user as well. Let’s check this in the rails c
.
For more information on how Bcrypt works you can read here
Setting up Routes
Let’s edit the generated route.rb
file to something like this
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
end
end
end
Now if we go to our terminal and hit rails route
we can see our endpoints url for users.
Writing Test Cases for User model
Let’s write some test cases for the user model first also known as unit test. I like using the shoulda_matchers gem which provides the common test functionality.
In the Gemfile add gem 'shoulda-matchers', '~> 5.0'
inside test group and hit bundle install, and in the rails_helpers.rb
add this at the end.
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
And in the user_spec.rb
let’s write some test cases
require 'rails_helper'
RSpec.describe User, type: :model do
it { should validate_presence_of(:email) }
it { should validate_presence_of(:fullname) }
it {should validate_uniqueness_of(:email)}
it {should validate_presence_of(:password)}
end
Let’s run the test cases and check if its pass or fails. In the terminal enter rspec spec/models
Of course, this test will fail as we haven’t added any active record validation in the user model so lets add that
Our user.rb
model will look something like this now
class User < ApplicationRecord
validates :email, :fullname, :password_digest, presence: true
validates :email, uniqueness: true
end
Let’s check the test again with rspec spec/models
Writing Test Cases for Users Controller
We have added the test for our user model likewise, let’s add the test for our user’s controller. If we go and see inside the spec folder we will already see a folder as requests/api/v1
which was auto-generated by rails when we did this
rails generate controller api/v1/users index show create update delete
We will be using this request spec for testing our user’s controller. As you can see that in the users_controller.rb
we already have some empty methods which we can also call API and each of them has its unique endpoints.
Let’s first write some test cases for our def index
method which is a GET method to retrieve all the existing users from the system.
What we should expect from the response of this method/api is an HTTP response status code of 200 and a list of users. So let’s add that in our test case which will look something like this now
require 'rails_helper'
RSpec.describe 'Api::V1::Users', type: :request do
describe 'GET api/v1/users' do
it 'returns list of available users' do
create(:user)
total_users = User.all.count
get api_v1_users_path
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq(total_users)
end
end
end
Above the create(:user)
is from the factory_bot_rails
gem that we have installed.
If we run rspec spec/requests/api/v1/users_spec.rb
in the terminal then we will see that the test will fail which is obvious cause we currently have an empty method.
Let’s try to pass this test case by adding code in our def index GET method at users_controller.rb
def index
render json: User.all, code: '200', status: :ok
end
Here we are sending all the available users in our system as a JSON response. Now let’s check the test case again if its passes or fails.
Noice, our test has passed and we have got our first api
Let’s write a test case for the API that creates a new user which will be a POST method i.e def create
This API is supposed to take the user information as request body params then according to it create a new user and finally give a response with an HTTP code status as 201. Also if we try to create a user with an existing email in the system then the API should respond with an error message.
So, considering this our test case will look something like
RSpec.describe 'Api::V1::Users', type: :request do
describe 'GET api/v1/users/:id' do
it 'returns a user' do
user = create(:user)
expected_response = { 'email' => user.email }
get api_v1_user_path(id: user.id)
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)).to include(expected_response)
end
end
describe 'POST /api/v1/users/' do
it 'creates a new user' do
total_users = User.all.count
request_body = {
user: {
fullname: 'New User',
email: 'new@gmail.com',
password: 'asdasd12ad'
}
}
post api_v1_users_path, params: request_body
expect(response).to have_http_status(201)
expect(User.all.count).to eq(total_users + 1)
end
end
end
Our test case like before will obviously fail since we have not added anything to the def create
method yet. In-order to pass this test case our method now will look something like this
def create
user = User.new(user_params)
if user.save
render json: user, status: :created, code: '201'
else
render json: user.errors, status: :unprocessable_entity, code: '422'
end
end
private
def user_params
params.require(:user).permit(:fullname, :email, :password, :gender)
end
Above I have made use of super params and now if we run our test it will pass. We can add other examples in the test case as well like if the password is empty, fullname is empty, and so on.
Great 🎉 🎉 we have now another API and we can follow the similar process above to create all the required APIs.
Thanks for reading till the end we have come to the end of part 2 of the Building API with Rails series. In the upcoming parts, we will be adding active model serializers, SWAGGER for API documentation, and others.
The code base is available here.
Please feel free to give your feedback on the comment section below or ping me at or . Have a great time