Routing
What is routing?
Section titled “What is routing?”Routing is the process of deciding which piece of code handles a given HTTP request. The decision is based on two properties of the request:
req.method— the HTTP verb:GET,POST,PUT,DELETE, etc.req.url— the path (and optional query string):/users,/users/42,/search?q=node
Node’s built-in server gives you both. You write the logic that maps them to handlers.
Manual routing with if/else
Section titled “Manual routing with if/else”The simplest router is a chain of if statements:
Needs the Node.js runtime — open in StackBlitz to run.
Parsing the URL with the URL class
Section titled “Parsing the URL with the URL class”When URLs contain path parameters or query strings, use Node’s global URL constructor to parse them cleanly. Pass req.url as the first argument and a fake base as the second:
Needs the Node.js runtime — open in StackBlitz to run.
Matching paths with URLPattern
Section titled “Matching paths with URLPattern”Hand-written startsWith checks break as soon as a route has parameters. Node 24 makes the web-standard URLPattern a global, so you can declare routes like /users/:id and read the captured groups. (The legacy url.parse() is runtime-deprecated since Node 24 — use URL and URLPattern instead.)
const userRoute = new URLPattern({ pathname: '/users/:id' });
const match = userRoute.exec('http://localhost:3000/users/42');console.log(match.pathname.groups.id); // '42'
console.log(userRoute.test('http://localhost:3000/posts/1')); // falseURLPattern in a real router, with 404 vs. 405
Section titled “URLPattern in a real router, with 404 vs. 405”A route can exist for a path while not supporting the method a client used — that is a different situation from the path not existing at all, and it deserves a different status code: 404 means “no such path,” 405 Method Not Allowed means “this path exists, but not for that verb” (and should include an Allow header listing what is supported):
Output (verified on Node 24)
GET /users/42: { status: 200, body: { id: '42' } }
DELETE /users/42: { status: 405, allow: 'GET' }
GET /unknown: { status: 404 }/users/42 matches the pattern for both GET and DELETE — the difference is only in the second lookup, route.methods[req.method]. That is what makes 405 possible here: the code already knows the path matched by the time it discovers the method did not.
A tiny router object
Section titled “A tiny router object”Once you have more than a few routes, extract them into a router map. This is exactly the pattern that Express uses internally:
Needs the Node.js runtime — open in StackBlitz to run.
Mental Model
Section titled “Mental Model”A router is a lookup table with two keys, checked in a specific order: first the path (does anything match this URL shape at all?), then the method (does the thing that matched support this verb?). Checking both at once with a single combined key — 'GET /users' as one string — collapses that two-step decision into one, which is exactly why the tiny router object above cannot tell “no such path” apart from “wrong verb for this path”: both fall into the same else branch.
Trade-offs
Section titled “Trade-offs”A single combined-key lookup (method + ' ' + path) is the least code and reads great for a handful of fixed routes — until you need parameters (/users/:id) or a 405 response, at which point you need to match path and method as two separate steps, which is more code than the map. URLPattern gives you real parameter capture and the two-step structure for free, but it is a newer global (Node 24+) — a route table like the one in this lesson is trivially portable to any Node version or to a framework’s router, while URLPattern code is not yet something you can assume every runtime supports.
Common Misconceptions
Section titled “Common Misconceptions”- “A 404 is the correct response whenever a route lookup fails.” Only when the path itself has no match. If the path matches but the method is unsupported, the correct code is 405, with an
Allowheader — 404 tells a client “stop trying this URL,” while 405 tells it “try a different verb here.” - “URLPattern parameters are validated automatically.” They are captured as strings, nothing more —
/users/:idmatches/users/abcjust as happily as/users/42. Validating that a captured group looks like an actual ID is still the handler’s job. - “A router built by hand is inherently fragile compared to a framework’s.” The underlying decision (match path, then match method) is identical in both — a framework mainly adds convenience (middleware, parameter coercion, wildcard/catch-all routes) on top of the same two-step lookup.
Reading AI-generated code
Section titled “Reading AI-generated code”AI-generated routers frequently key routes by method + path as a single string, which reads clean for the happy path and silently produces the wrong status code the moment a path is hit with an unsupported method.
🐛 Spot the bug
const routes = {'GET /users': listUsers,'POST /users': createUser,};
const server = http.createServer(function (req, res) {const key = req.method + ' ' + new URL(req.url, 'http://x').pathname;const handler = routes[key];if (handler) { handler(req, res);} else { res.writeHead(404); res.end('Not found');}});// A DELETE request to /users returns 404, as if the route didn't exist at allShow the answer
/users is a real resource — GET and POST are both defined for it — but DELETE /users is not in the map, so it falls into the same else branch as a request to a path that was never defined at all, like /completely-unknown. Both get a 404, which tells the client the path itself is wrong; the client has no way to know the path was actually fine and only the verb was the problem. The fix is to look up by path first, then check whether the matched route supports the request’s method — returning 405 with an Allow header when it does not, exactly like the URLPattern example earlier in this lesson.