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 transaction_type
(enum to discinct between Credit & Debit).
rails g scaffold UserTransaction amount:decimal details:string transaction_type: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 } 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
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
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.
Top comments (0)