DEV Community

Akhil
Akhil

Posted on • Originally published at blog.akhilgautam.me

Create REST APIs in 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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

UserTransaction will deal with amount of transaction, details and transaction_type(enum to discinct between Credit & Debit).

rails g scaffold UserTransaction amount:decimal details:string transaction_type:integer user:belongs_to 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 } validates :password, presence: true, length: { minimum: 8 } end # app/models/user_transaction.rb class UserTransaction < ApplicationRecord enum of_kind: [ :credit, :debit ] # associations belongs_to :user end 
Enter fullscreen mode Exit fullscreen mode

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 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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

โšกโšก 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 } 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)