Rails API Mode in 2026: RESTful APIs, Serialization and Best Practices
Master Rails API mode with best practices for RESTful design, JSON serialization with Alba and jsonapi-serializer, authentication strategies, and error handling in Rails 8.

Rails API mode strips away browser-specific middleware to deliver a lean, fast backend purpose-built for JSON APIs. With Rails 8.1 and the growing ecosystem of serialization libraries, building production-grade RESTful APIs has never been more streamlined.
Rails API mode removes sessions, cookies, views, and asset pipeline middleware. The result: a lighter stack optimized for JSON responses, ideal for mobile backends, microservices, and decoupled SPAs.
Setting Up a Rails 8 API-Only Application
Creating an API-only Rails app takes a single flag. The --api option configures ApplicationController to inherit from ActionController::API instead of ActionController::Base, removing 15+ middleware layers that serve no purpose in a pure API context.
# Terminal command
rails new order_service --api --database=postgresqlThis generates a project without view templates, asset compilation, or session cookies. The resulting application.rb includes config.api_only = true, which keeps the middleware stack minimal.
For existing full-stack Rails apps that need an API namespace, the approach differs: create a base API controller that inherits from ActionController::API and mount API routes under a versioned namespace.
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
# Token validation logic
end
end
end
endThis pattern keeps the full-stack app intact while adding a dedicated API layer.
RESTful Route Design and Versioning Strategies
Clean RESTful route design directly impacts API usability and maintainability. Rails routing DSL makes it straightforward to express resource hierarchies, but a few conventions separate solid APIs from fragile ones.
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update] do
resources :orders, only: [:index, :show, :create]
end
resources :products, only: [:index, :show] do
collection do
get :search
end
end
resource :session, only: [:create, :destroy]
end
end
endKey conventions:
- Namespace versioning (
/api/v1/) over header-based versioning for simplicity and cacheability - Shallow nesting limited to one level:
/users/:user_id/ordersworks, but/users/:user_id/orders/:order_id/itemsshould become/orders/:order_id/items - Singular resources for endpoints representing the current user's session or profile
When retiring an API version, return HTTP 410 Gone with a JSON body pointing to the new version rather than silently breaking clients.
JSON Serialization: Alba vs. jsonapi-serializer
Serialization determines how ActiveRecord objects become JSON. The choice of serializer affects response times, payload structure, and API contract flexibility. Two libraries dominate the Rails ecosystem in 2026: Alba for speed and simplicity, and jsonapi-serializer for JSON:API spec compliance.
Alba: Zero-Dependency Performance
Alba serializes Ruby objects up to 10x faster than legacy alternatives like ActiveModel::Serializer. It has zero dependencies, making it ideal for lightweight API services.
# app/resources/user_resource.rb
class UserResource
include Alba::Resource
root_key :user, :users
attributes :id, :email, :name, :created_at
attribute :full_name do |user|
"#{user.first_name} #{user.last_name}"
end
many :orders, resource: OrderResource
# Conditional attributes based on context
attribute :admin_notes, if: proc { |user, params|
params[:current_user]&.admin?
}
end# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::BaseController
def show
user = User.includes(:orders).find(params[:id])
render json: UserResource.new(user, params: { current_user: current_user })
end
def index
users = User.where(active: true).page(params[:page])
render json: UserResource.new(users)
end
endjsonapi-serializer: Strict Spec Compliance
When API consumers expect JSON:API formatted responses with data, type, attributes, and relationships keys, jsonapi-serializer (the maintained fork of Netflix's fast_jsonapi) handles the formatting automatically.
# app/serializers/user_serializer.rb
class UserSerializer
include JSONAPI::Serializer
set_type :user
set_id :id
attributes :email, :name, :created_at
has_many :orders, serializer: OrderSerializer
# Cache at the serializer level for high-traffic endpoints
cache_options store: Rails.cache, namespace: "jsonapi", expires_in: 1.hour
endThe choice depends on the project: Alba for internal APIs, microservices, and mobile backends where payload flexibility matters. jsonapi-serializer for public APIs where standardized contracts reduce integration friction.
Ready to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Authentication Patterns for Rails APIs
Rails 8 introduced a built-in authentication generator that scaffolds session-based auth. Adapting this for API-only mode requires swapping cookie sessions for token-based authentication. Two patterns dominate: JWT for stateless architectures and opaque bearer tokens for simpler revocation.
JWT with Short-Lived Tokens
# app/services/jwt_service.rb
class JwtService
SECRET = Rails.application.credentials.jwt_secret_key
ALGORITHM = "HS256"
def self.encode(payload, exp: 15.minutes.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET, ALGORITHM)
end
def self.decode(token)
body = JWT.decode(token, SECRET, true, algorithm: ALGORITHM).first
HashWithIndifferentAccess.new(body)
rescue JWT::ExpiredSignature, JWT::DecodeError => e
nil
end
end# app/controllers/concerns/jwt_authenticatable.rb
module JwtAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_request
end
private
def authenticate_request
token = request.headers["Authorization"]&.split(" ")&.last
decoded = JwtService.decode(token)
if decoded
@current_user = User.find_by(id: decoded[:user_id])
end
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
end
def current_user
@current_user
end
endShort-lived access tokens (15 minutes) paired with refresh token rotation provide a secure balance. The access token stays stateless, while refresh tokens stored in the database allow revocation on password change or logout.
Opaque Bearer Tokens
For simpler APIs where database lookups per request are acceptable, has_secure_token offers a straightforward approach:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_secure_token :api_token
def regenerate_api_token!
regenerate_api_token
end
endOpaque tokens simplify revocation (delete or regenerate the token) but require a database query on every authenticated request.
Structured Error Handling Across the API
Consistent error responses separate professional APIs from prototypes. A centralized error handler prevents Rails from returning HTML error pages and ensures every failure returns structured JSON.
# app/controllers/concerns/error_handler.rb
module ErrorHandler
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
end
private
def not_found(exception)
render json: {
error: "not_found",
message: "Resource not found",
details: exception.message
}, status: :not_found
end
def unprocessable_entity(exception)
render json: {
error: "validation_failed",
message: "Validation failed",
details: exception.record.errors.full_messages
}, status: :unprocessable_entity
end
def bad_request(exception)
render json: {
error: "bad_request",
message: "Missing required parameter",
details: exception.message
}, status: :bad_request
end
endInclude this concern in the base API controller. Every endpoint then returns predictable JSON errors with appropriate HTTP status codes, machine-readable error types, and human-readable messages.
API controllers using token-based authentication must skip CSRF verification. Add skip_before_action :verify_authenticity_token or inherit from ActionController::API, which does not include CSRF middleware by default.
Pagination and Response Optimization
Unbounded queries are the fastest path to performance degradation. Every list endpoint should paginate results and communicate pagination metadata clearly.
# app/controllers/api/v1/products_controller.rb
class Api::V1::ProductsController < Api::V1::BaseController
def index
products = Product
.where(active: true)
.order(created_at: :desc)
.page(params[:page])
.per(params[:per_page] || 25)
render json: {
data: ProductResource.new(products).serializable_hash,
meta: {
current_page: products.current_page,
total_pages: products.total_pages,
total_count: products.total_count
}
}
end
endBeyond pagination, three optimizations make a measurable difference in API response times:
- Eager loading with
includesorpreloadeliminates N+1 queries that multiply database round-trips - Select only needed columns with
.select(:id, :name, :price)when serializers use a subset of model attributes - HTTP caching headers via
stale?andfresh_whenallow clients and CDNs to cache responses without custom logic
Testing Rails API Endpoints with RSpec
API tests should verify status codes, response structure, and authentication gates. Request specs in RSpec hit the full middleware stack, making them the closest representation of real API behavior.
# spec/requests/api/v1/users_spec.rb
RSpec.describe "Api::V1::Users", type: :request do
let(:user) { create(:user) }
let(:token) { JwtService.encode(user_id: user.id) }
let(:headers) { { "Authorization" => "Bearer #{token}" } }
describe "GET /api/v1/users/:id" do
it "returns the user with correct structure" do
get "/api/v1/users/#{user.id}", headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json["user"]).to include(
"id" => user.id,
"email" => user.email,
"name" => user.name
)
end
it "returns 401 without authentication" do
get "/api/v1/users/#{user.id}"
expect(response).to have_http_status(:unauthorized)
end
it "returns 404 for non-existent user" do
get "/api/v1/users/0", headers: headers
expect(response).to have_http_status(:not_found)
json = JSON.parse(response.body)
expect(json["error"]).to eq("not_found")
end
end
endThese tests cover the three scenarios every API endpoint should handle: successful response structure, authentication enforcement, and error response format.
Rails 8.1 API Features Worth Adopting
Rails 8.1 (released October 2025) adds capabilities directly relevant to API development:
- Continuable Jobs allow long-running background tasks (data imports, batch processing) to resume from the last checkpoint after deploys or restarts, eliminating wasted work in background job pipelines
- Structured Event Logging via
Rails.event.notify(...)emits events consumable by APM platforms (Datadog, New Relic) without custom instrumentation code - Deprecated Associations can be marked with
:warn,:raise, or:notifymodes, helping teams phase out legacy relationships in large API codebases
These features reduce boilerplate in API projects and improve observability out of the box. The Rails 8 migration guide covers the full upgrade path.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Conclusion
- Use
--apimode for dedicated API services to eliminate unnecessary middleware and reduce response latency - Pick a serializer that fits the contract: Alba for speed and flexibility, jsonapi-serializer for strict JSON:API compliance
- Implement token authentication with short-lived JWTs and refresh rotation, or opaque bearer tokens for simpler revocation needs
- Centralize error handling in a shared concern so every endpoint returns structured JSON with consistent error types
- Paginate every list endpoint and apply eager loading to eliminate N+1 queries before they reach production
- Test request specs against the three core scenarios: success structure, auth enforcement, and error format
- Adopt Rails 8.1 features like continuable jobs and structured events to improve reliability and observability in API services
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Solid Queue and Solid Cache in Rails 8: Complete Guide for Technical Interviews 2026
Deep dive into Solid Queue and Solid Cache, the database-backed defaults in Rails 8. Architecture, configuration, concurrency controls, and interview-ready knowledge for 2026.

Ruby on Rails 8: New Features and Migration Guide 2026
Ruby on Rails 8 ships with the Solid Trifecta, a built-in authentication generator, Kamal 2 deployment, and Propshaft. This tutorial covers every major feature and walks through upgrading from Rails 7 step by step.

Action Cable and WebSockets in Rails: Complete Guide for Technical Interviews 2026
Deep dive into Action Cable and WebSockets in Ruby on Rails. Covers connections, channels, broadcasting, Solid Cable in Rails 8, scaling with Redis, and common interview questions with code examples.