πΊοΈ Basic Routing and Request Handling
Routing is how Express decides which code runs for which URL. In this lesson you'll wire up the four core HTTP methods, capture values from the URL, read query strings, and send back exactly the response β and status code β the client expects.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Define routes for the GET, POST, PUT, and DELETE methods
- Capture URL segments with route parameters (
req.params) - Read optional data from the query string (
req.query) - Parse a JSON request body with
express.json() - Send responses with the correct status codes using
res.status(),res.json(), andres.send()
Estimated Time: 35β45 minutes β’ Difficulty: Beginner
Hands-on: Build a small in-memory "tasks" API that reads and writes data over four HTTP methods.
In This Lesson
What Is a Route?
A route is a rule that pairs an HTTP method and a URL path with a handler function. When a request arrives, Express finds the first route whose method and path match and runs its handler. Every route follows the same shape:
app.METHOD(PATH, HANDLER);
appβ your Express application objectMETHODβ an HTTP method in lowercase:get,post,put,deletePATHβ the URL path to match, like/or/users/:idHANDLERβ(req, res) => { ... }, the function that answers the request
π‘ A useful analogy: Routing is a mail sorting room. Each letter (request) carries an address (path) and a type of delivery (method). The sorter reads both and drops the letter into exactly the right clerk's tray (handler). Get the address or delivery type wrong and it lands in the "return to sender" bin β a 404.
π Key Terms
req (request): an object describing the incoming request β its params, query, body, and headers.
res (response): the object you use to send data back β res.json(), res.status(), res.send().
Endpoint: a specific method + path combination, e.g. GET /tasks.
HTTP Methods as Routes
The same path can behave differently depending on the method. By convention, the four core methods map to the four things you do with data β often called CRUD (Create, Read, Update, Delete).
| Method | Intent | Example endpoint | CRUD |
|---|---|---|---|
| GET | Retrieve data | GET /tasks | Read |
| POST | Create a new resource | POST /tasks | Create |
| PUT | Replace/update a resource | PUT /tasks/:id | Update |
| DELETE | Remove a resource | DELETE /tasks/:id | Delete |
// The same path, four different behaviors by method
app.get('/tasks', (req, res) => {
res.send('List all tasks');
});
app.post('/tasks', (req, res) => {
res.send('Create a new task');
});
app.put('/tasks/:id', (req, res) => {
res.send(`Replace task ${req.params.id}`);
});
app.delete('/tasks/:id', (req, res) => {
res.send(`Delete task ${req.params.id}`);
});
π‘ Use nouns, not verbs
The method already says what you're doing, so the path should name the thing: prefer POST /tasks over POST /createTask. This is the heart of RESTful design.
Route Parameters
A path segment starting with a colon is a route parameter β a placeholder that captures whatever appears in that position. Express puts captured values on req.params.
// :id captures the segment after /tasks/
app.get('/tasks/:id', (req, res) => {
res.send(`You asked for task ${req.params.id}`);
});
// GET /tasks/42 -> "You asked for task 42"
// Multiple parameters
app.get('/users/:userId/tasks/:taskId', (req, res) => {
const { userId, taskId } = req.params;
res.send(`Task ${taskId} belonging to user ${userId}`);
});
:id placeholder captures whatever sits in that URL slot and delivers it as req.params.id.β οΈ Params are always strings
req.params.id is the string "42", not the number 42. Convert it before doing math or comparisons: Number(req.params.id) or parseInt(req.params.id, 10). Forgetting this is a classic first-week bug.
Query Parameters
Everything after the ? in a URL is the query string. Unlike route params (which identify a resource), query params carry options like filtering, sorting, and pagination. Express parses them onto req.query.
// URL: /tasks?status=done&limit=5
app.get('/tasks', (req, res) => {
const { status, limit } = req.query;
res.json({
status: status || 'all',
limit: Number(limit) || 10
});
});
// -> { "status": "done", "limit": 5 }
| Use case | Example URL |
|---|---|
| Filtering | /tasks?status=done |
| Pagination | /tasks?page=2&limit=20 |
| Sorting | /tasks?sortBy=due&order=asc |
| Searching | /tasks?q=report |
π‘ Params vs. query: The route parameter is like the street address on an envelope β it decides where the letter goes. The query string is like handling notes scrawled on the outside β "fragile", "deliver by noon" β extra instructions that don't change the destination.
Reading the Request Body
GET requests carry data in the URL, but POST and PUT usually send it in the request body as JSON. Express doesn't parse bodies by default β you must enable the built-in express.json() middleware once, near the top of your app. Then the parsed object appears on req.body.
const express = require('express');
const app = express();
// Enable JSON body parsing for every route (do this once, early)
app.use(express.json());
app.post('/tasks', (req, res) => {
// req.body is now the parsed JSON object
const { title } = req.body;
res.status(201).json({ id: 1, title });
});
β οΈ Forgot express.json()?
Without it, req.body is undefined and destructuring it throws. If your POST handler crashes reading req.body, this missing one-liner is almost always why. For HTML form submissions (URL-encoded), add app.use(express.urlencoded({ extended: true })) too.
Shaping Responses
The res object is how you reply. Pick the method that fits the payload, and always send an appropriate HTTP status code so clients know what happened.
res.send('Plain text or HTML'); // auto-detects content type
res.json({ message: 'Structured' }); // sends JSON + correct header
res.status(201).json({ id: 1 }); // set a status, then send
res.status(404).json({ error: 'Not found' });
res.sendStatus(204); // status code with no body
| Status | Meaning | When to use |
|---|---|---|
| 200 OK | Success | A GET, PUT, or DELETE that worked |
| 201 Created | Resource created | A successful POST |
| 400 Bad Request | Client error | Missing or invalid input |
| 404 Not Found | No such resource | The requested ID doesn't exist |
| 500 Server Error | Something broke | An unexpected exception |
β Status codes are part of your API
A well-behaved API returns 201 after creating, 404 for a missing item, and 400 for bad input β not 200 for everything. Clients rely on the code, not just the body, to react correctly.
Worked Example: A Tasks API
Let's tie every concept together into a small, complete API that stores tasks in memory (an array β no database yet). It uses all four methods, route params, the request body, and proper status codes.
const express = require('express');
const app = express();
app.use(express.json());
// In-memory "database"
let tasks = [
{ id: 1, title: 'Learn routing', done: false },
{ id: 2, title: 'Build an API', done: false }
];
let nextId = 3;
// READ all (with optional ?done=true filter)
app.get('/tasks', (req, res) => {
let result = tasks;
if (req.query.done !== undefined) {
const wantDone = req.query.done === 'true';
result = tasks.filter(t => t.done === wantDone);
}
res.json(result);
});
// READ one
app.get('/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === Number(req.params.id));
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(task);
});
// CREATE
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'title is required' });
}
const task = { id: nextId++, title, done: false };
tasks.push(task);
res.status(201).json(task);
});
// UPDATE
app.put('/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === Number(req.params.id));
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
if (req.body.title !== undefined) task.title = req.body.title;
if (req.body.done !== undefined) task.done = req.body.done;
res.json(task);
});
// DELETE
app.delete('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const exists = tasks.some(t => t.id === id);
if (!exists) {
return res.status(404).json({ error: 'Task not found' });
}
tasks = tasks.filter(t => t.id !== id);
res.sendStatus(204); // success, no body
});
app.listen(3000, () => console.log('Tasks API on http://localhost:3000'));
Try it (using curl):
$ curl http://localhost:3000/tasks
[{"id":1,"title":"Learn routing","done":false}, ...]
$ curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Write tests"}'
{"id":3,"title":"Write tests","done":false}
Notice the pattern in every handler: find or validate, return an error status early if something's wrong, otherwise do the work and respond. That "guard clause first" shape keeps handlers flat and readable.
Hands-on Exercise
ποΈ Build a Books API
Objective: Reproduce the CRUD pattern on your own with a new resource.
Instructions:
- Start from a fresh Express app with
app.use(express.json())and an in-memorybooksarray. Each book hasid,title, andauthor. GET /booksβ return all books. Support an optional?author=query filter.GET /books/:idβ return one book, or404if the id doesn't exist. Remember to convert the id to a number.POST /booksβ create a book; respond400iftitleis missing, otherwise201with the new book.DELETE /books/:idβ remove a book and respond204, or404if it's not there.
π‘ Hint
Reuse the exact shape from the Tasks API. For the filter, check if (req.query.author) and use books.filter(b => b.author === req.query.author). Always guard for "not found" with an early return res.status(404)... before doing the real work.
β Sample solution
const express = require('express');
const app = express();
app.use(express.json());
let books = [
{ id: 1, title: 'The Hobbit', author: 'Tolkien' },
{ id: 2, title: 'Dune', author: 'Herbert' }
];
let nextId = 3;
app.get('/books', (req, res) => {
let result = books;
if (req.query.author) {
result = books.filter(b => b.author === req.query.author);
}
res.json(result);
});
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: 'Book not found' });
res.json(book);
});
app.post('/books', (req, res) => {
const { title, author } = req.body;
if (!title) return res.status(400).json({ error: 'title is required' });
const book = { id: nextId++, title, author };
books.push(book);
res.status(201).json(book);
});
app.delete('/books/:id', (req, res) => {
const id = Number(req.params.id);
if (!books.some(b => b.id === id)) {
return res.status(404).json({ error: 'Book not found' });
}
books = books.filter(b => b.id !== id);
res.sendStatus(204);
});
app.listen(3000, () => console.log('Books API on http://localhost:3000'));
Quiz
π― Check Your Understanding
Question 1: A request comes in as GET /tasks/7. Inside a handler registered for /tasks/:id, what is req.params.id?
Question 2: Your POST /tasks handler throws when reading req.body.title, and req.body is undefined. What's the most likely fix?
Question 3: Which status code should a successful POST /tasks that creates a new task return?
Summary
π Key Takeaways
- A route is
app.METHOD(PATH, HANDLER)β matching method + path to a function. - GET/POST/PUT/DELETE map to the CRUD operations; name paths with nouns, not verbs.
- Route params (
req.params) identify a resource and are always strings; query params (req.query) carry options. - Enable
express.json()once so POST/PUT bodies land onreq.body. - Respond with the right method and status code:
201on create,404when missing,400for bad input.
π Further Reading
π What's Next?
You've been quietly using middleware already (express.json()). Next, Middleware Architecture and Flow unpacks how the middleware pipeline really works β the key to logging, authentication, and clean error handling.
π You built an API!
Routing and request handling are the backbone of every backend. Onward to middleware.