Backend Routing: How Requests Find Their Way Home
If HTTP is the what and how of communication, routing is the where. Understanding it deeply separates engineers who use frameworks from engineers who understand them.
In my last post, I built a mental model for HTTP — the shared language between clients and servers. But a language is useless without an address system. That's routing.
If HTTP is the what and how of communication, routing is the where. And understanding it deeply separates engineers who use frameworks from engineers who understand what the framework is doing for them.
The Mental Model: A City's Road System
Imagine your backend server as a city. Every incoming HTTP request is a person arriving at the city gates, holding a piece of paper with two things written on it:
- What they want to do (HTTP method — GET, POST, DELETE)
- Where they want to go (URL path — /api/users/123)
Routing is the road system of that city. It reads both pieces of information, finds the exact building (handler function) the person needs, and delivers them there.
The key insight: the method and the path together form a unique address. GET /api/books and POST /api/books are two completely different buildings on the same street. Same street name, different intent, different destination.
GET /api/books → Handler A (fetches all books)
POST /api/books → Handler B (creates a new book)
Without both pieces, the city doesn't know where to send you.
Static Routes: Named Buildings
Static routes are the simplest kind — fixed addresses that never change.
Think of them as permanent buildings with fixed names: the Library, the Hospital, the Post Office. The path /api/books always points to the same handler. No variables, no ambiguity.
/api/books → always the "books collection" handler
/api/health → always the "server health check" handler
/api/login → always the "authentication" handler
Every real application starts here. But the moment you need to access a specific resource — not "all books" but "book #42" — static routes aren't enough.
Dynamic Routes: Buildings With Room Numbers
This is where routing gets powerful.
Dynamic routes add variable slots into the path. Think of it this way: /api/users/:id isn't a single building — it's an apartment complex. The :id is the room number. When someone arrives at /api/users/123, the router reads the address, walks them to the apartment complex, and extracts "123" as the specific room to enter.
Pattern: /api/users/:id
Request: /api/users/123 → id = "123"
Request: /api/users/456 → id = "456"
One route definition handles infinite specific resources. The framework extracts the dynamic segment and hands it to your handler as a path parameter (also called a route parameter).
Important detail: the extracted value is always a string. Even though 123 looks like a number, it arrives as "123". Your handler is responsible for parsing and validating it.
The Two Ways to Send Data in a URL
This is a distinction that trips up a lot of engineers early on. There are two mechanisms, and they serve fundamentally different purposes.
Path Parameters — "Which resource?"
Path parameters are baked directly into the URL structure. They identify the resource.
/api/users/123
The 123 isn't optional metadata — it IS the address. Without it, you're asking for a different resource entirely. If 123 doesn't exist, the correct response is 404 Not Found, because the resource itself can't be located.
Query Parameters — "How should I look at it?"
Query parameters are appended after a ? at the end of the URL. They modify the request — filtering, sorting, paginating — without changing which resource you're targeting.
/api/books?genre=fiction&sort=rating&page=2
You're still asking for the books collection. You're just telling the server: "show me fiction books, sorted by rating, page 2." If nothing matches, the correct response is 200 OK with an empty list — the collection exists, the filter just returned no results.
The Decision Rule
Ask yourself: "Does the request make sense without this parameter?"
- No → It's a path parameter. It identifies the resource. (/api/users/123 — without 123, you're asking for all users, a completely different request.)
- Yes → It's a query parameter. It refines the request. (/api/books?page=2 — without page=2, you still get books, just the first page.)
Here's a mistake I've seen engineers make:
/api/search/iphone ← BAD: "iphone" isn't a resource, it's a filter
/api/search?query=iphone ← GOOD: the resource is "search", the query refines it
Path parameters say what thing. Query parameters say how to look at the thing.
Nested Routes: The Organizational Chart
Real data has relationships. Users have posts. Posts have comments. Comments have replies. Nested routing expresses this hierarchy directly in the URL, making the API self-documenting.
Think of it as zooming in on a map:
/api/users → The entire country (all users)
/api/users/123 → A specific state (user 123)
/api/users/123/posts → Cities in that state (user 123's posts)
/api/users/123/posts/456 → A specific building (post 456 by user 123)
Each / takes you one level deeper into the relationship tree. Reading the URL left to right tells you the exact context of what you're accessing. You don't need documentation to understand that /api/users/123/posts/456 means "post 456 belonging to user 123."
This is what makes REST APIs readable — the URL itself is a semantic sentence:
GET /api/users/123/posts/456/comments
Translation: "Get me the comments on post 456 by user 123"
A practical guardrail: if your nesting goes deeper than 3 levels, it's usually a sign you should flatten it. /api/comments?postId=456 is often cleaner than /api/users/123/posts/456/comments.
Route Versioning: Renovating Without Evicting Tenants
This is one of the most practically important routing concepts, and the mental model is straightforward.
Imagine you own an apartment building (your API). Tenants (frontend clients, mobile apps, third-party integrations) have signed leases based on the current floor plan. Now you want to renovate — change the response structure, rename fields, restructure data.
You can't just knock down walls while people are living there.
The solution: build a new wing.
/api/v1/products → { "id": 1, "name": "Shoe", "price": 100 }
/api/v2/products → { "id": 1, "title": "Shoe", "price": { "amount": 100, "currency": "USD" } }
Both versions run simultaneously. The old tenants (v1 clients) keep living in the original wing. New tenants move into v2. You give everyone a timeline:
- Release v2 — the new wing is open
- Notify v1 clients — "You have 6 months to move"
- Migration window — frontend teams update their code
- Deprecate v1 — tear down the old wing
Without versioning, every API change is a potential production outage. With it, you get backward compatibility by design.
Catch-All Routes: The Safety Net
Every well-designed city has a "you are lost" information desk. That's your catch-all route.
/* → { "error": "Route not found" }
It sits at the very bottom of your route definitions. The server checks routes top to bottom — static routes first, then dynamic routes, then the catch-all. If nothing above matches, the request falls through to this safety net.
Without it, a typo like /api/v3/prodcts would return a cryptic framework error or a blank response. With it, the client gets a clean 404 with a helpful message. Small thing, but it's the difference between a professional API and a confusing one.
Route Priority Mental Model
When a request arrives, the router evaluates in this order:
1. Exact static match → /api/books (highest priority)
2. Dynamic segment match → /api/books/:id (medium priority)
3. Catch-all wildcard → /* (lowest priority, last resort)
This is why route ordering matters in some frameworks. A wildcard route placed above a static route will swallow requests that were meant for the static handler.
Under the Hood: How Routers Actually Match
This part isn't necessary for daily work, but it strengthens the mental model.
A naive router checks every registered route one by one — linear scan, O(n). With 10 routes, fine. With 500 routes in a large application, every request pays the cost of scanning through all of them.
Production routers solve this with a radix tree (also called a prefix tree). Imagine organizing all your routes into a tree where shared prefixes are merged:
/api/
├── books
│ └── /:id
├── users
│ └── /:id
│ └── /posts
└── health
When a request for /api/users/123/posts arrives, the router doesn't scan 500 routes. It walks down the tree: /api → users → 123 (matches :id) → posts. Done. The lookup cost depends on the path length, not the number of routes. 10 routes or 10,000 routes — same speed.
The Complete Flow: Putting It All Together
Here's the full lifecycle of a routed request, from arrival to response:
Client sends: GET /api/users/123/posts?page=2&sort=newest
1. Server reads: Method = GET, Path = /api/users/123/posts
2. Router matches: GET /api/users/:id/posts
Extracts: id = "123"
Extracts: page = "2", sort = "newest"
3. Handler executes:
- Validates id is a valid user
- Queries database for user 123's posts
- Applies pagination (page 2) and sorting (newest first)
4. Server responds:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [...],
"pagination": { "page": 2, "totalPages": 5 }
}
Every piece we covered — method matching, path parameters, query parameters, nested hierarchy — plays its role in this single flow.
The Takeaway
Routing is deceptively simple on the surface: match a URL to a function. But the design decisions inside that simplicity — static vs dynamic, path vs query, flat vs nested, versioned vs unversioned — are what make an API intuitive or infuriating.
The mental model to carry forward:
- Method = what you want to do
- Path = which resource you want
- Path parameters = identity ("which one?")
- Query parameters = modifiers ("how should I look at it?")
- Nested routes = relationships ("belonging to what?")
- Versioning = backward-safe evolution
- Catch-all = the safety net for everything else
If HTTP is the language, routing is the address system. And a good address system means nobody gets lost.
This is part 2 of my "Backend from First Principles" series. Everything in this article comes from what I learned watching the Backend from First Principles playlist by Sriniously — specifically the video "What is Routing in Backend? How Requests Find Their Way Home." I'm not claiming original research. This is me taking what I studied, internalizing it, and presenting it as a mental model that made the concepts stick for me. If it helps you too, even better.
Playlist: https://youtube.com/playlist?list=PLui3EUkuMTPgZcV0QhQrOcwMPcBCcd_Q1
Follow along for the next one.
#BackendEngineering #Routing #RESTAPI #WebDevelopment #SoftwareEngineering #SystemDesign #LearningInPublic