Create REST APIs in minutes

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.

Did you find this article valuable?

Support Akhil by becoming a sponsor. Any amount is appreciated!