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
- API-only mode and rendering JSON
- HTTP status codes like 201 and 422
- Sessions and cookies for logged-in state
- has_secure_password and bcrypt
- Authenticating with user.authenticate
- Authentication vs authorization (and Devise)
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
end2️⃣ 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 hereCommon Errors (and the fix)
- Missing bcrypt — has_secure_password needs the bcrypt gem in your Gemfile and a password_digest column.
- Storing plaintext passwords — Never save raw passwords; let has_secure_password hash them.
- Returning 200 for everything — Use precise codes: 201 created, 404 not found, 422 invalid.
- Confusing authn and authz — Identify the user first, then separately check what they may do.
- Sessions in API-only mode — Cookie sessions are off by default; add token auth or re-enable the middleware deliberately.
Pro Tips
- 💡 Use status symbols: :created and :unprocessable_entity read better than raw numbers.
- 💡 Never trust the client: always check authorization on the server, even if the UI hides a button.
- 💡 Reach for Devise when ready: it handles registration, resets, and confirmations for you.
📋 Quick Reference — APIs & Auth
| Goal | Code |
|---|---|
| API-only app | rails new shop --api |
| Render JSON | render json: @post |
| Created status | status: :created |
| Invalid status | status: :unprocessable_entity |
| Secure passwords | has_secure_password |
| Verify a login | user.authenticate(pw) |
🎉 Lesson Complete!
- ✅ rails new app --api builds a lean JSON app
- ✅ render json: plus status codes drive REST responses
- ✅ Sessions store the logged-in user id in a signed cookie
- ✅ has_secure_password hashes passwords with bcrypt
- ✅ Authentication is identity; authorization is permission
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
- Previous: Forms & Strong Parameters
- Next: Capstone: A Rails Blog App — Build a full blog with posts, comments, auth and tests end to end
- Quick reference: Ruby cheat sheet
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.