When I started studying APIs more seriously, I realized there was a problem with the way I was learning.
I knew how to create a Rails API.
I knew how to write:
resources :products
I knew what GET, POST, PATCH and DELETE were supposed to do.
But I wasn't always able to explain why things worked the way they did.
So I decided to go one step back and review the fundamentals: routing, HTTP, REST and how Rails puts all of these things together.
This is what I learned.
Rails Routing
At its simplest, routing is the thing that connects a URL to some code in your application.
In Rails, this happens in routes.rb.
For example:
get '/about', to: 'pages#about'
If someone requests:
GET /about
Rails knows that it should call:
PagesController#about
Pretty straightforward.
But Rails gets much more interesting when we start using RESTful routes.
resources does a lot of work
Instead of manually defining every route for a resource:
get '/products', to: 'products#index'
get '/products/:id', to: 'products#show'
post '/products', to: 'products#create'
patch '/products/:id', to: 'products#update'
delete '/products/:id', to: 'products#destroy'
Rails lets us write:
resources :products
And generates the conventional CRUD routes for us.
| HTTP Verb | Action | Purpose |
|---|---|---|
| GET | index |
List resources |
| GET | show |
Show one resource |
| GET | new |
Form for a new resource |
| POST | create |
Create a resource |
| GET | edit |
Form to edit a resource |
| PATCH | update |
Update a resource |
| DELETE | destroy |
Delete a resource |
This is one of the reasons Rails feels so productive.
The framework isn't just giving us routing functionality. It is encouraging a convention.
resource vs resources
This one confused me for a while.
resources represents a collection:
resources :products
There can be many products, so Rails generates an index route.
resource represents a single resource:
resource :profile
There isn't an index because we're talking about one profile.
It is a small difference, but it makes sense once you think about the resource you're modeling.
only and except
We don't always need every CRUD action.
Instead of:
resources :products
we can be explicit:
resources :products, only: [:index, :show]
Or exclude actions:
resources :products, except: [:destroy]
I generally prefer only when designing an API because it makes the exposed interface explicit.
member vs collection
Eventually, you'll need an action that doesn't quite fit the standard CRUD operations.
For example:
resources :products do
member do
get :activate
end
end
This generates something like:
GET /products/:id/activate
The important part is that activate operates on one product.
That's what member means.
For operations on the collection:
resources :products do
collection do
get :download_all
end
end
The resulting route doesn't require a product ID.
So my mental shortcut is:
-
member→ one resource -
collection→ multiple resources / the collection
Nested resources
Sometimes resources have a natural relationship.
For example, posts and comments:
resources :posts do
resources :comments
end
Now Rails can generate routes such as:
/posts/:post_id/comments
/posts/:post_id/comments/:id
This makes the relationship explicit in the URL.
But nested routes can also become difficult to maintain when taken too far. So just because Rails lets us nest resources doesn't mean we should create five levels of nesting.
When the application becomes an API
Now things get more interesting.
Imagine we have:
/api/v1/products
We probably don't want our API controllers mixed together with our regular web controllers.
Rails gives us namespace for this.
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :products
end
end
Now we can have:
Api::V1::ProductsController
and a URL such as:
/api/v1/products
This is particularly useful when versioning an API.
We can have:
/api/v1/products
/api/v2/products
with different controllers and behavior behind them.
The idea is simple:
Versioning is a way of allowing the old world and the new world to exist at the same time.
Clients using v1 don't suddenly break because we changed something in v2.
namespace vs scope
These two can look very similar, but they have an important difference.
namespace changes both the URL and the controller module.
namespace :api do
namespace :v1 do
resources :products
end
end
This maps to something like:
Api::V1::ProductsController
A scope, on the other hand, can change the URL without necessarily changing the controller namespace.
This becomes useful when organizing routes without wanting to mirror that organization in the controller structure.
But what exactly is REST?
This is where I realized I had been using the word "REST" rather casually.
REST stands for:
Representational State Transfer.
It comes from Roy Fielding's dissertation and describes an architectural style based on a set of constraints.
The six constraints are:
- Client-Server
- Stateless
- Cacheable
- Uniform Interface
- Layered System
- Code on Demand (optional)
The important part here is that REST isn't simply:
"Use JSON and HTTP verbs."
There is considerably more to it.
Client-Server
The client and server have separate responsibilities.
The client is responsible for the user interface and user experience.
The server is responsible for the data and business logic.
They communicate through a defined interface.
Stateless
Each request should contain everything the server needs to understand it.
The server shouldn't need to remember the state of the previous request in order to process the next one.
This is one of those concepts that sounds obvious until you start designing distributed systems.
Cacheable
Responses should indicate whether they can be cached.
Caching isn't just an optimization that happens somewhere in front of your application. It is part of the architectural model.
Uniform Interface
This is probably the most important part of REST.
The interface should be consistent.
Resources should be identified by URLs.
Resources can be manipulated through representations.
Messages should be self-descriptive.
And HATEOAS is part of this constraint as well.
Resource vs Representation
Another distinction that helped me understand REST better:
A resource is the thing we're talking about.
A representation is how that resource is presented.
For example:
Resource:
Product #42
Representation:
{
"id": 42,
"name": "Keyboard"
}
The product is the resource.
The JSON is its representation.
The same resource could potentially have different representations, such as JSON or XML.
HTTP is more than GET and POST
When building APIs, it's easy to think about HTTP methods simply as CRUD operations.
But their semantics matter.
For example, idempotency is an important concept.
An operation is idempotent when repeating the same request produces the same intended result.
Methods such as:
GET
PUT
DELETE
HEAD
OPTIONS
are defined as idempotent.
PATCH is more interesting because it can be idempotent, but isn't necessarily so. It depends on what the operation actually does.
That distinction matters when designing APIs that clients might retry.
HTTP status codes
A good API communicates through status codes.
Some that I find particularly useful to keep in mind:
| Status | Meaning |
|---|---|
304 |
Not Modified |
404 |
Not Found |
409 |
Conflict |
422 |
Unprocessable Entity |
429 |
Too Many Requests |
They tell the client something about what happened without requiring the client to interpret an arbitrary application-specific response.
For example:
422
can communicate:
"The request was understood, but the data isn't valid."
While:
429
basically means:
"You're making too many requests. Slow down."
Stateless vs Stateful
This is another distinction worth remembering.
A stateless server doesn't depend on remembering previous requests from a client.
A stateful system does.
For APIs, statelessness is valuable because requests can be handled more independently. This becomes particularly useful when you have multiple application instances behind a load balancer.
CORS and OPTIONS
CORS is another thing that tends to appear when working with APIs.
At a high level, CORS controls whether a browser is allowed to make requests from one origin to another.
The server can tell the browser which origins, methods and headers are allowed.
OPTIONS requests are often involved in this process because the browser can use them to determine what the server permits.
This is one of those topics that feels mysterious until you realize that the browser is enforcing the rules, not your Rails application magically refusing the request.
REST vs GraphQL
REST and GraphQL solve some similar problems in different ways.
With REST, we generally expose multiple endpoints representing resources:
GET /products
GET /products/42
GET /products/42/reviews
The server defines the representation returned by each endpoint.
With GraphQL, we typically have a single endpoint and the client specifies which data it wants.
A very simplified mental model is:
REST is several doors with a fixed menu.
GraphQL is one door where you can order exactly what you want.
Neither approach is automatically better. They optimize for different problems.
HATEOAS
HATEOAS stands for Hypermedia as the Engine of Application State.
The idea is that a response can contain information about what the client can do next.
You can think of it as a kind of GPS for an API.
Instead of the client having to know every possible next URL, the server provides links representing the available transitions.
For example, a response could conceptually contain:
{
"id": 42,
"status": "pending",
"_links": {
"self": "/orders/42",
"cancel": "/orders/42/cancel"
}
}
The client can use the links to understand the available actions.
It's a powerful idea, although it also adds complexity and isn't used by every API that calls itself RESTful.
The Richardson Maturity Model
Another useful way of thinking about APIs is the Richardson Maturity Model.
It describes different levels of RESTfulness:
| Level | Idea |
|---|---|
| 0 | One endpoint / one method |
| 1 | Resources |
| 2 | HTTP verbs and status codes |
| 3 | Hypermedia / HATEOAS |
Level 0 is basically the "Swamp of POX": everything goes through a single endpoint, often using POST.
At level 1, we start identifying resources.
At level 2, we properly use HTTP methods and status codes.
At level 3, hypermedia enters the picture.
This was useful for me because it showed that there's a difference between:
"I have an HTTP API."
and:
"I have an API that follows REST principles."
So where does Rails fit into all of this?
Rails gives us conventions and tools that make it very easy to build APIs following these principles.
For example:
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :products
end
end
From this small piece of routing configuration, we're already expressing quite a lot:
- The API has a namespace.
- It has a version.
- It expects JSON.
- Products are modeled as resources.
- Standard HTTP verbs map to standard CRUD actions.
And that's probably the biggest thing I took away from revisiting all of this.
Rails makes a lot of these concepts feel automatic.
But "automatic" doesn't mean "unimportant."
Understanding what Rails is doing underneath the conventions makes it much easier to design APIs intentionally instead of just following patterns because that's what Rails usually does.
And I think that's one of the differences between knowing Rails and understanding what you're building with Rails.
These are my notes from revisiting Rails routing, HTTP and REST fundamentals. There are definitely deeper rabbit holes here, especially around REST constraints, idempotency, caching and HATEOAS—but understanding these fundamentals already makes the day-to-day work with APIs much less mysterious.
Top comments (0)