APIs with Rails
🔌 APIs with Rails
rails1-2/rails1-3 — resourceful routes, the 7 actions — carry over almost unchanged; what changes is what gets rendered at the end of each action.
⚙️ API-Only Mode
--api strips out everything a JSON-only backend doesn't need: no view layer generators, no asset pipeline, no cookie/session/flash middleware, no CSRF protection (there's no HTML form to forge a submission from). The base controller class changes too:
ActionController::API (instead of ActionController::Base) is a deliberately smaller module list — it keeps the parts of Action Controller genuinely useful for a JSON API (strong parameters, rescue_from, before_action) and drops the HTML-rendering machinery this app will never call.
📤 Rendering JSON
render json: posts calls .to_json on the object automatically, converting an Active Record relation or model instance into a JSON response with the correct Content-Type header set — no separate templating step required for the simplest cases. status: :created/:unprocessable_entity are the same symbol-named HTTP status codes express1-7 covered numerically (201, 422); Rails just lets you write the name instead of memorizing the number.
🏗️ Serializers — Controlling the Shape of the JSON
render json: post serializes every database column on the model — including things like a soft-delete flag, an internal moderation score, or (on a User) a password_digest column that should never leave the server. This is the JSON-API equivalent of the mass-assignment problem rails1-8's strong parameters solved for incoming data — except here it's the outgoing shape that needs explicit control.
A serializer declares an explicit allowlist of attributes — the opposite default of "serialize everything." Once PostSerializer exists, render json: post picks it up automatically by naming convention (Post → PostSerializer, the same convention-by-class-name pattern rails2-2's Pundit policies used). Nested associations (belongs_to :user) get their own serializer too, so a leaked column on one model can't sneak in through a different model's association.
🔢 Versioning
namespace nests both the URL path (/api/v1/...) and the expected controller location (Api::V1::PostsController) in one declaration — the same resources convention from rails1-2 generating all 7 routes at once, just scoped under a versioned prefix. When breaking API changes are needed later, Api::V2::PostsController can be added alongside the untouched v1 namespace, so existing API consumers keep working against the version they built against.
🔑 Authenticating an API
rails2-1 covered session-based authentication — a cookie holding a session ID, which fits a browser but doesn't fit a mobile app or a separate JS frontend well. API-only apps typically authenticate with a bearer token instead:
This is a simplified static-token example — a real app more often issues a short-lived JWT here, exactly the token-based approach the completed Auth & Session Security course (bc1-7) covered in depth, including access/refresh rotation and safe storage. The mechanism differs from Chapter 1's session cookie, but the shape of the check — reject the request before the action runs if authentication fails — is identical.
📜 Rails APIs vs Express REST APIs
Rails (API-only) vs Express (express1-7)
| Concern | Express | Rails (--api) |
|---|---|---|
| Routes | Manually defined per method/path | resources :posts generates all 7 |
| Response shaping | Hand-built response objects per route | Serializer classes, reused across actions |
| Status codes | Numeric (res.status(201)) | Symbol (status: :created) |
| Versioning | Manual route prefixing (/v1/posts) | namespace :api do namespace :v1 |
| Auth | JWT middleware, hand-written | before_action, token or JWT-based |
The philosophical difference is the same one that has run through this entire course: Express gives you the primitives and expects you to assemble them per project; Rails' --api mode is the same convention-over-configuration philosophy from rails1-1, just with the HTML-rendering half of the framework removed rather than the framework replaced by something new.
💻 Coding Challenges
Challenge 1: A JSON Index Action
Write an index action for an API-only CommentsController that renders all comments as JSON, and a create action that renders the created comment with a 201 status on success, or the comment's errors with a 422 status on failure.
Goal: Practice render json: with both success and failure status codes.
Challenge 2: A Serializer
Write a CommentSerializer that exposes only id, body, and created_at, plus a nested user association using a UserSerializer that exposes only id and name (no email or password data).
Goal: Practice controlling exactly what a model exposes to API consumers.
Challenge 3: Versioned Routes
Write the config/routes.rb entry that namespaces a resources :comments route under /api/v1, and give the full class name (including module nesting) the corresponding controller would need.
Goal: Practice namespacing routes and controllers together for API versioning.
This chapter's serializer allowlist, Chapter 2's Pundit policy, and Course 1's model validations are all the same underlying pattern: a rule about a model, written once, in one predictable location, applied consistently everywhere that model is touched. By this point in the course, spotting "where does this rule live, and is it written in exactly one place" should be a reflex whenever a new Rails feature comes up.
🎯 What's Next
Next chapter: Action Cable — Rails' WebSockets layer, for the cases where even a fast JSON API isn't enough and the server needs to push updates to connected clients in real time, contrasted with Socket.IO from express2-6.