Routing by Hand
Routing is the logic that decides which response to send based on the request's URL path and HTTP method — and before frameworks like Express, you write it by hand with conditionals on req.url and req.method.
Learn Routing by Hand in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end of this lesson you'll route requests with plain if and switch statements, parse paths and query strings safely with the URL class, and build a small method-aware route table that returns JSON with proper status codes like 404 and 405.
What You'll Learn in This Lesson
1️⃣ Routing with if on req.url and req.method
A Node server's request handler runs for every incoming request, no matter the URL. Routing is simply the code inside that handler that inspects the request and decides what to send back. The two properties you check are req.url (the path, like /about ) and req.method (the verb, like GET or POST ).
The pattern is: check a condition, respond, and return so no other branch runs. The crucial last branch is the 404 fallback — without it, unmatched requests hang forever because you never called res.end() . Read every line below, then run it.
2️⃣ Parsing the URL and Query Strings
There's a trap in Section 1: req.url includes the query string . A request to /greet?name=Sam has a req.url of "/greet?name=Sam" — so a plain req.url === '/greet' check silently fails. The fix is the built-in URL class.
Build a URL with new URL(req.url, 'http://localhost') (the second argument is a required base, since req.url is relative). Then url.pathname gives you the path with no query, and url.searchParams.get('name') reads query values safely.
3️⃣ Method-Aware Routing and a Route Table
Real APIs serve the same path differently depending on the method: GET /users lists users, while POST /users creates one. So you branch first on the path, then on req.method inside it. Status codes matter here: send 200 for a successful read, 201 Created after a POST, 404 when the path doesn't exist, and 405 Method Not Allowed when the path exists but the verb is wrong.
To return JSON, set 'Content-Type': 'application/json' and pass your object through JSON.stringify(...) . A small sendJson helper keeps every branch tidy and consistent.
Your turn. The handler below works once you fill in the blanks marked ___ . Follow the 👉 hints, run it with node server.js , and compare with the expected output.
Mini-Challenge: A Tiny Todo API
No blanks this time — just a brief and an outline. Build the route table yourself, run it, and check your output against the example in the comments. This is exactly the kind of hands-on routing that makes Express click later.
Common Errors (and the fix)
- Forgetting the 404 fallback — If no branch matches and you never call res.end() , the request hangs forever (the browser spins, curl never returns). Always finish with a catch-all that writes a 404 and ends the response.
- Comparing req.url with the query string attached — req.url is "/greet?name=Sam" , not "/greet" , so an exact match fails. Parse it with new URL(req.url, 'http://localhost') and compare url.pathname .
- Not checking req.method — Matching only on the path means a POST hits your GET logic. Check both, and return 405 (with an Allow header) when the path matches but the verb doesn't.
- Trailing-slash mismatches — /about and /about/ are different strings, so one slips through to the 404. Normalise the path (trim a trailing slash) or match both forms.
- Case sensitivity — /About won't match '/about' . Pick a convention; if you want case-insensitive paths, lowercase url.pathname before comparing.
Pro Tips
- 💡 Always return after responding: once you've called res.end() , returning prevents a later branch from trying to write to an already-finished response.
- 💡 Normalise once, compare many: compute const path = url.pathname at the top of the handler so every route checks the same clean value.
- 💡 This is what Express does: app.get('/about', ...) is the same method-plus-path check you just wrote — frameworks save typing, not understanding.
📋 Quick Reference — Manual Routing
Concept
Node Syntax
Request path (+ query)
req.url
HTTP method/verb
req.method
Parse the URL
new URL(req.url, base)
Path without query
url.pathname
Read a query param
url.searchParams.get(k)
Send JSON
JSON.stringify({...})
Not found
res.writeHead(404)
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ Routing decides the response from req.url and req.method
- ✅ Always end with a 404 fallback so requests never hang
- ✅ Parse with new URL(req.url, 'http://localhost') and compare url.pathname
- ✅ Read query values with url.searchParams.get('key')
- ✅ Branch on req.method and return 405 for wrong verbs
- ✅ Send JSON with 'Content-Type': 'application/json' and JSON.stringify
- ✅ Next lesson: Async Patterns — callbacks, promises, and async/await
Practice quiz
What two request properties do you inspect to route by hand?
- req.body and req.headers
- req.url and req.method
- req.host and req.port
- req.query and req.cookies
Answer: req.url and req.method. req.url gives the path and req.method gives the HTTP verb; together they decide the response.
Why must a manual router end with a 404 fallback?
- To log requests
- To set headers
- Otherwise unmatched requests hang because res.end is never called
- To enable HTTPS
Answer: Otherwise unmatched requests hang because res.end is never called. Without a catch-all that ends the response, an unmatched request never finishes and hangs.
Why is comparing req.url directly to '/greet' fragile?
- req.url is always uppercase
- req.url includes the query string, e.g. '/greet?name=Sam'
- req.url is a number
- req.url is undefined for GET
Answer: req.url includes the query string, e.g. '/greet?name=Sam'. req.url contains path plus query string, so an exact match fails when a query is present.
How do you parse the path out of req.url safely?
- ?
Building a URL with a base lets you read url.pathname without the query string.
How do you read a query parameter like ?name=Sam?
- url.searchParams.get('name')
- req.params.name
- url.query.name
- req.body.name
Answer: url.searchParams.get('name'). url.searchParams.get('name') reads query values from the parsed URL.
What status code means the path exists but the HTTP verb is not allowed?
- 404 Not Found
- 405 Method Not Allowed
- 200 OK
- 500 Server Error
Answer: 405 Method Not Allowed. 405 Method Not Allowed signals the path matched but the method is unsupported; pair it with an Allow header.
When returning JSON manually, what must you do to the object?
- Pass it directly to res.end
- Set status 201 only
- Pass it through JSON.stringify and set Content-Type application/json
- Encrypt it
Answer: Pass it through JSON.stringify and set Content-Type application/json. res.end sends strings, so stringify the object and set the JSON Content-Type.
Why might a route for '/about' fail to match '/about/'?
- Trailing slashes are reserved
- /about/ is a query string
- String comparison is exact, so the two differ
- Node lowercases paths
Answer: String comparison is exact, so the two differ. '/about' and '/about/' are different strings, so the trailing slash slips past an exact check.
What does Express's app.get('/about', ...) do under the hood?
- The same method-plus-path check you write by hand
- Opens a database connection
- Caches the response
- Encrypts the route
Answer: The same method-plus-path check you write by hand. Frameworks are sugar over the same req.method and req.url checks plus a 404 fallback.
Why should you return after calling res.end() in a branch?
- To restart the server
- To free memory
- To prevent a later branch writing to an already-finished response
- To log the request
Answer: To prevent a later branch writing to an already-finished response. Returning stops later code from trying to respond again on a finished response.