Building APIs with Rails API Mode
Rails API Mode is a way to generate a Rails application that’s optimized for building APIs. It strips away the unnecessary components of a full Rails application, like views, helpers, and asset pipelines, leaving you with a lean, focused API backend.
Before you begin
If you are used to full Rails apps, API mode feels like the same framework with the browser-facing pieces removed. You still get routes, controllers, models, and the database tools you expect, but the response path stops at JSON instead of HTML. That makes the app easier to reason about when the product is really a service or backend.
The benefit is not just speed. A smaller stack also means fewer files to touch and fewer framework features to disable later. If you know from the start that the app will serve JSON, API mode gives you a cleaner base than starting with a full stack and trimming it down afterward.
The other advantage is that the app becomes easier to explain to other developers. When the project is API-first, the codebase is about request handling, serialization, validation, and response shape. That clarity matters when the same service will be consumed by a mobile client, a front-end app, or another backend.
tl;dr
Use Rails API mode when your app serves JSON rather than HTML. Generate a fresh API-only app with rails new --api, or convert an existing app by setting config.api_only = true, switching controllers to ActionController::API, and removing middleware you do not need.
what is Rails API mode?
When you create a new Rails app with --api, you get a slimmed-down version that includes only what you need for an API:
- Controllers that respond to JSON requests
- Models for database interactions
- Routing for RESTful endpoints
- Middleware tailored for API requests (no sessions, cookies, or browser-specific stuff)
The key difference from a full Rails app is what’s excluded:
| Full Rails App | API Mode |
|---|---|
| ActionView | Excluded |
| ActionMailer | Excluded |
| Asset Pipeline | Excluded |
| Session Middleware | Excluded |
| Cookies | Excluded |
| Flash messages | Excluded |
This means faster boot times, smaller memory footprint, and a more focused codebase.
That leaner stack also makes the flow more obvious. Requests hit the router, the controller gathers data, and the controller responds with JSON. Because there is no template layer in the middle, you can focus on response shape, error handling, and serialization instead of view rendering. It also means fewer accidental dependencies. If you never need browser helpers, flash messages, or a session store, leaving them out makes the application easier to keep consistent over time.
creating an API-only app
Generate a new API-only Rails application with the --api flag. This creates a minimal Rails app without ActionView, ActionMailer, or the asset pipeline. The generated app is ready to serve JSON from the first request:
rails new my_api --api
This creates an app with the API-focused stack. You can verify the setting by checking config/application.rb, where Rails records the choice with the api_only flag. This line is the single source of truth for whether the app is in API mode:
module MyApi
class Application < Rails::Application
config.api_only = true
end
end
converting an existing app
If you have a full Rails app and want to convert it to API mode, you can do that too.
step 1: set API mode in config
In config/application.rb:
Setting api_only in the configuration file is the first step when converting an existing full Rails app to API mode. The second step is switching your base controller class. ActionController::API is a lighter-weight controller that includes only the modules needed for JSON APIs, while ActionController::Base pulls in view rendering, cookies, flash messages, and session management. Changing the parent class in ApplicationController propagates this lighter stack to every controller in your application.
config.api_only = true
step 2: use API controllers
Instead of ActionController::Base, use ActionController::API as your base class:
Switching to ActionController::API as the base class strips out view rendering, session management, and cookie handling from every controller. The next step removes the corresponding middleware, which runs before any controller action. Removing both layers keeps the request path as lean as possible for an API that only serves JSON.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
end
ActionController::API is a lighter weight controller that includes only the essentials for handling requests, with no view rendering, session management, or cookies. It still gives you render json:, params, and all the standard controller callbacks, so the day-to-day API workflow feels familiar.
That difference is easy to miss when you are converting an existing app. In practice, it keeps the controller focused on input, validation, and JSON output instead of slowly turning into a browser controller with extra baggage.
step 3: remove unnecessary middleware
In config/application.rb, you can remove middleware you don’t need. Each middleware class adds overhead to every request, so dropping the ones related to cookies, sessions, and flash messages keeps the API lean:
module MyApp
class Application < Rails::Application
config.api_only = true
# Remove these if not using:
config.middleware.delete ActionDispatch::Cookies
config.middleware.delete ActionDispatch::Session::CookieStore
config.middleware.delete ActionDispatch::Flash
end
end
building your first API endpoint
Let’s create a simple RESTful resource. Say we want an API for managing users.
generate a resource
Run the scaffold generator to create the model, migration, controller, and route definitions in one command. In API mode, the generated controller inherits from ActionController::API and responds with JSON by default:
rails generate scaffold User name:string email:string
This creates the model, controller, and routes. The controller will respond to JSON by default, with standard REST actions like index, show, create, update, and destroy already wired up.
check the routes
List the generated routes to confirm the RESTful endpoints are in place:
rails routes
You’ll see standard RESTful routes:
The route table shows what endpoints exist, but it does not confirm they work. Starting the server with rails server -p 3000 makes the API accessible on localhost, where you can send real HTTP requests and inspect the responses. The port flag is optional but useful when you need to run multiple Rails processes or when port 3000 is already in use.
users GET /users(.:format) users#index
users POST /users(.:format) users#create
user GET /users/:id(.:format) users#show
user PATCH /users/:id(.:format) users#update
user PUT /users/:id(.:format) users#update
user DELETE /users/:id(.:format) users#destroy
test the API
Start the server and make a request. The routes table confirms that standard RESTful endpoints like GET /users and POST /users are ready to accept requests:
With the server running, you can test each endpoint with curl. The POST request to /users sends a JSON body with a Content-Type header, which tells Rails to parse the request as JSON. After creating a user, the subsequent GET requests retrieve the data. The responses will be JSON automatically because API mode skips the view rendering step and serializes directly.
rails server -p 3000
With the server running, send a few curl requests to test each endpoint. The server should respond with JSON for every action:
After creating a record with POST, a GET request to /users should return the user you just created. The response is JSON by default because API controllers bypass the view layer. The curl http://localhost:3000/users/1 command fetches a single record by its database ID. These curl commands are a quick way to verify that the entire request-to-response pipeline is working before you write any front-end code that will consume the same endpoints.
# Create a user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# List users
curl http://localhost:3000/users
# Get a specific user
curl http://localhost:3000/users/1
The responses will be JSON automatically. That is the whole goal of API mode, since the caller usually expects structured data rather than a rendered page. A clean JSON response keeps the contract obvious and reduces the amount of client-side guessing.
customizing JSON rendering
using Jbuilder
Rails includes Jbuilder for building complex JSON responses. Create a view file:
# app/views/users/show.json.jbuilder
json.id @user.id
json.name @user.name
json.email @user.email
json.created_at @user.created_at.iso8601
using active model serializers
For more control, add the active_model_serializers gem:
Jbuilder is included with Rails and works well for simple JSON shaping, but as your API grows, you may want a more structured approach. The active_model_serializers gem introduces a serializer class per model, which centralises the logic for deciding which attributes and associations to include in API responses. This separation keeps controllers thin and makes it easier to version your JSON output when the API evolves over time.
gem 'active_model_serializers'
Generate a serializer for the User model. The generator creates a serializer class where you declare which attributes and associations to include in the JSON response:
Adding the gem to your Gemfile makes it available, but you still need to generate a serializer for each model. The rails generate serializer User command creates a serializer class where you declare which fields to expose. The attributes method lists the model columns you want in the JSON response, and you can define custom methods like created_at to format values before they reach the client.
rails generate serializer User
With the serializer defined, Rails automatically matches it to the model by convention. When your controller calls render json: @user, Rails looks for a UserSerializer and uses it to build the response. You do not need to specify the serializer explicitly unless you want to override the convention. This automatic matching is one of the reasons active_model_serializers reduces boilerplate compared to hand-writing JSON in every controller action.
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :created_at
def created_at
object.created_at.iso8601
end
end
In your controller, rendering a model instance automatically uses the matching serializer. Rails looks up UserSerializer by convention, so you do not need to specify the serializer explicitly:
Serializers decouple the data model from the JSON output, which becomes valuable when the same resource needs different representations for different API versions or client types. The next section covers authentication, the layer that controls which clients can access those serialized resources. A clean separation between serialization and authentication keeps each concern testable on its own.
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
render json: @user
end
end
Serializers are especially useful when several endpoints need the same object shape. They keep formatting rules in one place, which makes it easier to add or remove fields without copying JSON-building code into multiple controllers. When the API grows, having a single serializer per resource also makes it simpler to version your JSON output.
authentication
APIs typically need authentication. Here are common approaches:
token authentication
Implement a simple token-based auth:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
token = request.headers['Authorization']&.split(' ')&.last
@current_user = User.find_by(api_token: token)
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
end
end
using devise token auth
For production apps, use a gem like devise_token_auth:
Authentication answers the question of who is making the request. Once you know the caller, error handling defines what happens when a request goes wrong. The next section shows how to rescue common ActiveRecord exceptions and return consistent JSON error responses. Predictable error shapes make client-side error handling much simpler.
gem 'devise_token_auth'
This provides full token-based authentication out of the box.
When you add authentication to an API, think about the error shape at the same time. A client should be able to tell the difference between missing credentials, expired credentials, and a bad request without guessing from the message string alone. Consistent status codes and predictable JSON make client code much easier to maintain.
error handling
API mode still supports standard Rails error handling, but you might want custom JSON error responses:
# app/controllers/application_controller.rb
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
rescue_from ActiveRecord::RecordInvalid, with: :render_validation_error
private
def render_not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def render_validation_error(exception)
render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
end
Good API errors should stay predictable even as the app grows. If every endpoint returns the same general shape for unauthorized, missing, and invalid requests, front-end code can handle them with a single pattern instead of special-casing each controller.
CORS configuration
If your API will be accessed from browsers on different domains, configure CORS. First, add the rack-cors gem to your Gemfile and run bundle install:
# Gemfile
gem 'rack-cors'
After installing the gem, configure the allowed origins in an initializer. This tells the browser which domains are permitted to make cross-origin requests to your API:
CORS configuration controls which external origins can call your API from a browser. After securing cross-origin access, the next step is versioning your API so you can evolve it without breaking existing clients. A versioned API lets you introduce new endpoints, change response shapes, or deprecate old behaviour without forcing every consumer to upgrade at once.
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3001' # Your frontend domain
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options]
end
end
summary
Rails API mode trims away browser-specific pieces so Rails can focus on JSON responses, controllers, and data access. It is a strong fit when the app is really a backend service, and it becomes even more useful when you keep serializers, authentication, and error shapes consistent from the start.
versioning your API
As your API evolves, you’ll want to version it. A common approach is to place the version in the path and keep the controller namespace aligned with that version:
# config/routes.rb
namespace :api do
namespace :v1 do
resources :users
end
end
This creates routes like /api/v1/users. The namespace prefix keeps versioned endpoints separate, so you can evolve the v2 API without breaking existing v1 clients.
Organize your controllers in a matching namespace so the file structure mirrors the URL structure:
Versioned namespaces keep old and new API surfaces separate at the routing and controller level. The next section summarises when API mode is the right choice versus when a full Rails stack makes more sense. Choosing the right mode early shapes how the entire application grows, from middleware configuration to the expectations your team sets about what the app should return.
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApplicationController
# Your logic here
end
end
end
when to use API mode
Rails API mode is ideal when:
- Building a JSON API for a JavaScript frontend (React, Vue, etc.)
- Creating a mobile app backend
- Providing an API for third-party integrations
- Microservices that don’t need views
Stick with full Rails when:
- You need server-side rendering (SSR)
- You want the asset pipeline
- You’re building a traditional web app
Next steps
Once API mode feels comfortable, the next step is to see how routing and controllers still work together in a normal Rails request flow. The Rails middleware tutorial is useful for understanding what still happens before your controller responds, and the Rails MVC pattern tutorial connects that API request flow to the rest of the app structure.
API mode gives you a focused JSON backend, but understanding what happens before your controller responds is equally important. Once you have a working API, the next step is often adding background processing for tasks that should not block the request-response cycle. The ActiveJob and Sidekiq tutorial shows how to offload heavy work to background jobs. You may also want to explore Rails Routing for more advanced route design patterns that keep your API endpoints clean as the surface area grows.
See Also
- Rails Middleware — Understanding the request processing pipeline
- ActiveJob and Sidekiq — Background job processing
- Rails Routing — RESTful route design