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 RESTful APIs and serialization best practices diagram

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.

Quick Takeaway

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.

ruby
# Terminal command
rails new order_service --api --database=postgresql

This 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.

ruby
# 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
end

This 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.

ruby
# 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
end

Key conventions:

  • Namespace versioning (/api/v1/) over header-based versioning for simplicity and cacheability
  • Shallow nesting limited to one level: /users/:user_id/orders works, but /users/:user_id/orders/:order_id/items should become /orders/:order_id/items
  • Singular resources for endpoints representing the current user's session or profile
Version Deprecation

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.

ruby
# 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
ruby
# 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
end

jsonapi-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.

ruby
# 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
end

The 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

ruby
# 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
ruby
# 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
end

Short-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:

ruby
# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_secure_token :api_token

  def regenerate_api_token!
    regenerate_api_token
  end
end

Opaque 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.

ruby
# 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
end

Include 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.

Skip CSRF for Token Auth

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.

ruby
# 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
end

Beyond pagination, three optimizations make a measurable difference in API response times:

  • Eager loading with includes or preload eliminates 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? and fresh_when allow 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.

ruby
# 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
end

These 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 :notify modes, 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 --api mode 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

#ruby-on-rails
#api
#rest
#serialization
#best-practices

Share

Related articles