Introduction to Request and Response Objects
In Express.js, the request (req) and response (res) objects are the two primary objects that you'll work with in every route handler. These objects represent the HTTP request made by the client and the HTTP response that your Express app sends back.
What are Request and Response Objects?
The request object (req) contains information about the HTTP request such as the URL, HTTP headers, query parameters, URL parameters, body data, and more.
The response object (res) represents the HTTP response that an Express app sends when it receives an HTTP request. It provides methods to set response status, headers, and body content.
The Mail Delivery Analogy
Think of Express.js handling HTTP communication like a mail delivery system:
- The client is the person sending a letter (HTTP request)
- The Express server is the post office
- The request object (req) is the envelope and its contents, containing:
- The address (URL and parameters)
- The sender's information (headers)
- The letter inside (request body)
- The route handler is the postal worker who processes the mail
- The response object (res) is the reply letter, containing:
- A status code (like "Delivered" or "Address Unknown")
- Headers (like "Handle with Care" or "Confidential")
- The actual response content (the letter itself)
The Request Object (req)
The request object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and more.
Key Request Properties
| Property | Description | Example |
|---|---|---|
req.params |
Contains route parameters | /users/:id → req.params.id |
req.query |
Contains query string parameters | /search?q=express → req.query.q |
req.body |
Contains data submitted in the request body | Form data or JSON data sent in POST requests |
req.headers |
Contains HTTP headers | req.headers['user-agent'] |
req.cookies |
Contains cookies sent by the client (requires cookie-parser) | req.cookies.sessionId |
req.path |
Contains the path part of the URL | /users/profile |
req.protocol |
Contains the request protocol | 'http' or 'https' |
req.hostname |
Contains the hostname from the Host HTTP header | 'example.com' |
req.ip |
Contains the remote IP address | '192.168.1.1' |
req.method |
Contains the HTTP method | 'GET', 'POST', etc. |
req.originalUrl |
Contains the original request URL | '/users/123?sort=asc' |
req.baseUrl |
Contains the URL path on which a router instance was mounted | For router mounted on '/api', req.baseUrl would be '/api' |
Key Request Methods
| Method | Description | Example |
|---|---|---|
req.get() |
Returns the specified HTTP request header | req.get('Content-Type') |
req.accepts() |
Checks if the specified content types are acceptable | req.accepts('html') |
req.is() |
Checks if the request's Content-Type matches the specified type | req.is('application/json') |
req.param() (deprecated) |
Returns the value of the specified parameter | req.param('id') |
Working with Route Parameters
Route parameters are named URL segments used to capture values specified at their position in the URL. The captured values are stored in the req.params object.
// Route with parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
// Access route parameters
const userId = req.params.userId;
const bookId = req.params.bookId;
res.send(`User ID: ${userId}, Book ID: ${bookId}`);
});
// Example: /users/34/books/8989
// req.params: { "userId": "34", "bookId": "8989" }
Working with Query Parameters
Query parameters are specified in the URL after a question mark (?) and include key-value pairs. They are parsed and populated in the req.query object.
// Route handling query parameters
app.get('/products', (req, res) => {
// Access query parameters
const { category, minPrice, maxPrice, sort } = req.query;
// Log parameters for demonstration
console.log('Category:', category);
console.log('Min Price:', minPrice);
console.log('Max Price:', maxPrice);
console.log('Sort:', sort);
// Use parameters to filter products (example implementation)
let products = getAllProducts();
if (category) {
products = products.filter(p => p.category === category);
}
if (minPrice) {
products = products.filter(p => p.price >= parseFloat(minPrice));
}
if (maxPrice) {
products = products.filter(p => p.price <= parseFloat(maxPrice));
}
res.json(products);
});
// Example: /products?category=electronics&minPrice=10&maxPrice=100
// req.query: { "category": "electronics", "minPrice": "10", "maxPrice": "100" }
Working with Query Parameters Best Practices
- Always validate and sanitize query parameters as they are user input
- Convert string values to the appropriate type (e.g., numbers, booleans)
- Provide default values for optional parameters
- Use clear parameter naming conventions
- Document expected parameters in your API documentation
// Example of proper query parameter handling
app.get('/api/search', (req, res) => {
// Extract and validate parameters with defaults
const query = (req.query.q || '').trim();
const page = Math.max(1, parseInt(req.query.page || '1', 10));
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit || '10', 10)));
const sortBy = ['title', 'date', 'relevance'].includes(req.query.sort)
? req.query.sort
: 'relevance';
// Validate search query
if (query.length < 2) {
return res.status(400).json({
error: 'Search query must be at least 2 characters long'
});
}
// Execute search with validated parameters
const results = performSearch(query, page, limit, sortBy);
res.json(results);
});
Working with Request Body
The request body contains data submitted in the request, such as form data or JSON data. To access this data, you need to use middleware to parse it.
// Middleware to parse request bodies
app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// Route handling POST request with body
app.post('/users', (req, res) => {
// Access body data
const { name, email, password } = req.body;
// Validate required fields
if (!name || !email || !password) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Process the data (example implementation)
const newUser = createUser({ name, email, password });
res.status(201).json({
message: 'User created successfully',
user: newUser
});
});
// Example request with JSON body:
// POST /users
// Content-Type: application/json
//
// {
// "name": "John Doe",
// "email": "john@example.com",
// "password": "secretpassword"
// }
//
// req.body: { "name": "John Doe", "email": "john@example.com", "password": "secretpassword" }
Working with HTTP Headers
HTTP headers provide additional information about the request. You can access these headers using the req.headers object or the req.get() method.
// Route that uses request headers
app.get('/api/data', (req, res) => {
// Access headers
const contentType = req.get('Content-Type');
const userAgent = req.get('User-Agent');
const authorization = req.get('Authorization');
console.log('Content Type:', contentType);
console.log('User Agent:', userAgent);
// Check for authentication
if (!authorization) {
return res.status(401).json({ error: 'Authorization required' });
}
// Process request with headers
// ...
res.json({ message: 'Request processed successfully' });
});
Reading Cookies from Request
Cookies are sent by the client in the Cookie HTTP header. To easily access cookies, use the cookie-parser middleware:
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
// Use cookie-parser middleware
app.use(cookieParser());
// Route that reads cookies
app.get('/profile', (req, res) => {
// Access cookies
const sessionId = req.cookies.sessionId;
const theme = req.cookies.theme || 'light';
if (!sessionId) {
return res.redirect('/login');
}
// Get user from session
const user = getUserFromSession(sessionId);
if (!user) {
// Clear invalid session cookie
res.clearCookie('sessionId');
return res.redirect('/login');
}
res.render('profile', { user, theme });
});
File Uploads with the Request Object
To handle file uploads, you can use the multer middleware, which adds a req.file or req.files object to the request:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configure multer storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname));
}
});
// Create upload middleware
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: function (req, file, cb) {
// Accept images only
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
}
});
// Route for single file upload
app.post('/upload/profile', upload.single('avatar'), (req, res) => {
// req.file contains information about the uploaded file
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
message: 'File uploaded successfully',
file: {
filename: req.file.filename,
mimetype: req.file.mimetype,
size: req.file.size,
path: req.file.path
}
});
});
// Route for multiple file uploads
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
// req.files contains an array of file information
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
res.json({
message: `${req.files.length} files uploaded successfully`,
files: req.files.map(file => ({
filename: file.filename,
mimetype: file.mimetype,
size: file.size,
path: file.path
}))
});
});
The Response Object (res)
The response object represents the HTTP response that an Express app sends when it receives an HTTP request. It provides methods to set status codes, headers, and send response data.
Key Response Methods
| Method | Description | Example |
|---|---|---|
res.send() |
Sends a response with various types of data | res.send('Hello World'); |
res.json() |
Sends a JSON response | res.json({ name: 'John' }); |
res.status() |
Sets the HTTP status code | res.status(404); |
res.sendFile() |
Sends a file as the response | res.sendFile('/path/to/file.pdf'); |
res.download() |
Sends a file as an attachment (prompted for download) | res.download('/path/to/file.pdf'); |
res.render() |
Renders a view template | res.render('index', { title: 'Home' }); |
res.redirect() |
Redirects to the specified path or URL | res.redirect('/login'); |
res.set() |
Sets response headers | res.set('Content-Type', 'text/plain'); |
res.type() |
Sets the Content-Type header | res.type('application/pdf'); |
res.cookie() |
Sets a cookie | res.cookie('name', 'value'); |
res.clearCookie() |
Clears a cookie | res.clearCookie('name'); |
res.end() |
Ends the response process | res.end(); |
Sending Basic Responses
The res.send() method is a versatile way to send different types of responses:
// Sending a string response
app.get('/text', (req, res) => {
res.send('Hello World');
});
// Sending HTML
app.get('/html', (req, res) => {
res.send('<h1>Hello World</h1><p>Welcome to Express!</p>');
});
// Sending a Buffer
app.get('/buffer', (req, res) => {
const buffer = Buffer.from('Hello World');
res.send(buffer);
});
// Sending an array or object (automatically converted to JSON)
app.get('/array', (req, res) => {
res.send([1, 2, 3]);
});
app.get('/object', (req, res) => {
res.send({ name: 'John', age: 30 });
});
Sending JSON Responses
When building APIs, you'll frequently send JSON responses using res.json():
// Sending a JSON response
app.get('/api/user', (req, res) => {
const user = {
id: 123,
name: 'John Doe',
email: 'john@example.com',
roles: ['user', 'admin'],
createdAt: new Date(),
settings: {
theme: 'dark',
notifications: true
}
};
res.json(user);
});
// Sending an empty response with just a status code
app.delete('/api/user/:id', (req, res) => {
// Delete user logic...
// Send a 204 No Content response
res.status(204).json();
// or simply: res.status(204).end();
});
Setting Status Codes
Use res.status() to set the appropriate HTTP status code:
// Success with data (200 OK is default)
app.get('/api/products', (req, res) => {
const products = getProducts();
res.json(products);
});
// Created successfully (201 Created)
app.post('/api/products', (req, res) => {
const newProduct = createProduct(req.body);
res.status(201).json(newProduct);
});
// Bad request (400 Bad Request)
app.post('/api/register', (req, res) => {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
// Register user logic...
res.status(201).json({ message: 'User registered successfully' });
});
// Not found (404 Not Found)
app.get('/api/products/:id', (req, res) => {
const product = getProductById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
});
// Server error (500 Internal Server Error)
app.get('/api/data', (req, res) => {
try {
const data = fetchData();
res.json(data);
} catch (error) {
console.error('Error fetching data:', error);
res.status(500).json({ error: 'Failed to fetch data' });
}
});
Common HTTP Status Codes
| Code | Message | Description |
|---|---|---|
| 200 | OK | The request succeeded |
| 201 | Created | The request succeeded and a new resource was created |
| 204 | No Content | The request succeeded but returns no content |
| 400 | Bad Request | The server cannot process the request due to client error |
| 401 | Unauthorized | Authentication is required and has failed or not been provided |
| 403 | Forbidden | The client does not have access rights to the content |
| 404 | Not Found | The server cannot find the requested resource |
| 409 | Conflict | The request conflicts with the current state of the server |
| 422 | Unprocessable Entity | The request was well-formed but could not be processed |
| 500 | Internal Server Error | The server encountered an unexpected condition |
| 503 | Service Unavailable | The server is not ready to handle the request |
Setting Response Headers
Use res.set() or res.header() to set custom headers:
// Setting a single header
app.get('/api/data', (req, res) => {
// Set a custom header
res.set('X-Custom-Header', 'Hello World');
// Set Content-Type
res.set('Content-Type', 'application/json');
// Send response
res.send({ message: 'Hello World' });
});
// Setting multiple headers at once
app.get('/api/data', (req, res) => {
// Set multiple headers
res.set({
'Content-Type': 'application/json',
'X-API-Version': '1.0.0',
'Cache-Control': 'no-cache',
'X-Response-Time': process.hrtime()[1] / 1000000
});
res.send({ message: 'Hello World' });
});
Redirecting Responses
Use res.redirect() to redirect the client to a different URL:
// Redirect to a relative path (same domain)
app.get('/old-page', (req, res) => {
res.redirect('/new-page');
});
// Redirect to a different domain
app.get('/external', (req, res) => {
res.redirect('https://example.com');
});
// Redirect with a status code (default is 302 Found)
app.get('/permanent-redirect', (req, res) => {
// 301 Moved Permanently
res.redirect(301, '/new-location');
});
// Redirect based on conditions
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
// Redirect to login if not authenticated
return res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl));
}
// Continue to dashboard if authenticated
res.render('dashboard', { user: req.session.user });
});
Sending Files
Use res.sendFile() to send a file:
const path = require('path');
// Send a file
app.get('/file', (req, res) => {
// Using absolute path
const filePath = path.join(__dirname, 'public', 'files', 'document.pdf');
res.sendFile(filePath, (err) => {
if (err) {
// Handle errors
console.error('Error sending file:', err);
res.status(err.status).end();
} else {
console.log('File sent successfully');
}
});
});
// Send a file with options
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'public', 'files', 'document.pdf');
const options = {
headers: {
'Content-Disposition': 'attachment; filename="report.pdf"',
'Content-Type': 'application/pdf'
}
};
res.sendFile(filePath, options);
});
Alternatively, use res.download() to prompt a file download:
// Prompt a file download
app.get('/download/:filename', (req, res) => {
const fileName = req.params.filename;
const filePath = path.join(__dirname, 'downloads', fileName);
res.download(filePath, fileName, (err) => {
if (err) {
// Check if file doesn't exist
if (err.code === 'ENOENT') {
return res.status(404).send('File not found');
}
// Handle other errors
console.error('Error downloading file:', err);
res.status(500).send('Error downloading file');
}
});
});
Working with Cookies
Use res.cookie() to set cookies and res.clearCookie() to clear them:
// Set a basic cookie
app.get('/set-theme', (req, res) => {
const theme = req.query.theme || 'light';
// Set a cookie that expires in 30 days
res.cookie('theme', theme, { maxAge: 30 * 24 * 60 * 60 * 1000 });
res.redirect('/');
});
// Set a secure cookie with various options
app.post('/login', (req, res) => {
// Authenticate user...
// Set a session cookie with options
res.cookie('sessionId', 'abc123', {
maxAge: 24 * 60 * 60 * 1000, // 1 day
httpOnly: true, // Not accessible via JavaScript
secure: true, // Only sent over HTTPS
sameSite: 'strict', // Strict same-site policy
signed: true // Sign the cookie if using cookie-parser with secret
});
res.redirect('/dashboard');
});
// Clear a cookie
app.get('/logout', (req, res) => {
// Clear session cookie
res.clearCookie('sessionId');
res.redirect('/login');
});
Secure Cookie Options
For secure applications, consider these cookie options:
httpOnly: true- Prevents client-side JavaScript from accessing the cookie (mitigates XSS)secure: true- Only sends the cookie over HTTPS connectionssameSite: 'strict'or'lax'- Controls when cookies are sent with cross-site requests (mitigates CSRF)signed: true- Signs the cookie to detect tampering (requires cookie-parser with a secret)domain- Specifies which domains can receive the cookiepath- Limits the cookie to a specific path on the servermaxAgeorexpires- Sets when the cookie expires
Rendering Views
Use res.render() to render view templates with a template engine:
// Setup view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Render a view
app.get('/', (req, res) => {
res.render('home', {
title: 'Home Page',
user: req.session.user,
posts: getLatestPosts(),
currentYear: new Date().getFullYear()
});
});
// Render with locals and callback
app.get('/profile/:id', (req, res) => {
const userId = req.params.id;
getUserById(userId, (err, user) => {
if (err) {
return res.status(500).render('error', { message: 'Server error' });
}
if (!user) {
return res.status(404).render('error', { message: 'User not found' });
}
res.render('profile', { user }, (err, html) => {
if (err) {
console.error('Error rendering view:', err);
return res.status(500).send('Error rendering page');
}
// Do something with the rendered HTML
// For example, add custom headers
res.set('Content-Security-Policy', "default-src 'self'");
res.send(html);
});
});
});
Real-World Example: API Response Pattern
Here's a practical example of a consistent API response pattern:
// Helper function for consistent API responses
function apiResponse(res, data = null, error = null, statusCode = 200) {
const response = {
success: error === null,
timestamp: new Date().toISOString(),
data: data,
error: error
};
return res.status(statusCode).json(response);
}
// Example usage in routes
app.get('/api/users', async (req, res) => {
try {
const users = await User.find();
return apiResponse(res, users);
} catch (error) {
console.error('Error fetching users:', error);
return apiResponse(res, null, 'Failed to fetch users', 500);
}
});
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return apiResponse(res, null, 'User not found', 404);
}
return apiResponse(res, user);
} catch (error) {
console.error(`Error fetching user ${req.params.id}:`, error);
return apiResponse(res, null, 'Failed to fetch user', 500);
}
});
app.post('/api/users', async (req, res) => {
try {
const { name, email, password } = req.body;
// Validate input
if (!name || !email || !password) {
return apiResponse(res, null, 'Missing required fields', 400);
}
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return apiResponse(res, null, 'Email already registered', 409);
}
// Create new user
const newUser = await User.create({ name, email, password });
return apiResponse(res, newUser, null, 201);
} catch (error) {
console.error('Error creating user:', error);
return apiResponse(res, null, 'Failed to create user', 500);
}
});
The resulting responses would have a consistent format:
// Success response
{
"success": true,
"timestamp": "2025-05-04T10:23:45.678Z",
"data": { /* data object */ },
"error": null
}
// Error response
{
"success": false,
"timestamp": "2025-05-04T10:23:45.678Z",
"data": null,
"error": "User not found"
}
Advanced Request and Response Techniques
Streaming Responses
For large files or real-time data, you can use streams to send data incrementally:
const fs = require('fs');
const path = require('path');
// Stream a large file
app.get('/stream/video', (req, res) => {
const videoPath = path.join(__dirname, 'videos', 'sample.mp4');
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
// Handle range request (partial content)
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = (end - start) + 1;
const file = fs.createReadStream(videoPath, { start, end });
const headers = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': 'video/mp4'
};
res.writeHead(206, headers);
file.pipe(res);
} else {
// Send entire file
const headers = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4'
};
res.writeHead(200, headers);
fs.createReadStream(videoPath).pipe(res);
}
});
// Stream real-time data
app.get('/stream/events', (req, res) => {
// Set headers for SSE (Server-Sent Events)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send initial data
res.write(`data: ${JSON.stringify({ message: 'Connection established' })}\n\n`);
// Send event every second
const intervalId = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
value: Math.random() * 100
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// Clean up on client disconnect
req.on('close', () => {
clearInterval(intervalId);
res.end();
console.log('Client disconnected');
});
});
Handling Content Negotiation
Content negotiation allows clients to request data in their preferred format:
// Handle content negotiation with Accept header
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
const user = getUserById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Determine response format based on Accept header
const acceptHeader = req.get('Accept');
if (acceptHeader.includes('application/xml')) {
// Convert data to XML and send
const xml = convertToXml(user);
res.type('application/xml').send(xml);
} else if (acceptHeader.includes('text/csv')) {
// Convert data to CSV and send
const csv = convertToCsv(user);
res.type('text/csv').send(csv);
} else {
// Default to JSON
res.json(user);
}
});
// Using res.format() for content negotiation
app.get('/api/products/:id', (req, res) => {
const productId = req.params.id;
const product = getProductById(productId);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.format({
'application/json': () => {
res.json(product);
},
'application/xml': () => {
const xml = convertToXml(product);
res.type('application/xml').send(xml);
},
'text/html': () => {
res.render('product', { product });
},
default: () => {
// default to json
res.json(product);
}
});
});
Handling CORS
Cross-Origin Resource Sharing (CORS) allows browsers to make requests to your API from different domains:
// Manual CORS handling
app.use((req, res, next) => {
// Allow requests from any origin
res.header('Access-Control-Allow-Origin', '*');
// Allow specific HTTP methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// Allow specific headers
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// Using cors middleware
const cors = require('cors');
// Enable CORS for all routes
app.use(cors());
// Or with specific options
app.use(cors({
origin: ['https://example.com', 'https://subdomain.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
// CORS for specific routes
app.get('/api/public-data', cors(), (req, res) => {
// This route is accessible from any origin
res.json({ message: 'This is public data' });
});
// CORS with different configuration for specific routes
const corsOptions = {
origin: 'https://trusted-partners.com',
methods: 'GET,POST'
};
app.get('/api/partner-data', cors(corsOptions), (req, res) => {
// This route is only accessible from the specified origin
res.json({ message: 'Partner data' });
});
Compression
Reduce response size using compression middleware:
const compression = require('compression');
// Enable compression for all responses
app.use(compression());
// Or with specific options
app.use(compression({
level: 6, // Compression level (0-9)
threshold: 1024, // Only compress responses larger than 1KB
filter: (req, res) => {
// Don't compress responses with this header
if (req.headers['x-no-compression']) {
return false;
}
// Fallback to standard filter function
return compression.filter(req, res);
}
}));
Request Timeout Handling
Set timeouts to handle long-running requests:
// Set timeout for all requests
app.use((req, res, next) => {
// Set timeout to 30 seconds
req.setTimeout(30000, () => {
res.status(408).json({ error: 'Request timeout' });
});
next();
});
// Handle long-running requests
app.get('/api/long-process', async (req, res) => {
// Use a promise with timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Process timeout'));
}, 10000); // 10 seconds
});
try {
// Race between the actual process and the timeout
const result = await Promise.race([
longRunningProcess(), // Your actual process
timeoutPromise
]);
res.json(result);
} catch (error) {
if (error.message === 'Process timeout') {
res.status(408).json({ error: 'Process timed out' });
} else {
res.status(500).json({ error: 'Process failed' });
}
}
});
Best Practices
Request Object Best Practices
- Validate Input: Always validate and sanitize user input from req.body, req.params, and req.query
- Type Conversion: Convert string values to appropriate types (numbers, booleans, dates)
- Default Values: Provide default values for optional parameters
- Security Checks: Verify authorization and authentication before processing requests
- Logging: Log request information for debugging and monitoring
- Custom Properties: Use req object to pass data between middleware (e.g., req.user)
Response Object Best Practices
- Appropriate Status Codes: Use the correct HTTP status code for each response
- Consistent Format: Maintain a consistent response format for your API
- Error Handling: Provide meaningful error messages with appropriate status codes
- Security Headers: Set security headers for all responses
- Compression: Use compression for large responses
- Caching Headers: Set appropriate cache control headers
- Performance: Use streaming for large files
Security Considerations
// Use helmet middleware for security headers
const helmet = require('helmet');
app.use(helmet());
// Set custom security headers
app.use((req, res, next) => {
// Content Security Policy
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' https://trusted-cdn.com; img-src 'self' data: https:;"
);
// Strict Transport Security
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// XSS Protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent MIME-type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Referrer Policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Frame Options
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
});
// Use rate limiting to prevent abuse
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false // Disable the `X-RateLimit-*` headers
});
// Apply to all requests to /api/ routes
app.use('/api/', apiLimiter);
// Stricter limits for authentication endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour window
max: 5, // 5 attempts per hour
message: 'Too many login attempts, please try again after an hour'
});
app.use('/api/auth/login', authLimiter);
Performance Considerations
// Use compression for text-based responses
const compression = require('compression');
app.use(compression());
// Set appropriate cache headers for static content
const express = require('express');
const path = require('path');
// Cache static assets for 1 week
app.use('/static', express.static(path.join(__dirname, 'public'), {
maxAge: '7d',
etag: true,
lastModified: true
}));
// Non-cached static route for frequently changing files
app.use('/dynamic', express.static(path.join(__dirname, 'dynamic'), {
maxAge: 0,
etag: true,
lastModified: true
}));
// Set cache headers for API responses
app.get('/api/data', (req, res) => {
const data = fetchData();
// Set cache control header for 5 minutes
res.set('Cache-Control', 'public, max-age=300');
// Set ETag for validation
const etag = generateETag(data);
res.set('ETag', etag);
// Check If-None-Match header
const ifNoneMatch = req.get('If-None-Match');
if (ifNoneMatch === etag) {
return res.status(304).end(); // Not Modified
}
res.json(data);
});
Practical Exercise
Exercise: Build a RESTful API with Content Negotiation
Create an API that supports multiple response formats (JSON, XML, HTML) based on client preferences.
Step 1: Setup Project
mkdir content-negotiation-api
cd content-negotiation-api
npm init -y
npm install express morgan xml2js ejs
Step 2: Create the Main Application File
Create a file named app.js:
const express = require('express');
const morgan = require('morgan');
const path = require('path');
const { Builder } = require('xml2js');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Setup view engine for HTML responses
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Sample data
const products = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999.99, inStock: true },
{ id: 2, name: 'Headphones', category: 'Electronics', price: 149.99, inStock: true },
{ id: 3, name: 'Coffee Maker', category: 'Appliances', price: 79.99, inStock: false },
{ id: 4, name: 'Running Shoes', category: 'Sports', price: 89.99, inStock: true },
{ id: 5, name: 'Yoga Mat', category: 'Sports', price: 29.99, inStock: true }
];
// Helper function to find product by ID
function getProductById(id) {
return products.find(product => product.id === parseInt(id));
}
// Helper function to convert data to XML
function toXml(data) {
const builder = new Builder({
rootName: Array.isArray(data) ? 'products' : 'product',
headless: true,
renderOpts: { pretty: true, indent: ' ' }
});
return builder.buildObject(data);
}
// Helper function to convert data to CSV
function toCsv(data) {
if (!Array.isArray(data)) {
data = [data]; // Convert single item to array
}
if (data.length === 0) {
return '';
}
// Create headers from the keys of the first object
const headers = Object.keys(data[0]);
const headerRow = headers.join(',');
// Create data rows
const rows = data.map(item => {
return headers.map(header => {
const value = item[header];
// Handle different data types
if (typeof value === 'string') {
// Escape quotes and wrap in quotes
return `"${value.replace(/"/g, '""')}"`;
} else if (value === null || value === undefined) {
return '';
} else {
return String(value);
}
}).join(',');
});
// Combine header and rows
return [headerRow, ...rows].join('\n');
}
// Routes
app.get('/', (req, res) => {
res.send('Content Negotiation API - Use /api/products to access the API');
});
// GET all products with content negotiation
app.get('/api/products', (req, res) => {
// Using res.format for content negotiation
res.format({
'application/json': () => {
res.json(products);
},
'application/xml': () => {
const xml = toXml(products);
res.type('application/xml').send(xml);
},
'text/csv': () => {
const csv = toCsv(products);
res.type('text/csv')
.set('Content-Disposition', 'attachment; filename="products.csv"')
.send(csv);
},
'text/html': () => {
res.render('products', { products, title: 'Product List' });
},
default: () => {
// Default to JSON
res.json(products);
}
});
});
// GET a specific product with content negotiation
app.get('/api/products/:id', (req, res) => {
const product = getProductById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.format({
'application/json': () => {
res.json(product);
},
'application/xml': () => {
const xml = toXml(product);
res.type('application/xml').send(xml);
},
'text/csv': () => {
const csv = toCsv(product);
res.type('text/csv')
.set('Content-Disposition', `attachment; filename="product_${product.id}.csv"`)
.send(csv);
},
'text/html': () => {
res.render('product-detail', { product, title: product.name });
},
default: () => {
// Default to JSON
res.json(product);
}
});
});
// POST a new product
app.post('/api/products', (req, res) => {
const { name, category, price, inStock } = req.body;
// Validate required fields
if (!name || !category || price === undefined) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Create new product
const newProduct = {
id: products.length + 1,
name,
category,
price: parseFloat(price),
inStock: inStock === true || inStock === 'true',
};
// Add to collection
products.push(newProduct);
// Respond with created product
res.status(201).json(newProduct);
});
// Error handler for non-existent routes
app.use((req, res, next) => {
res.status(404).json({ error: 'Not found' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3: Create View Templates
Create a views directory and add the following files:
views/products.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
.stock-status {
display: inline-block;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
}
.in-stock {
background-color: #d4edda;
color: #155724;
}
.out-of-stock {
background-color: #f8d7da;
color: #721c24;
}
.actions {
margin-top: 20px;
}
.formats {
margin-top: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>Product List</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Stock Status</th>
</tr>
</thead>
<tbody>
<% products.forEach(product => { %>
<tr>
<td><%= product.id %></td>
<td><a href="/api/products/<%= product.id %>"><%= product.name %></a></td>
<td><%= product.category %></td>
<td> <h1>Product List</h1>lt;%= product.price.toFixed(2) %></td>
<td>
<span class="stock-status <%= product.inStock ? 'in-stock' : 'out-of-stock' %>">
<%= product.inStock ? 'In Stock' : 'Out of Stock' %>
</span>
</td>
</tr>
<% }); %>
</tbody>
</table>
<div class="formats">
<h3>Available Formats:</h3>
<ul>
<li><a href="/api/products" data-format="application/json">JSON</a> (set Accept header to application/json)</li>
<li><a href="/api/products" data-format="application/xml">XML</a> (set Accept header to application/xml)</li>
<li><a href="/api/products" data-format="text/csv">CSV</a> (set Accept header to text/csv)</li>
<li><a href="/api/products" data-format="text/html">HTML</a> (set Accept header to text/html)</li>
</ul>
</div>
</body>
</html>
views/product-detail.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
}
.product-card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
margin-top: 20px;
}
.product-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.product-price {
font-size: 24px;
font-weight: bold;
color: #28a745;
}
.product-details {
margin-bottom: 20px;
}
.product-details p {
margin: 10px 0;
}
.stock-status {
display: inline-block;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
}
.in-stock {
background-color: #d4edda;
color: #155724;
}
.out-of-stock {
background-color: #f8d7da;
color: #721c24;
}
.back-link {
display: inline-block;
margin-top: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.formats {
margin-top: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
}
</style>
</head>
<body>
<a href="/api/products" class="back-link">← Back to Products</a>
<div class="product-card">
<div class="product-header">
<h1><%= product.name %></h1>
<div class="product-price"> <h1>Product List</h1>lt;%= product.price.toFixed(2) %></div>
</div>
<div class="product-details">
<p><strong>ID:</strong> <%= product.id %></p>
<p><strong>Category:</strong> <%= product.category %></p>
<p>
<strong>Stock Status:</strong>
<span class="stock-status <%= product.inStock ? 'in-stock' : 'out-of-stock' %>">
<%= product.inStock ? 'In Stock' : 'Out of Stock' %>
</span>
</p>
</div>
</div>
<div class="formats">
<h3>Available Formats:</h3>
<ul>
<li><a href="/api/products/<%= product.id %>" data-format="application/json">JSON</a> (set Accept header to application/json)</li>
<li><a href="/api/products/<%= product.id %>" data-format="application/xml">XML</a> (set Accept header to application/xml)</li>
<li><a href="/api/products/<%= product.id %>" data-format="text/csv">CSV</a> (set Accept header to text/csv)</li>
<li><a href="/api/products/<%= product.id %>" data-format="text/html">HTML</a> (set Accept header to text/html)</li>
</ul>
</div>
</body>
</html>
Step 4: Test Your API
- Start your server:
node app.js - Test different response formats using a tool like Postman, curl, or a web browser:
- JSON format:
curl -H "Accept: application/json" http://localhost:3000/api/products - XML format:
curl -H "Accept: application/xml" http://localhost:3000/api/products - CSV format:
curl -H "Accept: text/csv" http://localhost:3000/api/products - HTML format: Visit
http://localhost:3000/api/productsin your browser
- JSON format:
- Test creating a new product:
curl -X POST -H "Content-Type: application/json" -d '{ "name": "Bluetooth Speaker", "category": "Electronics", "price": 49.99, "inStock": true }' http://localhost:3000/api/products
Bonus Challenges
- Add Pagination: Implement pagination for the product list with
pageandlimitquery parameters - Add Filtering: Allow filtering products by category, price range, or availability
- Add PDF Format: Add support for PDF format using a library like PDFKit
- Add Authentication: Implement basic authentication for write operations
- Add Rate Limiting: Add rate limiting to protect the API from abuse
Summary
- Request Object (req) contains information about the HTTP request:
req.params: URL parametersreq.query: Query string parametersreq.body: Request body datareq.headers: HTTP headersreq.cookies: Cookies sent by the clientreq.method: HTTP methodreq.path: URL path
- Response Object (res) provides methods to send HTTP responses:
res.send(): Send a responseres.json(): Send a JSON responseres.status(): Set the HTTP status coderes.sendFile(): Send a fileres.download(): Send a file as an attachmentres.redirect(): Redirect to another URLres.render(): Render a view templateres.cookie(): Set a cookie
- Advanced Techniques:
- Streaming responses for large files
- Content negotiation for different response formats
- CORS handling for cross-origin requests
- Compression to reduce response size
- Request timeout handling
- Best Practices:
- Input validation and sanitization
- Consistent response formats
- Proper error handling
- Security headers and practices
- Performance optimization
Further Reading
Next Lesson Preview
In our next session, we'll explore RESTful API design principles. We'll dive into how to structure your API endpoints, use HTTP methods correctly, handle versioning, implement proper error handling, and document your API effectively. You'll learn best practices for building APIs that are intuitive, easy to use, and follow industry standards.