Handling Requests and Responses
Handling Requests and Responses
Every interaction between a client and your Express server follows the same pattern: the client sends an HTTP request, and your server sends back an HTTP response. Understanding the full range of options available on the request and response objects is fundamental to building well-designed APIs.
Reading Request Data
Express exposes all parts of an incoming request through the req object.
app.post('/orders', (req, res) => {
// Request body (JSON) — requires express.json() middleware
const { product, quantity, address } = req.body;
// Route parameters — defined with :param in the route
const orderId = req.params.id;
// Query string — ?sort=price&page=2
const { sort, page = 1, limit = 10 } = req.query;
// Request headers
const contentType = req.headers['content-type'];
const authToken = req.headers.authorization;
// IP address of the client
const clientIp = req.ip;
console.log({ product, quantity, sort, page, authToken });
res.status(201).json({ message: 'Order created' });
});
Validating Request Data
Never trust client-supplied data. Always validate before using it:
app.post('/users', (req, res) => {
const { name, email, age } = req.body;
// Basic validation
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Name is required and must be a string' });
}
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'A valid email address is required' });
}
if (age !== undefined && (typeof age !== 'number' || age < 0 || age > 120)) {
return res.status(400).json({ error: 'Age must be a number between 0 and 120' });
}
// Proceed with creating the user
const user = { id: Date.now(), name, email, age };
res.status(201).json({ user });
});
Sending Responses
Express provides multiple methods on the res object:
// JSON — the standard for REST APIs
res.json({ success: true, data: user });
// With a status code (chain it)
res.status(201).json({ user });
res.status(400).json({ error: 'Bad request' });
res.status(404).json({ error: 'Not found' });
// Plain text
res.send('Hello World');
// Set a specific status and end (no body)
res.sendStatus(204); // 204 No Content — common for DELETE endpoints
// Redirect
res.redirect(301, '/new-url');
res.redirect('/dashboard'); // Defaults to 302
// Download a file
res.download('/path/to/file.pdf', 'report.pdf');
// Send an HTML file
res.sendFile('/path/to/index.html');
Setting Response Headers
app.get('/download', (req, res) => {
// Set a single header
res.set('Content-Type', 'application/json');
// Set multiple headers at once
res.set({
'X-Powered-By': 'MyAPI',
'Cache-Control': 'no-store',
'X-Request-Id': 'abc-123'
});
res.json({ data: 'some data' });
});
Consistent API Response Structure
A well-designed API always returns a consistent response shape. Here is a pattern used by many professional teams:
// helpers/response.js
function successResponse(res, data, statusCode = 200) {
return res.status(statusCode).json({
success: true,
data,
timestamp: new Date().toISOString()
});
}
function errorResponse(res, message, statusCode = 500) {
return res.status(statusCode).json({
success: false,
error: message,
timestamp: new Date().toISOString()
});
}
module.exports = { successResponse, errorResponse };
// Using the helpers
const { successResponse, errorResponse } = require('./helpers/response');
app.get('/users/:id', (req, res) => {
const user = { id: req.params.id, name: 'Amara Okafor' };
if (!user) {
return errorResponse(res, 'User not found', 404);
}
return successResponse(res, user);
});
Key Takeaways
req.bodyholds the parsed request body;req.paramsholds route parameters;req.queryholds query string values.- Always validate user-supplied data before using it — never assume the client sends correct data.
- Use
res.status().json()to send JSON responses with the correct HTTP status code. res.sendStatus(204)is the correct response for DELETE endpoints that return no body.- A consistent response structure (success, data, timestamp) makes your API predictable and easier to consume.
Practice Exercise
- Build a
POST /registerroute that accepts{ name, email, password }and validates:- name must be at least 2 characters
- email must contain @ and .
- password must be at least 8 characters
- Return appropriate 400 errors for each validation failure.
- Create a helper file
helpers/response.jswithsuccessResponseanderrorResponsefunctions. - Refactor your existing routes to use those helpers.
- Add a
DELETE /users/:idroute that returns 204 No Content on success.
Try it yourself
Key Takeaways
- The req object exposes body, params, query, headers, and ip — each sourced from a different part of the HTTP request.
- Always validate backend input independently of any frontend validation because API clients can bypass UIs entirely.
- Chain res.status() with .json() to send structured responses with the correct HTTP status code.
- A consistent API response shape (success, data, error, timestamp) makes your API predictable and easy to integrate.
- res.sendStatus(204) is the idiomatic response for successful deletions that return no body.
Quick Quiz
1.Which property of the req object holds the parsed JSON body of a POST request?
2.What HTTP status code is conventionally returned by a DELETE endpoint that succeeds but sends no response body?
3.Why is input validation important on the backend even if the frontend already validates inputs?
4.How do you chain a status code and a JSON response in Express?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx