Rails Development

Rails API Development: Building Fast, Reliable APIs That Don't Fall Apart

How I approach Rails API development for real production systems — authentication, versioning, performance, and the mistakes most teams make.

J

Justin Hamilton

Founder & Principal Engineer

rails ruby api rest web development

I’ve built a lot of Rails APIs over the years. Some were clean and a pleasure to work with. Others were inherited messes that taught me exactly what not to do. Here’s what I’ve learned from both.

Why Rails for APIs?

There’s a constant debate in developer circles about whether Rails is “too much” for an API. The argument usually goes that Rails was built for full-stack web apps, so using it for a JSON API means you’re dragging in a bunch of baggage you don’t need.

I disagree. Rails API mode — introduced with rails new myapp --api — strips out the view layer, cookie-based session middleware, and a handful of other things you don’t need. What’s left is a fast, well-organized framework with a 20-year track record of production reliability.

The real advantages: mature ecosystem, excellent ORM with ActiveRecord, solid background job support, and a community that’s seen every problem you’re about to encounter. When you’re building for a manufacturing company or mid-market business, you want boring and reliable. Rails delivers that.

Setting Up Rails in API Mode

Starting fresh is easy:

rails new myapp --api --database=postgresql

That gives you a trimmed-down stack — no ActionView, no sprockets, no cookie sessions. Your ApplicationController inherits from ActionController::API instead of ActionController::Base.

For an existing app being converted to API-only, it’s a bit more work, but manageable.

Authentication That Actually Works

The most common mistake I see in Rails APIs is authentication bolted on as an afterthought. Here’s how I approach it:

JWT for stateless APIs. For most API use cases, JSON Web Tokens make sense. The jwt gem is solid, and combining it with a service object for token generation and verification keeps things clean.

class JsonWebToken
  SECRET = Rails.application.credentials.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET)
  end

  def self.decode(token)
    JWT.decode(token, SECRET)[0]
  rescue JWT::DecodeError
    nil
  end
end

Devise + devise-jwt for apps that already use Devise. It’s opinionated but handles a lot of the edge cases for you.

API keys for machine-to-machine. If you’re building an API that other services will consume — not user-facing — API keys with proper scoping are simpler and more appropriate than JWT.

Versioning Your API

Version from day one. Don’t wait until you need to break a contract and then scramble to figure out how to do it without destroying existing integrations.

The simplest approach: namespace your routes and controllers by version.

# routes.rb
namespace :api do
  namespace :v1 do
    resources :orders
    resources :products
  end
end

Your controllers live in app/controllers/api/v1/. When you need to make breaking changes, you add v2 without touching v1. Existing integrations keep working. New ones use the new contract.

Serialization Done Right

ActiveRecord models shouldn’t be your API response format. Use a serialization layer.

I’ve used several options over the years — active_model_serializers, fast_jsonapi (now jsonapi-serializer), and just plain Ruby objects. My current preference is jsonapi-serializer for complex APIs and simple presenter objects for smaller ones.

The point is the same regardless of the tool: keep your API contract separate from your data model. What’s in your database is an implementation detail. What you expose to API consumers is a contract.

Error Handling

Consistent error responses are non-negotiable. Every error from your API should return a predictable JSON structure so clients can handle them programmatically.

# application_controller.rb
rescue_from ActiveRecord::RecordNotFound do |e|
  render json: { error: 'not_found', message: e.message }, status: :not_found
end

rescue_from ActiveRecord::RecordInvalid do |e|
  render json: { error: 'validation_failed', errors: e.record.errors }, status: :unprocessable_entity
end

Set this up once in ApplicationController and it propagates everywhere.

Performance That Matters

The biggest performance killers I see:

N+1 queries. Every Rails developer knows about this, and yet it keeps showing up. Use includes() to eager load associations. The Bullet gem in development will catch the ones you miss.

No pagination. Returning unbounded collections to API consumers is a disaster waiting to happen. Paginate everything with something like kaminari or pagy. pagy is my current preference — it’s fast and has minimal overhead.

Missing database indexes. Every foreign key, every column you filter or sort on — it needs an index. Run rails db:migrate and check the explain plan for your slow queries.

Too much in the controller. Business logic in controllers kills testability and usually leads to performance problems because you lose visibility into what’s actually happening. Service objects keep things clean.

Testing Rails APIs

Test your API contracts, not just your models. I use RSpec with rails-controller-testing or request specs. Request specs are the better option — they test the full stack including routing, middleware, and serialization.

describe "GET /api/v1/orders" do
  it "returns the user's orders" do
    user = create(:user)
    orders = create_list(:order, 3, user: user)

    get "/api/v1/orders", headers: auth_headers(user)

    expect(response).to have_http_status(:ok)
    expect(json_response["data"].length).to eq(3)
  end
end

The Part Everyone Ignores: Documentation

Your API is only as useful as its documentation. If you’re building an API that others will consume, document it. I use rswag to generate OpenAPI docs directly from RSpec specs — it forces you to test and document at the same time.

If you’re building an internal API that only your own frontend consumes, keep a simple API changelog and make sure your frontend devs are in the loop when contracts change.

When Rails API Mode Is the Right Call

Rails API mode makes sense when:

  • You have a team that knows Ruby/Rails
  • You’re building a complex domain model that benefits from ActiveRecord
  • You need the Rails ecosystem (background jobs, mailers, solid testing tools)
  • You’re building for long-term maintenance

If you’re building a tiny service that does one thing — maybe just handles webhooks or proxies a third-party API — a lighter option might be more appropriate. But for most real business applications with real domain complexity, Rails API mode is a solid, boring, reliable choice. And boring and reliable is exactly what your clients need.


If you’re working on a Rails API and things have gotten messy, or you’re starting from scratch and want to do it right — I’d be happy to talk. Reach out here.

Let's Build Something Together

Hamilton Development Company builds custom software for businesses ready to stop fitting themselves into someone else's box. $500/mo retainer or $125/hr — no surprises.

Schedule a Consultation