REST APIs & Authentication

Reviewed & published by Brayan K

Modern Rails apps often serve JSON to a frontend or mobile client, and almost all of them need to know who the user is. This lesson covers building APIs and adding authentication.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end you'll create an API-only controller that renders JSON with proper status codes, store a logged-in user in the session, secure passwords with has_secure_password, and tell authentication from authorization.

What You'll Learn in This Lesson

1️⃣ Building a JSON API

Generate with rails new shop --api for a lean app. Controllers use render json: and communicate results with HTTP status codes — :created (201) for a new record, :unprocessable_entity (422) for validation failures.

# Generate an API-only app (no view layer, lighter middleware):
#
#   rails new shop --api
#
# An API controller renders JSON instead of HTML and uses HTTP
# status codes to communicate the outcome.

class Api::PostsController < ApplicationController
  # GET /api/posts
  def index
    posts = Post.all
    render json: posts                     # 200 OK by default
  end

  # GET /api/posts/:id
  def show
    post = Post.find(params[:id])
    render json: post
  end

  # POST /api/posts
  def create
    post = Post.new(post_params)
    if post.save
      render json: post, status: :created            # 201
    else
      render json: { errors: post.errors.full_messages },
             status: :unprocessable_entity            # 422
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body)
  end
end

2️⃣ Authentication with has_secure_password

has_secure_password hashes passwords with bcrypt into a password_digest column and gives you authenticate. Store the logged-in user's id in the session. Remember: authentication is identity; authorization is permission.

# Authentication = WHO you are. Built into Rails with has_secure_password,
# which uses the bcrypt gem and a password_digest column.

class User < ApplicationRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
end

# has_secure_password gives you:
#   user.password = "secret"     # setter that hashes into password_digest
#   user.authenticate("secret")  # => the user if correct, else false

class SessionsController < ApplicationController
  # Log in: verify the password, then remember the user in the session.
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      session[:user_id] = user.id       # stored in a signed cookie
      redirect_to root_path, notice: "Welcome back!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    session.delete(:user_id)            # log out
    redirect_to root_path
  end
end

# Authorization = WHAT you may do. A simple example:
class ApplicationController < ActionController::Base
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def require_login
    redirect_to login_path unless current_user
  end
end

# For full-featured auth (registration, password reset, confirmations),
# many teams add the Devise gem instead of hand-rolling everything.

Your turn. Finish an API create action that returns the right JSON and status codes.

# 🎯 YOUR TURN — finish an API create action with correct status codes.

class Api::CommentsController < ApplicationController
  def create
    comment = Comment.new(comment_params)
    if comment.save
      render json: comment, status: :created   # 👉 201 on success
    else
      render json: { errors: comment.errors.full_messages },
             status: :unprocessable_entity      # 👉 422 on validation failure
    end
  end

  private

  # 1) Permit only :body and :post_id
  def comment_params
    params.require(:comment).permit(:body, :post_id)  # 👉 TODO: complete
  end
end

# ✅ A valid POST returns the comment as JSON with status 201;
#    an invalid one returns the errors array with status 422.

Mini-Challenge: A Login Flow

Add has_secure_password, build a SessionsController#create, and a current_user helper. Then label which step is authentication and which is authorization.

# 🎯 MINI-CHALLENGE: secure a model and a sessions flow.
#
# 1. Add has_secure_password to a User model (needs a
#    password_digest column and the bcrypt gem).
# 2. In SessionsController#create, find the user by email, call
#    user.authenticate(params[:password]), and on success set
#    session[:user_id].
# 3. Add a current_user helper that loads User.find_by(id: session[:user_id]).
#
# Then explain in a comment: which step is AUTHENTICATION and which
# would be AUTHORIZATION (deciding what current_user is allowed to do)?
#
# ✅ Goal: a login that verifies a password and remembers the user.

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — APIs & Auth

GoalCode
API-only apprails new shop --api
Render JSONrender json: @post
Created statusstatus: :created
Invalid statusstatus: :unprocessable_entity
Secure passwordshas_secure_password
Verify a loginuser.authenticate(pw)

🎉 Lesson Complete!

Practice quiz

