Request and Response Objects in Express.js

Understanding the core objects used to handle HTTP communication

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.

flowchart LR A[Client] -->|HTTP Request| B[Express Server] B -->|HTTP Response| A subgraph Server C[Route Handler] D[Request Object] E[Response Object] D -.-> C C -.-> E end

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.

flowchart TB A[Request Object] --> B[req.params] A --> C[req.query] A --> D[req.body] A --> E[req.headers] A --> F[req.cookies] A --> G[req.path] A --> H[req.protocol] A --> I[req.hostname] A --> J[req.ip] A --> K[req.method] A --> L[req.originalUrl] A --> M[req.route]

Key Request Properties

Property Description Example
req.params Contains route parameters /users/:idreq.params.id
req.query Contains query string parameters /search?q=expressreq.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.

flowchart TB A[Response Object] --> B[res.status()] A --> C[res.send()] A --> D[res.json()] A --> E[res.render()] A --> F[res.redirect()] A --> G[res.sendFile()] A --> H[res.set()] A --> I[res.type()] A --> J[res.cookie()] A --> K[res.clearCookie()] A --> L[res.end()] A --> M[res.download()]

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 connections
  • sameSite: '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 cookie
  • path - Limits the cookie to a specific path on the server
  • maxAge or expires - 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

  1. Start your server:
    node app.js
  2. 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/products in your browser
  3. 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

  1. Add Pagination: Implement pagination for the product list with page and limit query parameters
  2. Add Filtering: Allow filtering products by category, price range, or availability
  3. Add PDF Format: Add support for PDF format using a library like PDFKit
  4. Add Authentication: Implement basic authentication for write operations
  5. Add Rate Limiting: Add rate limiting to protect the API from abuse

Summary

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.