Every time you interact with an application, there is usually a request happening somewhere in the background.
You open a product:
GET /api/products/42
You log in:
POST /api/login
You update your profile:
PATCH /api/profile
You delete a post:
DELETE /api/posts/10
But when these requests reach the backend, how does the server know which piece of code should handle which request?
That's where backend routing comes in.
Routing is the mechanism that maps an incoming request to the code responsible for processing it.
A simple mental model is:
HTTP Request
↓
Router
↓
Matching Route
↓
Middleware
↓
Controller
↓
Business Logic
↓
Response
Routing sounds simple at first, but it becomes an important architectural concern as an application grows.
Let's understand what actually happens.
1. What Is Backend Routing?
A route is essentially a rule that says:
"When a request with this HTTP method reaches this path, run this handler."
For example:
app.get("/api/products", getProducts);
This tells the backend:
Method: GET
Path: /api/products
Handler: getProducts
So when the client sends:
GET /api/products
the router finds the matching route and executes:
getProducts();
That's the basic idea behind routing.
But a production backend usually does much more than simply match a URL.
The request may go through authentication, authorization, validation, logging, rate limiting, controllers, services, caches, databases, and external APIs before a response is produced.
2. Why Do We Need Routing?
Imagine a backend application with hundreds of APIs.
You might have:
/api/users
/api/users/:id
/api/products
/api/products/:id
/api/orders
/api/orders/:id
/api/payments
/api/login
/api/logout
/api/notifications
The server needs a way to distinguish between them.
For example:
GET /api/products
↓
Product Handler
GET /api/orders
↓
Order Handler
POST /api/login
↓
Login Handler
Without routing, the backend would have no organized way to decide which code should process an incoming request.
Routing gives the application structure.
It creates a clear boundary between the outside world and the internal code that performs the actual work.
As the number of endpoints grows, this becomes increasingly important. A backend with 10 routes can be easy to understand even if everything is in one file. A backend with 200 or 500 routes needs much stronger organization.
3. A Route Is More Than Just a URL
A common mistake is to think:
Route = URL
It is actually closer to:
Route = HTTP Method + Path + Handler
For example:
app.get("/users", getUsers);
app.post("/users", createUser);
Both routes use:
/users
but they mean completely different things.
GET /users
↓
Retrieve users
POST /users
↓
Create a user
The HTTP method is therefore part of the route definition.
This is why changing only the method can completely change the meaning of an endpoint.
4. Routing Starts After the Request Arrives
Suppose the browser sends:
GET /api/products/42 HTTP/1.1
Host: example.com
The backend receives the request.
Conceptually:
Client
↓
HTTP Request
↓
Server
↓
Router
The router examines information such as:
Method → GET
Path → /api/products/42
It then searches for a matching route.
For example:
app.get("/api/products/:id", getProduct);
The router recognizes that:
/api/products/42
matches:
/api/products/:id
and passes the request to getProduct.
The router doesn't necessarily care what happens inside getProduct. Its primary responsibility is determining where the request should go.
5. Static Routes
The simplest routes use fixed paths.
For example:
app.get("/api/products", getProducts);
app.get("/api/orders", getOrders);
app.get("/api/users", getUsers);
These are static routes.
The path has to match the defined route.
For example:
GET /api/products
matches:
/api/products
but:
GET /api/product
doesn't.
Static routes are useful for endpoints where the resource itself doesn't need an identifier in the path.
They are especially common for collection-level operations.
6. Dynamic Routes
What if you want to retrieve a specific product?
You could create:
/api/products/1
/api/products/2
/api/products/3
/api/products/4
You obviously don't want to create a separate route for every product.
Instead, you use a dynamic parameter:
app.get("/api/products/:id", getProduct);
Now all of these can match:
/api/products/1
/api/products/42
/api/products/999
The :id part is a route parameter.
The backend can access it:
app.get("/api/products/:id", (req, res) => {
const id = req.params.id;
console.log(id);
});
For:
GET /api/products/42
you get:
req.params.id
↓
"42"
Dynamic routes allow one route definition to handle potentially thousands or millions of resources.
7. Route Parameters Represent Resources
Dynamic routes are especially useful for REST-style APIs.
For example:
GET /users/42
can mean:
Get user 42.
GET /users/42/orders
can mean:
Get orders belonging to user 42.
GET /products/100/reviews
can mean:
Get reviews for product 100.
The path can communicate relationships between resources.
A useful structure might be:
/users
/users/:id
/users/:id/orders
/products
/products/:id
/products/:id/reviews
This makes APIs easier for developers to understand because the URL structure communicates the resource hierarchy.
8. Query Parameters Are Different
Consider:
/api/products?page=2&limit=20
Here:
/api/products
is the route path.
While:
?page=2&limit=20
contains query parameters.
In Express:
app.get("/api/products", (req, res) => {
const page = req.query.page;
const limit = req.query.limit;
});
So:
/api/products?page=2
gives:
req.query.page
↓
"2"
A useful distinction is:
Path Parameter
/products/:id
↓
Identifies a resource
Query Parameter
/products?page=2
↓
Modifies, filters, sorts, or paginates the request
For example:
/products?category=phones
/products?sort=price
/products?page=3
/products?search=keyboard
These usually don't represent different routes.
They are different ways of querying the same route.
9. Request Body Is Another Source of Data
For a POST request:
POST /api/users
the client might send:
{
"name": "Alex",
"email": "alex@example.com"
}
This data is in the request body.
In Express:
app.post("/api/users", (req, res) => {
const name = req.body.name;
const email = req.body.email;
});
Now you have three common places where request data can come from:
req.params → /users/:id
req.query → /users?page=2
req.body → JSON payload
Knowing which type of data belongs where makes API design much clearer.
10. Routing and Middleware Work Together
A route usually doesn't directly jump into business logic.
There can be middleware in between.
For example:
Request
↓
Logging Middleware
↓
Authentication Middleware
↓
Validation Middleware
↓
Router
↓
Controller
↓
Response
Consider:
app.get(
"/api/profile",
authenticate,
getProfile
);
The request first goes through:
authenticate
and only if that middleware allows it does the request reach:
getProfile
This is useful because authentication doesn't need to be manually repeated inside every handler.
Middleware creates reusable processing steps that can be shared across routes.
11. Route-Level Middleware
You can also apply middleware only to certain routes.
For example:
app.delete(
"/api/users/:id",
authenticate,
requireAdmin,
deleteUser
);
The flow becomes:
DELETE /api/users/42
↓
Authentication
↓
Admin Check
↓
Delete User
This is much cleaner than putting all those checks inside deleteUser.
The route definition itself now describes the processing pipeline.
You can almost read the route like a sentence:
Delete this user, but first authenticate the requester and verify that they are an administrator.
12. Controllers Handle the Request
As applications grow, developers usually avoid putting everything directly inside route definitions.
Instead of:
app.get("/products", async (req, res) => {
// database query
// business logic
// validation
// response
});
you might have:
app.get("/products", getProducts);
and:
async function getProducts(req, res) {
// controller logic
}
Now the architecture becomes:
Route
↓
Controller
↓
Service
↓
Database
This separation becomes valuable as the codebase gets larger.
The route is responsible for mapping requests.
The controller handles the HTTP-specific part.
The service can contain business logic.
The database layer handles persistence.
Each layer has a clearer responsibility.
13. Routes Shouldn't Usually Contain All Business Logic
Imagine this:
app.post("/orders", async (req, res) => {
// authenticate user
// validate product
// check inventory
// calculate discount
// calculate tax
// charge payment
// create order
// update inventory
// send email
});
It works.
But eventually this route becomes difficult to understand and test.
A better structure might be:
Route
↓
Controller
↓
Order Service
↓
Inventory Service
↓
Payment Service
↓
Database
The route answers:
"Which operation should handle this request?"
The service layer answers:
"How should this operation actually work?"
This separation helps control complexity.
It also means changes to business logic don't necessarily require changing the route structure.
14. Route Organization
A large backend might have many route files.
For example:
routes/
users.js
products.js
orders.js
payments.js
auth.js
Then the main application can combine them:
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/orders", orderRoutes);
Inside productRoutes:
router.get("/", getProducts);
router.get("/:id", getProduct);
router.post("/", createProduct);
router.patch("/:id", updateProduct);
router.delete("/:id", deleteProduct);
This produces:
/api/products
/api/products/:id
while keeping product-related routing in one place.
This kind of organization becomes especially useful when multiple developers are working on the same backend.
15. Route Prefixes Reduce Duplication
Instead of writing:
router.get("/api/products", ...);
router.get("/api/products/:id", ...);
router.post("/api/products", ...);
you can mount the router:
app.use("/api/products", productRoutes);
Then inside:
router.get("/", getProducts);
router.get("/:id", getProduct);
router.post("/", createProduct);
The backend combines them:
/api/products + /
↓
/api/products
/api/products + /:id
↓
/api/products/:id
This makes route organization much cleaner.
It also gives each resource its own boundary.
16. Route Matching Order Can Matter
Suppose you have:
router.get("/:id", getProduct);
router.get("/featured", getFeaturedProducts);
Depending on the framework and routing rules, a request like:
/featured
could potentially match the dynamic route first.
A safer ordering is often:
router.get("/featured", getFeaturedProducts);
router.get("/:id", getProduct);
The broader lesson is:
Routing rules are evaluated according to the framework's matching behavior, so route specificity and ordering can matter.
This becomes particularly important when you have dynamic parameters mixed with special static paths.
17. What Happens If No Route Matches?
Suppose the client requests:
GET /api/does-not-exist
and no route matches.
The backend should return an appropriate response, commonly:
404 Not Found
You might have a fallback handler:
app.use((req, res) => {
res.status(404).json({
error: "Route not found"
});
});
Conceptually:
Request
↓
Router
↓
Any matching route?
|
├── Yes → Handler
|
└── No → 404
This makes it clear to the client that the requested endpoint doesn't exist.
18. Error Handling Happens After Routing Too
A route can match correctly and still fail.
For example:
GET /api/products/42
↓
Route matches
↓
Database query
↓
Database fails
↓
Error Handler
↓
500 Response
In Express, applications often have centralized error-handling middleware.
The idea is:
Route
↓
Controller
↓
Service
↓
Error
↓
Central Error Handler
↓
HTTP Response
This prevents every route from having to implement completely different error formatting.
It also gives the API a consistent error structure.
For example:
{
"error": "Something went wrong"
}
A consistent API is easier for frontend and mobile developers to consume.
19. Routing Is Not the Same as Business Logic
This distinction is worth remembering.
Routing answers:
Where should this request go?
Business logic answers:
What should happen after it gets there?
For example:
POST /api/orders
↓
Routing
↓
Order Controller
↓
Order Service
↓
Check inventory
↓
Calculate price
↓
Create order
The route doesn't need to know every detail about creating an order.
It only needs to send the request to the correct part of the application.
This separation keeps the routing layer relatively simple even when the underlying business operation is complicated.
20. Authentication Often Starts Around the Route
Consider:
GET /api/profile
A backend might process it as:
Request
↓
Route
↓
Authentication
↓
Controller
↓
User Service
↓
Database
↓
Response
The route identifies the operation.
Authentication identifies the user.
The service retrieves the relevant information.
The controller turns the result into an HTTP response.
This separation is one of the reasons layered backend architectures are easier to reason about.
21. Routing and REST API Design
Good routing also makes APIs easier to understand.
Instead of creating routes like:
/getAllProducts
/getProductById
/createNewProduct
/deleteProduct
a REST-style API might use:
GET /products
GET /products/:id
POST /products
PATCH /products/:id
DELETE /products/:id
The HTTP method communicates the operation.
The path represents the resource.
So:
GET /products/42
means:
Retrieve product 42.
while:
DELETE /products/42
means:
Delete product 42.
The same resource can therefore have multiple operations without creating completely different naming conventions.
22. Nested Routes Represent Relationships
Sometimes resources are related.
For example:
GET /users/42/orders
can represent the orders belonging to user 42.
Similarly:
GET /products/10/reviews
can represent reviews belonging to product 10.
The structure communicates the relationship:
User
↓
Orders
Product
↓
Reviews
However, deeply nested routes can become difficult to work with.
For example:
/users/42/orders/10/items/5/reviews
may technically work, but it can become unnecessarily complicated.
Good API design usually aims for routes that are clear without making the URL hierarchy excessively deep.
23. Versioning Routes
APIs sometimes need to evolve without immediately breaking existing clients.
You might see:
/api/v1/users
/api/v2/users
For example:
app.use("/api/v1/users", userRoutesV1);
app.use("/api/v2/users", userRoutesV2);
This allows different clients to use different API versions while the backend evolves.
API versioning is only one strategy, but routing provides a natural place to express these boundaries.
This becomes particularly useful when an API has mobile clients that cannot all be updated at the same time.
24. Routing at Scale
A small application might have:
Client
↓
One Backend
↓
Database
A larger system might have:
Load Balancer
↓
┌──────────┼──────────┐
↓ ↓ ↓
API 1 API 2 API 3
↓ ↓ ↓
Services Services Services
↓ ↓ ↓
Databases / Caches / Queues
At this point, routing can happen at multiple levels.
For example, an API gateway might route:
/api/users
↓
User Service
/api/orders
↓
Order Service
/api/payments
↓
Payment Service
So routing isn't limited to a single Express router.
The same fundamental idea appears throughout distributed systems:
Look at the request and determine where it should go.
25. Routing Inside a Microservices Architecture
In a monolithic backend, routing might look like:
Client
↓
Backend Application
↓
Router
↓
User / Order / Product Code
With microservices, the architecture can look different:
Client
↓
API Gateway
↓
┌───────────────┐
↓ ↓ ↓
User Order Payment
Service Service Service
Now the first routing decision might happen at the API gateway.
The gateway sees:
/api/users
and sends it to the User Service.
For:
/api/orders
it sends the request to the Order Service.
The Order Service might then perform additional internal routing or service-to-service communication.
The core idea hasn't changed.
The system still needs to answer:
Where should this request go?
Only the scale and number of routing layers have changed.
26. Routing Can Also Help With Traffic Control
Routing isn't always just about finding a piece of code.
At infrastructure level, traffic can be routed based on different rules.
For example:
Incoming Traffic
↓
Load Balancer
↓
┌─────┼─────┐
↓ ↓ ↓
Server Server Server
Traffic might be distributed across multiple backend instances.
At a larger level, requests might be routed based on:
- hostname
- URL path
- service
- region
- deployment version
- availability
This is where routing starts becoming a system-design concept rather than just a framework feature.
27. Routing and Security
Routing also creates important security boundaries.
For example:
/api/public/*
might be publicly accessible.
While:
/api/admin/*
might require authentication and administrator permissions.
You can structure middleware accordingly:
/api/public
↓
Public Routes
/api/users
↓
Authentication
↓
User Routes
/api/admin
↓
Authentication
↓
Authorization
↓
Admin Routes
The route structure can therefore make security requirements easier to understand.
However, simply hiding or naming a route as /admin does not provide security by itself.
The backend still needs to actually enforce authentication and authorization.
28. One Request, End to End
Let's put everything together.
A user clicks:
"View Product"
The browser sends:
GET /api/products/42
The backend might process it like this:
HTTP Request
|
↓
Load Balancer
|
↓
Backend
|
↓
Middleware
|
↓
Router
|
GET /products/:id
|
↓
Controller
|
↓
Service
|
┌───────┴───────┐
↓ ↓
Redis Database
| |
└───────┬───────┘
↓
Response
|
↓
Client
What looked like:
GET /api/products/42
was actually the entry point into an entire processing pipeline.
The router was responsible for finding the correct path through that system.
29. What Happens When You Type a URL?
Suppose you visit:
https://example.com/api/products/42
A simplified backend journey is:
Browser
↓
HTTP Request
↓
Load Balancer
↓
Backend
↓
Router
↓
GET /api/products/:id
↓
Middleware
↓
Controller
↓
Service
↓
Database
↓
Response
↓
Browser
The router is the component that recognizes:
GET /api/products/42
as belonging to:
GET /api/products/:id
and sends it to the correct handler.
That's the core of backend routing.
30. The Bigger Picture
When you first learn Express or another backend framework, routing can look like a few simple lines:
app.get("/users", getUsers);
app.post("/users", createUser);
app.delete("/users/:id", deleteUser);
But those lines are actually defining the public interface of your application.
They tell clients:
Which resources exist?
Which operations are supported?
Which URLs represent those resources?
Which HTTP methods should be used?
As the application grows, these decisions become part of API design.
Poorly designed routes can make an API confusing.
Well-designed routes make it easier for developers to understand how the system works without reading the backend implementation.
A Simple Mental Model
Whenever you see:
GET /api/products/42
think:
HTTP Request
↓
Method + Path
↓
Router
↓
Find Matching Route
↓
Middleware
↓
Controller
↓
Business Logic
↓
Database / Cache / Services
↓
HTTP Response
Routing is essentially the traffic director of your backend.
It doesn't necessarily perform the actual business operation.
Instead, it determines where the request needs to go.
That sounds simple, but as an application grows, good routing becomes increasingly important.
Clear routes make APIs easier to understand.
Well-organized route modules make codebases easier to maintain.
Middleware keeps cross-cutting concerns separate.
Controllers and services prevent route handlers from becoming massive.
And at the system-design level, routing allows traffic to be directed between different services and infrastructure components.
The next time you write:
app.get("/api/users/:id", getUser);
don't think of it as just one line of framework syntax.
You're defining a rule:
When this kind of request arrives, this is where the application should send it.
That's what backend routing really does.
It is the bridge between an external HTTP request and the internal logic of your application.
And once you understand that bridge, designing APIs and understanding backend architecture becomes much easier.
Top comments (0)