When a request reaches a server, one simple question decides everything.
“Where should this request go, and what should happen next?”
That decision is handled by routing.
Routing isn’t a framework feature. It isn’t magic. It’s a very deliberate system that takes your intent and your destination, matches them together, and hands them to the correct server-side logic.
This article breaks routing down exactly how it works in real backend systems.
Routing Starts With Intent
Every HTTP request carries intent. That intent is expressed using the HTTP method.
- GET -> fetch data
- POST -> create data
- PUT -> replace data
- PATCH -> partially update data
- DELETE -> remove data
Methods answer the question: What do you want to do?
But intent alone isn’t enough. The server also needs to know: Where do you want to do it?
That’s where routing comes in.
What Routing Actually Does
Routing defines where your intent should be applied.
A route is the address of a resource on the server.
Example:
GET /users
Here’s what you’re telling the server:
- What -> GET (fetch)
- Where -> /users (the users resource)
The server takes:
- The method (GET)
- The route (/users)
And maps them to a specific handler -> It’s a piece of server side logic that knows exactly what to do.
Routing is essentially: Mapping request intent + URL to server-side logic
Static Routes
A static route never changes.
Example:
GET /api/books
- No variables
- No dynamic values
- Always returns the same type of data
This route always points to the same handler and always represents the same resource: books.
Static routes are predictable and easy to reason about.
Same Route, Different Intent
Routes don’t exist alone. They are always paired with methods.
Example:
GET /api/books
POST /api/books
Even though the route is the same:
GET /api/books-> fetch all booksPOST /api/books-> create a new book
Inside the server, method + route together form a unique key. They don’t clash.
The server checks:
- The HTTP method
- The route path
Only then does it decide which handler to execute.
How Servers Match Routes Internally
Conceptually, routing works like this:
(method, route) → handler
So:
(GET, /api/books) → fetchBooksHandler(POST, /api/books) → createBookHandler
Same path. Different intent. Different logic.
Dynamic Routes (Path Parameters)
Not all routes are static. Sometimes part of the route represents data.
Example:
GET /api/users/123
Here:
123is not part of the route structure- It represents a specific user ID
This is called a path parameter (or route parameter).
Why Path Parameters Exist
Path parameters provide semantic meaning.
/api/users/:id
This clearly communicates: “Fetch the user whose ID is X”
The server extracts the value (123) and passes it to the handler, which can:
- Query the database
- Perform Business logic
- Return the specific user
Pseudocode Example:
r.GET("/api/users/:id", handler)
This tells the server:
- Match GET requests
- Match
/api/users/anything - Treat the last segments as
id
Query Parameters
Sometimes you want to send extra data with a request, without changing the meaning of the resource.
Example:
GET /api/search?query=phone
Everything after ? is a query parameter.
Why Not Use Path Parameters For Everything?
Path parameters are for semantic identification.
Example:
/api/users/123
This has a very specific meaning.
Query parameters are for:
- Filtering
- Sorting
- Pagination
- Optional values
They are key-value pairs. Using path parameters for random data would make APIs hard to maintain and unclear.
Common Use cases for Query Parameters
Pagination:
GET /api/products?page=2
Filtering:
GET /api/products?category=books
Sorting:
GET /api/products?sort=price
Nested Routes
Resources are often related. Nested routes express that relationship.
Example:
GET /api/users/123/posts
Meaning: “Get posts that belong to user 123”
Going deeper:
GET /api/users/123/posts/456
Now you’re requesting: “Post 456 that belongs to user 123”
Each level adds context.
Each Nested Route Is Its Own Handler
These are all separate routes:
/api/users/api/users/:id/api/users/:id/posts/api/users/:id/posts/:postId
Each maps to a different handler and different logic.
Route Versioning
APIs evolve. When response formats change, you need a safe way to support existing clients.
Example:
GET /api/v1/products
GET /api/v2/products
Why Version Routes?
Let’s say:
v1returns:id, name, pricev2returns:id, title, price
Old clients keep working. New clients use the new structure. No breaking changes.
Deprecation Workflow
- Release v2
- Notify frontend or mobile teams
- Allow a migration window
- Deprecate v1
- Eventually remove it
This creates stable and predictable API evolution.
Catch-All Routes
What happens when a request hits a route that doesn’t exist?
Example:
GET /api/v3/products
If the server has no handler, the request falls through.
Catch-All Handling
Servers usually define a final route: /*
This catches everything that wasn’t matched earlier. Instead of returning nothing, the server responds with a clear message:
- Route not found
- Helpful error response
This improves developer experience and debugging.
Final Thoughts
Routing is simple in concept, but powerful in practice.
It connects:
- Intent (HTTP method)
- Destination (URL)
- Logic (handler)
Once you understand routing, APIs stop feeling abstract. They become structured, predictable systems that are easy to extend.