Which flag creates a slimmed-down API-only Rails app?

  • rails new shop --json
  • rails new shop --api
  • rails new shop --no-views
  • rails new shop --rest

Answer: rails new shop --api. rails new shop --api generates an API-only app without view layers.

In a controller, how do you return JSON for a record?

  • return @post.json
  • render json: @post
  • respond @post
  • json_out @post

Answer: render json: @post. render json: @post serialises the object to JSON in the response.

Which HTTP status best represents a successfully created resource?

  • 200 OK
  • 201 Created
  • 204 No Content
  • 302 Found

Answer: 201 Created. 201 Created signals a new resource was successfully made.

Which status fits a request that failed model validation?

  • 422 Unprocessable Entity
  • 404 Not Found
  • 500 Internal Server Error
  • 301 Moved Permanently

Answer: 422 Unprocessable Entity. 422 Unprocessable Entity is conventional for validation failures.

What does has_secure_password add to a model?

  • A plaintext password column
  • Two-factor auth
  • A login form
  • password_digest handling, a password= setter, and authenticate

Answer: password_digest handling, a password= setter, and authenticate. has_secure_password manages a bcrypt password_digest plus authenticate.

What column does has_secure_password expect by default?

  • password
  • encrypted_pw
  • password_digest
  • secure_password

Answer: password_digest. It stores the bcrypt hash in a password_digest column.

What is the difference between authentication and authorization?

  • They are the same thing
  • Authentication is who you are; authorization is what you may do
  • Authentication is for APIs only
  • Authorization always happens first

Answer: Authentication is who you are; authorization is what you may do. Authentication verifies identity; authorization decides permitted actions.

In a traditional server-rendered app, where is the logged-in user id usually kept?

  • A query string
  • A global variable
  • The session (a signed cookie)
  • The database id column

Answer: The session (a signed cookie). session[:user_id] is stored in a signed, tamper-resistant cookie.

What is Devise?

  • A testing framework
  • A popular authentication gem for Rails
  • A database adapter
  • A templating engine

Answer: A popular authentication gem for Rails. Devise is a widely used gem that provides ready-made authentication.

Which gem does has_secure_password rely on for hashing?

  • bcrypt
  • openssl-only
  • jwt
  • argon-rails

Answer: bcrypt. has_secure_password uses the bcrypt gem to hash passwords.

Continue this course

Frequently asked questions

What does API-only mode change?

Running rails new app --api generates a leaner application aimed at serving JSON rather than HTML. Controllers inherit from ActionController::API instead of ActionController::Base, the view layer and asset pipeline are trimmed, and browser-oriented middleware (like cookie-based sessions and flash) is left out by default. You still get routing, controllers, ActiveRecord, and strong parameters. It is ideal when a separate frontend (a single-page app or mobile client) consumes your API. You can always add pieces back if you later need them.

What is the difference between authentication and authorization?

Authentication answers "who are you?" — it verifies identity, for example by checking an email and password with user.authenticate. Authorization answers "what are you allowed to do?" — it decides whether the already-identified user may perform a given action, like editing a post they don't own. They are distinct steps: you authenticate first to establish current_user, then authorize specific actions against that user's permissions. Mixing them up is a common source of security bugs. Gems like Pundit and CanCanCan focus on the authorization half.

How does has_secure_password keep passwords safe?

has_secure_password (backed by the bcrypt gem) never stores the raw password. When you assign user.password = "secret", it hashes the value with bcrypt and saves only the result in the password_digest column. To verify a login you call user.authenticate("secret"), which hashes the attempt and compares it to the stored digest, returning the user on a match or false otherwise. bcrypt is deliberately slow and salted, which makes brute-forcing stolen hashes expensive. You add it by including has_secure_password in the model and having a password_digest column.

When should I use Devise instead of rolling my own auth?

Hand-rolling authentication with has_secure_password and a sessions controller is great for learning and for simple needs. Devise is a mature gem that bundles registration, login, logout, password reset, email confirmation, account locking, and "remember me" out of the box, all configurable. Reach for Devise when you want those features quickly and don't want to maintain the security-sensitive plumbing yourself. The trade-off is added abstraction and configuration to learn. For an API that issues tokens, teams often pair Devise with a token strategy or use a JWT-based approach instead of cookie sessions.