How Does Routing Work in a Backend?
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": "a***@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("/", getP
Comments
No comments yet. Start the discussion.