Master the Express request and response objects — the foundation of every route handler.
Every Express route handler receives two arguments: req (the request) and res (the response). The request object contains all the information about what the client is asking for. Mastering its properties is essential because you will use them in every single route you write.
Here are the most important properties of req:
req.method — The HTTP method of the request as an uppercase string: "GET", "POST", "PUT", "PATCH", "DELETE". You rarely check this directly in Express because you already specify the method when defining the route (app.get, app.post, etc.), but it is useful in middleware that needs to handle all methods.
req.url — The full URL path including query string. For a request to /api/users?page=2, req.url is "/api/users?page=2". In Express, you typically use req.path (without query string) or the more specific req.params and req.query instead.
req.params — An object containing route parameters. For a route defined as /users/:id and a request to /users/42, req.params is { id: "42" }. Values are always strings.
req.query — An object containing parsed query string parameters. For a request to /search?q=node&page=3, req.query is { q: "node", page: "3" }. Values are always strings.
req.body — The parsed body of the request. Only available for POST, PUT, and PATCH requests, and only if you have body-parsing middleware like express.json() enabled. For a POST request with JSON body {"name": "Alice"}, req.body is { name: "Alice" }.
req.headers — An object containing all HTTP headers sent by the client. Header names are automatically lowercased by Node.js. Common headers include req.headers['content-type'] (format of the body), req.headers['authorization'] (credentials like JWT tokens), and req.headers['user-agent'] (what browser or tool sent the request).
req.cookies — An object containing cookies sent by the client. Only available if you use the cookie-parser middleware. Cookies are small pieces of data that the browser stores and sends with every request to your server.
Think of the request object as a complete dossier on what the client wants. By reading its properties, you can determine exactly what to do and how to respond.
The response object is how you send data back to the client. While the request tells you what the client wants, the response is your answer. Express adds several powerful convenience methods to the standard Node.js response object.
Here are the most important methods of res:
res.json(data) — Sends a JSON response. This is the most common method for API development. It automatically sets the Content-Type header to application/json, converts the JavaScript object to a JSON string, and sends it. Example: res.json({ name: "Alice", age: 30 }) sends {"name":"Alice","age":30}.
res.send(data) — Sends a response of any type. If you pass a string, it sets Content-Type to text/html. If you pass an object, it behaves like res.json(). It is more general than res.json(), but for API routes, prefer res.json() for clarity.
res.status(code) — Sets the HTTP status code. This method is chainable, meaning you can follow it with another method: res.status(201).json({ created: true }). Common status codes:
res.redirect(url) — Redirects the client to a different URL. Example: res.redirect('/login') sends a 302 redirect. You can specify the status code: res.redirect(301, '/new-url') for a permanent redirect.
res.set(header, value) — Sets a response header. Example: res.set('X-Custom-Header', 'my-value'). You can also pass an object: res.set({ 'X-Request-Id': '12345', 'Cache-Control': 'no-cache' }).
res.sendStatus(code) — Sets the status code AND sends the status text as the body. res.sendStatus(404) is equivalent to res.status(404).send('Not Found').
One critical rule: you can only send one response per request. If you call res.json() twice, the second call will throw an error. Always use return before sending error responses in conditional logic to prevent your code from continuing to execute and trying to send a second response.
When a client sends a POST, PUT, or PATCH request, it usually includes data in the request body. For example, when a user fills out a registration form and clicks submit, the browser sends the form data in the body of a POST request. When an API client creates a new product, it sends the product details in the body.
Here is the important part: Express does not parse request bodies automatically. If you try to access req.body without any body-parsing middleware, it will be undefined. You need to tell Express how to parse incoming data.
For JSON data (the most common format for APIs):
app.use(express.json());This middleware intercepts every incoming request, checks if the Content-Type header is application/json, and if so, reads the raw body data, parses it with JSON.parse(), and assigns the result to req.body. After this, you can do:
app.post('/api/users', (req, res) => {
console.log(req.body); // { name: 'Alice', email: 'alice@example.com' }
const { name, email } = req.body;
// ... create user in database
});For form data (submitted by HTML forms with application/x-www-form-urlencoded):
app.use(express.urlencoded({ extended: true }));The extended: true option allows for rich objects and arrays to be encoded in the URL-encoded format using the qs library. With extended: false, it uses the simpler querystring library.
For file uploads, you need a separate middleware like multer, because files are sent as multipart/form-data which neither express.json() nor express.urlencoded() can handle.
It is common to add both JSON and URL-encoded parsers at the top of your app:
const app = express();
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse form bodiesBody size limits: By default, express.json() limits body size to 100kb. For most APIs, this is plenty. If you need to accept larger payloads, you can configure it: express.json({ limit: '10mb' }). Be careful with large limits — they can expose your server to denial-of-service attacks where malicious clients send enormous payloads to exhaust your server's memory.
const express = require('express');
const app = express();
app.use(express.json());
// In-memory users array (simulates a database)
let users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' },
];
// GET /api/users — List users with optional filtering
// Demonstrates: req.query
app.get('/api/users', (req, res) => {
let result = users;
// Filter by role: GET /api/users?role=admin
if (req.query.role) {
result = result.filter(u => u.role === req.query.role);
}
res.json({ success: true, data: result, count: result.length });
});
// GET /api/users/:id — Get a single user
// Demonstrates: req.params, status codes, error handling
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
// Validate the param is a valid number
if (isNaN(id)) {
return res.status(400).json({ success: false, error: 'Invalid user ID' });
}
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ success: false, error: 'User not found' });
}
res.json({ success: true, data: user });
});
// POST /api/users — Create a new user
// Demonstrates: req.body, 201 status, validation
app.post('/api/users', (req, res) => {
const { name, role } = req.body;
// Validate required fields
if (!name) {
return res.status(400).json({ success: false, error: 'Name is required' });
}
const newUser = {
id: users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1,
name,
role: role || 'user', // Default role
};
users.push(newUser);
// 201 Created — resource was successfully created
res.status(201).json({ success: true, data: newUser });
});
app.listen(3000);Following consistent patterns in your API responses makes your code predictable, debuggable, and easy for frontend developers to consume. Here are the most important best practices:
1. Always set appropriate status codes. Do not return 200 for everything. If a resource was created, use 201. If something was deleted, use 204. If the client sent bad data, use 400. If they are not authenticated, use 401. Status codes are how HTTP communicates outcomes — use them correctly.
// Bad: 200 for everything
res.json({ error: 'Not found' }); // Client thinks this succeeded!
// Good: appropriate status codes
res.status(404).json({ error: 'Not found' });2. Use a consistent response shape. Pick a format and stick with it across your entire API. A common pattern is:
// Success response
{ success: true, data: { ... } }
// Error response
{ success: false, error: "Description of what went wrong" }This way, the frontend can always check response.success to know if the request worked, and always look at response.data for results or response.error for the error message.
3. Always return JSON for API routes. APIs should consistently return JSON. Do not return plain text for some routes and JSON for others. Consistency makes the API easier to consume.
4. Always return a response — never leave requests hanging. Every code path must end with a response. If your handler has conditional logic, make sure every branch sends something back:
app.get('/api/items/:id', (req, res) => {
const item = findItem(req.params.id);
if (!item) {
return res.status(404).json({ success: false, error: 'Not found' });
}
// Without the return above, this would also execute after sending 404!
res.json({ success: true, data: item });
});Notice the return before the 404 response. Without it, the code continues to the next res.json() call, which throws an error because you cannot send two responses.
5. Include useful metadata. For list endpoints, include the total count, pagination info, and any applied filters:
res.json({
success: true,
data: users,
meta: { total: 150, page: 2, limit: 10, pages: 15 }
});This gives the frontend everything it needs to build pagination UI without making additional requests.
What method sends a JSON response in Express?