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
Enter fullscreen mode Exit fullscreen mode
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'
Enter fullscreen mode Exit fullscreen mode
If someone requests:
GET /about
Enter fullscreen mode Exit fullscreen mode
Rails knows that it should call:
PagesController#about
Enter fullscreen mode Exit fullscreen mode
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'
Enter fullscreen mode Exit fullscreen mode
Rails lets us write:
resources :products
Enter fullscreen mode Exit fullscreen mode
And generates the conventional CRUD routes for us.
HTTP Verb Action Purpose GETindex
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
Enter fullscreen mode Exit fullscreen mode
There can be many products, so Rails generates an index route.
resource represents a single resource:
resource :profile
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
we can be explicit:
resources :products, only: [:index, :show]
Enter fullscreen mode Exit fullscreen mode
Or exclude actions:
resources :products, except: [:destroy]
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
This generates something like:
GET /products/:id/activate
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Now Rails can generate routes such as:
/posts/:post_id/comments
/posts/:post_id/comments/:id
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Now we can have:
Api::V1::ProductsController
Enter fullscreen mode Exit fullscreen mode
and a URL such as:
/api/v1/products
Enter fullscreen mode Exit fullscreen mode
This is particularly useful when versioning an API.
We can have:
/api/v1/products
/api/v2/products
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
This maps to something like:
Api::V1::ProductsController
Enter fullscreen mode Exit fullscreen mode
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"
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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 Meaning304
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
Enter fullscreen mode Exit fullscreen mode
can communicate:
“The request was understood, but the data isn’t valid.”
While:
429
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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"
}
}
Enter fullscreen mode Exit fullscreen mode
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 / HATEOASLevel 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
Enter fullscreen mode Exit fullscreen mode
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.