Rails API Patterns
Analyze and recommend best practices for Rails API design including controllers, serialization, authentication, versioning, and error handling.
Applies on the api delivery axis (Axis B, rails-stack-profiles) — including an /api namespace inside an otherwise html app. For server-rendered HTML use rails-views-patterns and hotwire-patterns.
Follow standard Rails API controller conventions (ActionController::API, strong parameters, rescue_from). See patterns.md for opinionated decisions and configuration.
Quick Reference
| Area | Decision | Key File |
|------|----------|----------|
| Base controller | ActionController::API + shared concerns | app/controllers/api/base_controller.rb |
| Versioning | URL namespace (/api/v1/) -- NOT header-based | config/routes.rb |
| Auth | JWT bearer token via JwtService | app/controllers/concerns/authenticatable.rb |
| Serialization | Blueprinter preferred (views for complexity) | app/blueprints/ |
| Response format | { data:, meta:, errors: } envelope always | All API responses |
| Pagination | Pagy or Kaminari + max_per_page: 100 cap | app/controllers/concerns/paginatable.rb |
| Rate limiting | Rack::Attack: 100/min per token, 20/min per IP | config/initializers/rack_attack.rb |
| CORS | rack-cors gem, env-driven origins | config/initializers/cors.rb |
Core Decisions
- Response envelope always -- every response wraps in
{ data:, meta:, errors: }, never bare objects/arrays - URL versioning from day one --
/api/v1/namespace even for first version; header-based versioning adds complexity without benefit for most apps - Blueprinter over AMS -- ActiveModel::Serializer is unmaintained; prefer Blueprinter for views-based serialization, or Jbuilder for complex one-offs
- Cap per_page at 100 -- enforce
[params[:per_page].to_i, 100].minto prevent clients from requesting unbounded results - Authenticate in base, skip explicitly --
before_action :authenticateinApi::BaseController, useskip_before_actionfor public endpoints - Dual rate limits -- per-token (100/min) for authenticated, per-IP (20/min) for unauthenticated; return
Retry-Afterheader
Anti-Patterns
| Bad | Good | Why |
|-----|------|-----|
| Render ActiveRecord objects directly | Use serializers/blueprints | Controls exposed fields, prevents leaking columns |
| No pagination on index | Always paginate with max cap | Memory + performance, prevents client abuse |
| Version in header only | URL versioning (/api/v1/) | Simpler routing, cacheable, easier debugging |
| N+1 queries in serializers | includes() in controller | Serializers should not trigger queries |
| Bare error strings | Structured { error:, errors: } envelope | Clients need machine-parseable error format |
| CORS origins "*" in production | Env-driven origin allowlist | Security; wildcard disables credential sharing |
Output Format
When reporting on API design, use:
## API Analysis: [controller_or_endpoint]
**Issues:**
- [severity] description
**Recommendations:**
1. actionable recommendation