Routing Basics
Routing is how an Express application decides what to do when a request arrives. Each route ties together three things: an HTTP method (GET, POST, and so on), a path (the URL the client asks for), and a handler function that builds the response. When a request comes in, Express walks your routes in the order you defined them and runs the handler for the first one that matches — making routing the central nervous system of every Express app.
The shape of a route
Every route follows the same pattern. You call a method on your app object named after the HTTP verb, pass it a path string, and pass it one or more handler functions:
app.METHOD(path, handler);
A concrete example registers a handler for GET /:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Home page");
});
app.listen(3000);
Here app.get is the method, "/" is the path, and the arrow function is the handler. Express now knows that a GET request for / should respond with the text Home page.
HTTP verb methods
Express provides a routing method for each HTTP verb, named in lowercase to match the verb. The common ones map directly onto the standard CRUD operations of a REST API:
| Method | HTTP verb | Typical use |
|---|---|---|
app.get() | GET | Read/fetch a resource |
app.post() | POST | Create a new resource |
app.put() | PUT | Replace a resource entirely |
app.patch() | PATCH | Update part of a resource |
app.delete() | DELETE | Remove a resource |
app.all() | any | Run for every verb on a path |
A small CRUD-style set of routes for a users resource looks like this:
app.get("/users", (req, res) => {
res.json([{ id: 1, name: "Ada" }]);
});
app.post("/users", (req, res) => {
res.status(201).json({ id: 2, name: "Grace" });
});
app.put("/users/:id", (req, res) => {
res.json({ id: req.params.id, updated: true });
});
app.patch("/users/:id", (req, res) => {
res.json({ id: req.params.id, patched: true });
});
app.delete("/users/:id", (req, res) => {
res.status(204).end();
});
The same path (/users) can carry different behavior under different verbs — GET /users lists users while POST /users creates one. They are two distinct routes.
The handler signature
A route handler receives up to three arguments: req, res, and next.
app.get("/profile", (req, res, next) => {
// req → the incoming request (params, query, body, headers)
// res → the response you send back
// next → hand control to the next matching handler/middleware
res.send("Profile");
});
req and res are the request and response objects. The third argument, next, is a function you call to pass control to the next handler in the chain — this is how middleware works and how a route can defer to whatever comes after it. Most simple handlers ignore next and just send a response.
Handlers can be async, which is essential when you need to await a database call or external service:
app.get("/users/:id", async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: "Not found" });
res.json(user);
} catch (err) {
next(err);
}
});
Note: In Express 5, a rejected promise from an
asynchandler is automatically forwarded to your error-handling middleware, so the explicittry/catchabove is optional. In Express 4 you must catch errors and callnext(err)yourself.
First match wins
Express evaluates routes top to bottom and stops at the first one that matches the method and path. Order therefore matters. In this example the second route can never run, because the first one already matches every GET /search request and sends a response:
app.get("/search", (req, res) => {
res.send("Search A");
});
app.get("/search", (req, res) => {
res.send("Search B"); // unreachable — never sends
});
A request to GET /search produces:
Output:
Search A
This ordering rule is why specific routes should be registered before more general ones, and why a catch-all 404 handler belongs at the very end of your route definitions:
app.get("/users/me", (req, res) => res.send("Current user"));
app.get("/users/:id", (req, res) => res.send(`User ${req.params.id}`));
// Catch-all: only reached if nothing above matched
app.use((req, res) => {
res.status(404).json({ error: "Route not found" });
});
If /users/:id came before /users/me, the literal /me route would be shadowed because :id would match the segment me first.
Tip: A handler that neither sends a response nor calls
next()leaves the request hanging until it times out. Always end each path: send a response, or pass control along withnext().
Best Practices
- Name routes with the lowercase verb method that matches the operation —
GETto read,POSTto create,PUT/PATCHto update,DELETEto remove. - Register specific paths before parameterized or catch-all paths, since the first matching route wins.
- Always terminate a handler by sending a response or calling
next()— never leave a request unresolved. - Use
asynchandlers for I/O, and forward errors withnext(err)(or rely on Express 5’s automatic forwarding). - Set meaningful status codes (
201on create,204on delete,404when missing) rather than relying on the default200. - Add a final catch-all
404handler after all real routes so unmatched requests get a clean response.