Rails is a productive back end for mobile apps and SPAs, and APIs need consistent responses and errors.
rails new myapi --api creates an API-only app: controllers inherit from ActionController::API, and middleware for cookies, sessions, flash and views is left out. A regular app can serve JSON too, from the same models.
Render JSON with render json: @article, status: :created. By default this calls as_json on the object. Control the output with:
as_json(only: [...], include: { author: { only: :name } })for simple cases.- Jbuilder templates (
show.json.jbuilder), which ship with Rails. - Serializer libraries such as Alba or Blueprinter, or plain Ruby objects with a
to_h, for larger APIs.
Never expose models wholesale, or new columns (and secrets like password digests) leak automatically.
Consistent errors: use rescue_from ActiveRecord::RecordNotFound to render a JSON 404, and return 422 with { errors: record.errors } for validation failures. Use the right status codes: 201 on create, 204 with head :no_content on delete.
Versioning: namespace :api do namespace :v1 do resources :articles end end puts controllers in Api::V1::.
Authentication: API clients send a bearer token. authenticate_or_request_with_http_token reads the Authorization: Bearer ... header. generates_token_for :api, expires_in: 30.days on the model creates signed, expiring tokens that find_by_token_for(:api, token) verifies; for long-lived random tokens, store only a digest. Rate-limit with rate_limit (Rails 7.2+) and paginate collections with a gem such as Pagy or with limit and offset.
Example
# app/controllers/api/v1/articles_controller.rb
module Api::V1
class ArticlesController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate!
rescue_from ActiveRecord::RecordNotFound, with: -> { render json: { error: "not found" }, status: :not_found }
def index
render json: Article.published.includes(:author).limit(50)
.as_json(only: %i[id title], include: { author: { only: :name } })
end
def create
article = @user.articles.new(params.expect(article: %i[title body]))
if article.save then render json: article, status: :created
else render json: { errors: article.errors }, status: :unprocessable_entity
end
end
private
def authenticate!
@user = authenticate_with_http_token { |token, _| User.find_by_token_for(:api, token) }
head :unauthorized unless @user
end
end
endRemember
- --api apps use ActionController::API with a slimmer middleware stack
- Shape JSON explicitly (as_json options, Jbuilder or serializers); don't dump models
- Return proper status codes and JSON errors via rescue_from
Watch out for
- Rendering models straight to JSON and leaking columns such as password_digest or internal flags.