Create REST APIs in minutes
Build an expense manager application API in 5 minutes
In my Ruby on Rails series, I have already shared few blogs showcasing the magic of Ruby on Rails. In this blog, we will see how we can create a full-fledged REST API in minutes.
Expense Manager API
- Create a user
- Add transactions
- Monthly report
STEP 1: Create Rails project in API mode
rails new expense_manager --api
cd expense_manager
Open up the Gemfile
and uncomment bcrypt
gem as we will use it to generate password digest.
STEP 2: Scaffold models, controllers, and routes
Instead of generating each of them individually, we will scaffold them.
User will have name
, email
, password_digest
(store password hash given by bcrypt), auth_token
for token-based authentication, and a balance
field that contains the current balance.
rails g scaffold User name:string email:string password_digest:string auth_token:string balance:decimal
UserTransaction will deal with amount
of transaction, details
and of_kind
(enum to discinct between Credit & Debit).
rails g scaffold UserTransaction amount:decimal details:string of_kind:integer user:belongs_to
It creates models, controllers, and migrations. We need small modifications in create_users.rb
migration file to add default 0
as balance for users.
t.decimal :balance, default: 0
STEP 3: Refine the models
We have two models: User and UserTransaction. User model has a has_many
relation with UserTransaction. Every UserTransaction belongs_to
a User.
# app/models/user.rb
class User < ApplicationRecord
EMAIL_FORMAT = /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
# methods for authentication
has_secure_password
# associations
has_many :user_transactions
#validations
validates :email,
presence: true,
format: { with: EMAIL_FORMAT },
on: :create
validates :password,
presence: true,
length: { minimum: 8 },
on: :create
end
# app/models/user_transaction.rb
class UserTransaction < ApplicationRecord
enum of_kind: [ :credit, :debit ]
# associations
belongs_to :user
end
STEP 4: Refine the controllers
Let's refine the controllers generated by scaffold and include proper authentication. We will generate auth_token
on login
. We are going to accept auth_token
as query parameters for now.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :verify_user?
def current_user
@current_user ||= authenticate_token
end
def verify_user?
return true if authenticate_token
render json: { errors: [ { detail: "Access denied" } ] }, status: 401
end
private
def authenticate_token
User.find_by(auth_token: params[:auth_token])
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
skip_before_action :verify_user?
# POST /users
def create
user = User.new(user_params)
user.auth_token = SecureRandom.hex
if user.save
render json: { message: 'Create successfully!' }, status: :created
else
render json: user.errors, status: :unprocessable_entity
end
end
def balance
render json: { balance: current_user.balance }, status: :ok
end
def login
user = User.find_by(email: params[:user][:email])
if user && user.authenticate(params[:user][:password])
render json: { auth_token: user.auth_token }, status: :ok
else
render json: { message: 'Email & Password did not match.' }, status: :unprocessable_entity
end
end
private
# Only allow a list of trusted parameters through.
def user_params
params.require(:user).permit(:name, :email, :password, :balance)
end
end
# app/controllers/user_transactions_controller.rb
class UserTransactionsController < ApplicationController
before_action :set_user_transaction, only: [:show, :update, :destroy]
# GET /user_transactions
def index
user_transactions = current_user.user_transactions
if params[:filters]
start_date = params[:filters][:start_date] && DateTime.strptime(params[:filters][:start_date], '%d-%m-%Y')
end_date = params[:filters][:end_date] && DateTime.strptime(params[:filters][:end_date], '%d-%m-%Y')
render json: user_transactions.where(created_at: start_date..end_date)
else
render json: user_transactions
end
end
# GET /user_transactions/1
def show
render json: @user_transaction
end
# POST /user_transactions
def create
user_transaction = current_user.user_transactions.build(user_transaction_params)
if user_transaction.save
if user_transaction.debit?
current_user.update(balance: current_user.balance - user_transaction.amount)
else
current_user.update(balance: current_user.balance + user_transaction.amount)
end
render json: user_transaction, status: :created
else
render json: user_transaction.errors, status: :unprocessable_entity
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user_transaction
@user_transaction = current_user.user_transactions.where(id: params[:id]).first
end
# Only allow a list of trusted parameters through.
def user_transaction_params
params.require(:user_transaction).permit(:amount, :details, :of_kind)
end
end
STEP 5: Add routes
# config/routes.rb
Rails.application.routes.draw do
resources :user_transactions, only: [:index, :create, :show]
resources :users, only: [:create] do
post :login, on: :collection
get :balance, on: :collection
end
end
⚡⚡ Our REST API is ready to use. We have trimmed down the controller and the routes to very basic actions that we need.
The only modification we needed apart from the default ones we adding the filter's query and authentication in the application controller.
We can use the above REST API in our React, Vue, or any front-end framework. Let's just see a quick example using fetch
:
// create a user
fetch('http://localhost:3000/users', {method: 'POST', headers: {
'Content-Type': 'application/json'}, body: JSON.stringify({user: {email: 'test@example.com', password: 'iamareallycomplexpassword', name: 'Ram'}})})
// login with that user
fetch('http://localhost:3000/users/login', {method: 'POST', headers: {
'Content-Type': 'application/json'}, body: JSON.stringify({user: {email: 'test@example.com', password: 'iamareallycomplexpassword'}})})
// => response
{ auth_token: "09a93b1c0b40e2edf0560b8a7d7e712c" }
// create user_transactions using the above auth_token
fetch('http://localhost:3000/user_transactions?auth_token=09a93b1c0b40e2edf0560b8a7d7e712c', {method: 'POST', headers: {
'Content-Type': 'application/json'}, body: JSON.stringify({user_transaction: {amount: 12, details: 'first transaction', of_kind: 'credit'}})})
// => response
{
id: 1,
amount: '12.0',
details: 'first transaction',
of_kind: 'credit',
user_id: 2,
created_at: '2021-08-23T07: 44: 35.356Z',
updated_at: '2021-08-23T07: 44: 35.356Z',
}
// list all transactions
fetch('http://localhost:3000/user_transactions?auth_token=09a93b1c0b40e2edf0560b8a7d7e712c')
// => response
[
{
id: 1,
amount: '12.0',
details: 'first transaction',
of_kind: 'credit',
user_id: 2,
created_at: '2021-08-23T06:55:50.913Z',
updated_at: '2021-08-23T06:55:50.913Z',
},
{
id: 2,
amount: '12.0',
details: 'first transaction',
of_kind: 'debit',
user_id: 2,
created_at: '2021-08-23T07:44:35.356Z',
updated_at: '2021-08-23T07:44:35.356Z',
},
]
// filter transactions by date for monthly reports
fetch('http://localhost:3000/user_transactions?filters[start_date]=10-08-2021&filters[end_date]=24-08-2021&auth_token=09a93b1c0b40e2edf0560b8a7d7e712c')
// => response
[
{
id: 2,
amount: '12.0',
details: 'first transaction',
of_kind: 'credit',
user_id: 2,
created_at: '2021-08-23T06:55:50.913Z',
updated_at: '2021-08-23T06:55:50.913Z',
},
{
id: 3,
amount: '150.0',
details: 'refund from amazon',
of_kind: 'credit',
user_id: 2,
created_at: '2021-08-23T07:44:35.356Z',
updated_at: '2021-08-23T07:44:35.356Z',
},
{
id: 4,
amount: '120.0',
details: 'flight tickets',
of_kind: 'debit',
user_id: 2,
created_at: '2021-08-23T07:48:29.749Z',
updated_at: '2021-08-23T07:48:29.749Z',
},
]
// check user balance
fetch('http://localhost:3000/users/balance?auth_token=09a93b1c0b40e2edf0560b8a7d7e712c')
// => response
{ balance: 32.0 }
One thing to notice is the dirty code inside the controller and validation scattered in the model. As this is not even 1% of real-world applications, the code inside the controller is not that dirty but as the codebase grows, it becomes messy. It is a quite popular issue, fat controller, skinny model
. At times if we don't follow any design pattern, it can lead to fat model, fat controller
and it will be hard to understand the code in long run. Debugging would be near to impossible if we don't clean it up.
In the next blog, we will talk a bit about few design patterns that we follow in the Rails world and then we will clean up the above application code by using the interactor
gem which is based on command pattern
.
Thanks for reading. If you liked it then do follow me and react to this blog. Also if you can, please share it wherever you can.