Part 5: Error Handling and Logging in Express APIs

Ram Kumar

Ram Kumar

October 11, 20245 min read

Part 5: Error Handling and Logging in Express APIs

Welcome back to the fifth installment of our Security Practices for Express APIs series! So far, we've covered HTTPS encryption, Authentication and Authorization, Rate Limiting, and Input Validation and Sanitization. In this post, we’ll focus on Error Handling and Logging, two essential practices that help you monitor, diagnose, and respond to issues in your API.

Error handling ensures your API can gracefully recover from unexpected situations without crashing or leaking sensitive information. Logging, on the other hand, helps track errors and monitor the overall health of your API, providing valuable insights for debugging and security audits.

Let's explore how to handle errors effectively in Express APIs and integrate logging using console.log, Winston, and Morgan.

Why Error Handling and Logging Matter

Error Handling: Without proper error handling, your API might expose stack traces, sensitive internal details, or return improper status codes, which can confuse clients and create security vulnerabilities.

Logging: Logs provide insight into what’s happening within your application. Effective logging allows you to monitor API usage, trace errors, and detect abnormal behavior, helping you maintain application health and security.

1. Error Handling in Express

Error handling in Express can be achieved using middleware. This special error-handling middleware catches errors that occur in your routes and sends proper responses without exposing sensitive details.

Example: Error Handling Middleware in TypeScript

import express, { Request, Response, NextFunction } from 'express';

const app = express();
app.use(express.json());

// Example route that might throw an error
app.get('/error-prone-route', (req: Request, res: Response, next: NextFunction) => {
  try {
    // Simulate an error
    throw new Error('Something went wrong!');
  } catch (error) {
    next(error);  // Forward error to the error-handling middleware
  }
});

// Error-handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(`Error: ${err.message}`);  // Basic logging of error
  res.status(500).json({ message: 'Internal Server Error' });  // Respond with a generic message
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

In this example:

  • We simulate an error in a route and forward it using next(error) to the error-handling middleware.
  • The middleware logs the error to the console and returns a generic response to the client (without leaking sensitive details).

2. Logging with console.log

While basic logging using console.log is often used during development, it's not sufficient for production. Here's an example of how you might log important events with console.log:

app.get('/user/:id', (req: Request, res: Response) => {
  const userId = req.params.id;
  console.log(`Fetching user with ID: ${userId}`);
  
  // Simulate fetching a user
  res.json({ id: userId, name: 'John Doe' });
});

This will output logs like:

Fetching user with ID: 123

Although this works fine for development or quick debugging, it doesn’t provide log levels, timestamps, or proper storage of logs for later analysis, which brings us to more advanced logging libraries.

3. Logging with Winston

Winston is a versatile logging library for Node.js that allows you to log messages at various levels (error, info, debug, etc.), format them, and output logs to different destinations (console, file, etc.).

Example: Integrating Winston for Structured Logging

import express, { Request, Response, NextFunction } from 'express';
import winston from 'winston';

const app = express();
app.use(express.json());

// Configure Winston logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.printf(({ timestamp, level, message }) => `${timestamp} [${level.toUpperCase()}]: ${message}`)
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/app.log' })  // Log to file
  ]
});

// Example route with Winston logging
app.get('/order/:id', (req: Request, res: Response) => {
  const orderId = req.params.id;
  logger.info(`Fetching order with ID: ${orderId}`);
  
  // Simulate fetching an order
  res.json({ id: orderId, status: 'shipped' });
});

// Error-handling middleware with Winston
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  logger.error(`Error occurred: ${err.message}`);
  res.status(500).json({ message: 'Internal Server Error' });
});

app.listen(3000, () => {
  logger.info('Server is running on port 3000');
});

Here, we configure Winston to:

  • Log both to the console and to a file (logs/app.log).
  • Include timestamps and custom log formats.
  • Use logger.info() for normal logging and logger.error() for error logging in the error-handling middleware.

Winston is powerful because you can easily adjust the log level or transport (e.g., log to a database or external service in the future).

4. Logging Requests with Morgan

Morgan is a popular HTTP request logger middleware for Express. It logs information about each incoming request, such as the HTTP method, URL, status code, response time, and more. This is useful for monitoring API usage and detecting abnormal traffic patterns.

Example: Integrating Morgan with Winston

import express from 'express';
import winston from 'winston';
import morgan from 'morgan';

const app = express();
app.use(express.json());

// Configure Winston logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/access.log' })  // Log HTTP requests
  ]
});

// Use Morgan to log incoming HTTP requests and pipe them through Winston
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));

// Sample route
app.get('/profile', (req, res) => {
  res.json({ username: 'John Doe', email: 'john@example.com' });
});

app.listen(3000, () => {
  logger.info('Server is running on port 3000');
});

In this setup:

  • Morgan logs HTTP requests (in the "combined" format) and passes the log messages to Winston.
  • Winston writes the logs to both the console and an access.log file.

Morgan automatically logs every incoming HTTP request with details like:

127.0.0.1 - - [10/Oct/2024:12:10:15 +0000] "GET /profile HTTP/1.1" 200 - "-" "-"

This is particularly helpful in understanding API usage patterns and quickly diagnosing issues related to incoming traffic.

Best Practices for Error Handling and Logging

Always Catch and Handle Errors: Use error-handling middleware to catch errors and respond to clients without exposing sensitive details.

Log at Different Levels: Use different log levels (info, warn, error, debug, etc.) for different kinds of messages. Not every message should be logged as an error.

Don't Log Sensitive Data: Be mindful not to log sensitive user data, such as passwords or credit card information, to avoid compromising user privacy.

Centralized Log Management: If your app is deployed across multiple servers, consider using centralized log management tools like ELK Stack, Datadog, or Papertrail to aggregate logs from different sources.

Rotate Logs: Logs can grow large quickly, so implement log rotation to prevent disk space exhaustion. Winston’s File transport supports this feature out of the box.

Conclusion

Error Handling and Logging are essential components of a secure and reliable Express API. By properly handling errors and logging important events, you ensure that your application remains stable, secure, and easy to maintain.

In this post, we discussed:

  • How to handle errors gracefully using Express error-handling middleware.
  • Logging with basic console.log, as well as using more advanced tools like Winston for structured logging and Morgan for HTTP request logging.

By implementing robust logging and error handling, you can quickly diagnose problems, monitor API performance, and ensure that errors don't expose sensitive details to users or attackers.

What's Next?

In the final post of this series, we'll explore Security Headers and CORS (Cross-Origin Resource Sharing), two key practices that can further harden your Express API against common web vulnerabilities. Stay tuned for Part 6!

Previous: Part 4: Input Validation and Sanitization in Express APIs
Next: Part 6: Security Headers and CORS (Cross-Origin Resource Sharing) in Express APIs