Building a RESTful API from Scratch with Node.js (Part 8)
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

Adding Logging to Your Node.js API

This tutorial builds on our paginated API by implementing comprehensive logging. We'll cover structured logging, different log levels, request tracking, error logging, and how to manage logs in different environments.

Prerequisites

  • Completed Parts 1-7 of the tutorial
  • Basic understanding of REST APIs
  • Node.js installed

Project Setup

Install the required dependencies:

npm install winston morgan winston-daily-rotate-file uuid

Updated Project Structure

Add these new files to your project:

books-api/
├── src/
│   ├── config/
│   │   └── logging.js
│   ├── middleware/
│   │   └── logging.js
│   ├── utils/
│   │   └── logger.js
│   └── types/
│       └── logging.js

Logging Configuration

Create src/config/logging.js:

module.exports = {
    // Log levels and their priorities
    LEVELS: {
        error: 0,
        warn: 1,
        info: 2,
        http: 3,
        debug: 4
    },

    // Default logging settings
    DEFAULT_LEVEL: 'info',
    LOG_FILE_PATH: 'logs',
    MAX_FILE_SIZE: '20m',
    MAX_FILES: '14d',

    // Environment-specific settings
    ENVIRONMENTS: {
        development: {
            level: 'debug',
            console: true,
            file: true
        },
        test: {
            level: 'warn',
            console: false,
            file: false
        },
        production: {
            level: 'info',
            console: false,
            file: true
        }
    },

    // Request logging settings
    REQUEST_LOGGING: {
        skip: ['/health', '/metrics'],
        maskFields: ['password', 'token', 'secret'],
        maxBodyLength: 1000
    }
};

Logger Implementation

Create src/utils/logger.js:

const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const { format } = winston;
const config = require('../config/logging');

// Custom format for structured logging
const structuredFormat = format.combine(
    format.timestamp(),
    format.errors({ stack: true }),
    format.metadata(),
    format.json()
);

// Create base logger configuration
const createLogger = () => {
    const env = process.env.NODE_ENV || 'development';
    const envConfig = config.ENVIRONMENTS[env];
    
    const transports = [];
    
    // Add console transport if enabled
    if (envConfig.console) {
        transports.push(new winston.transports.Console({
            format: format.combine(
                format.colorize(),
                format.simple()
            )
        }));
    }
    
    // Add file transport if enabled
    if (envConfig.file) {
        transports.push(new DailyRotateFile({
            dirname: config.LOG_FILE_PATH,
            filename: 'application-%DATE%.log',
            datePattern: 'YYYY-MM-DD',
            maxSize: config.MAX_FILE_SIZE,
            maxFiles: config.MAX_FILES,
            format: structuredFormat
        }));
        
        // Separate error log file
        transports.push(new DailyRotateFile({
            dirname: config.LOG_FILE_PATH,
            filename: 'error-%DATE%.log',
            datePattern: 'YYYY-MM-DD',
            maxSize: config.MAX_FILE_SIZE,
            maxFiles: config.MAX_FILES,
            level: 'error',
            format: structuredFormat
        }));
    }
    
    return winston.createLogger({
        level: envConfig.level,
        levels: config.LEVELS,
        transports
    });
};

const logger = createLogger();

// Add request context
const addRequestContext = (req) => {
    return {
        requestId: req.id,
        method: req.method,
        path: req.path,
        ip: req.ip,
        userAgent: req.get('user-agent')
    };
};

// Mask sensitive data
const maskSensitiveData = (data, fields = config.REQUEST_LOGGING.maskFields) => {
    if (!data) return data;
    
    const masked = { ...data };
    fields.forEach(field => {
        if (masked[field]) {
            masked[field] = '********';
        }
    });
    return masked;
};

module.exports = {
    logger,
    addRequestContext,
    maskSensitiveData
};

Logging Middleware

Create src/middleware/logging.js:

const morgan = require('morgan');
const { v4: uuidv4 } = require('uuid');
const { logger, addRequestContext, maskSensitiveData } = require('../utils/logger');
const config = require('../config/logging');

// Request ID middleware
const requestId = (req, res, next) => {
    req.id = req.get('X-Request-ID') || uuidv4();
    res.set('X-Request-ID', req.id);
    next();
};

// Custom morgan token for request body
morgan.token('body', (req) => {
    if (req.method === 'POST' || req.method === 'PUT') {
        const masked = maskSensitiveData(req.body);
        return JSON.stringify(masked).substring(0, config.REQUEST_LOGGING.maxBodyLength);
    }
    return '';
});

// Custom morgan format
const morganFormat = ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time ms :body';

// HTTP request logging middleware
const httpLogger = morgan(morganFormat, {
    skip: (req) => config.REQUEST_LOGGING.skip.includes(req.path),
    stream: {
        write: (message) => logger.http(message.trim())
    }
});

// Error logging middleware
const errorLogger = (err, req, res, next) => {
    const context = addRequestContext(req);
    
    logger.error('Request error', {
        ...context,
        error: {
            message: err.message,
            stack: err.stack,
            code: err.code
        }
    });
    
    next(err);
};

module.exports = {
    requestId,
    httpLogger,
    errorLogger
};

Update Application Entry Point

Update src/app.js to include logging:

const express = require('express');
const { requestId, httpLogger, errorLogger } = require('./middleware/logging');
const { logger } = require('./utils/logger');

const app = express();

// Add request ID to all requests
app.use(requestId);

// Log all HTTP requests
app.use(httpLogger);

// Existing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/books', require('./routes/books'));

// Error logging
app.use(errorLogger);

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    logger.info(`Server started on port ${PORT}`, {
        port: PORT,
        env: process.env.NODE_ENV,
        node_version: process.version
    });
});

Logging Tests

Create __tests__/integration/logging.test.js:

const request = require('supertest');
const app = require('../../src/server');
const { logger } = require('../../src/utils/logger');

describe('Logging Tests', () => {
    let logSpy;
    
    beforeEach(() => {
        logSpy = jest.spyOn(logger, 'info');
    });
    
    afterEach(() => {
        logSpy.mockRestore();
    });
    
    it('should log HTTP requests', async () => {
        await request(app)
            .get('/api/books')
            .expect(200);
        
        expect(logSpy).toHaveBeenCalled();
    });
    
    it('should include request ID in response headers', async () => {
        const response = await request(app)
            .get('/api/books')
            .expect(200);
        
        expect(response.headers['x-request-id']).toBeDefined();
    });
    
    it('should mask sensitive information', async () => {
        const response = await request(app)
            .post('/api/books')
            .send({
                title: 'Test Book',
                password: 'secret123'
            })
            .expect(201);
        
        const logs = logSpy.mock.calls.flat();
        expect(logs.some(log => log.includes('secret123'))).toBe(false);
    });
    
    it('should log errors with stack traces', async () => {
        const errorSpy = jest.spyOn(logger, 'error');
        
        await request(app)
            .get('/api/books/invalid-id')
            .expect(404);
        
        expect(errorSpy).toHaveBeenCalled();
        const errorLog = errorSpy.mock.calls[0][1];
        expect(errorLog.error.stack).toBeDefined();
        
        errorSpy.mockRestore();
    });
});

Best Practices Implemented

  1. Structured Logging

    • JSON format for machine readability
    • Consistent log levels
    • Request context tracking
    • Error stack traces
  2. Security

    • Sensitive data masking
    • Request ID tracking
    • IP address logging
    • User agent tracking
  3. Performance

    • Log rotation
    • Size limits
    • Skip unnecessary paths
    • Async logging
  4. Maintenance

    • Daily log files
    • Environment-specific config
    • Separate error logs
    • Clean up old logs

Conclusion

You now have a robust logging system that:

  • Tracks all API requests
  • Handles errors properly
  • Maintains security
  • Supports different environments
  • Follows best practices

Remember to:

  • Monitor log storage
  • Review error patterns
  • Update logging rules
  • Analyze log data
  • Maintain log security

Related Posts

API Management Tools: A Comprehensive Overview
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

APIs (Application Programming Interfaces) are the backbone of modern digital applications. They allow different software systems to communicate, exchange data, and collaborate seamlessly. As businesse...

API Security: Best Practices for Protecting Your Application
APIs tutorialcreate an APIAPI development guideTutorial

4 min read

In today’s interconnected digital world, APIs (Application Programming Interfaces) are the backbone of communication between different software applications. From mobile apps to cloud services, APIs e...

API Design Best Practices: Crafting Robust and Scalable APIs
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

In the modern digital ecosystem, APIs (Application Programming Interfaces) serve as the backbone of connectivity. Whether you're building microservices, enabling integrations, or crafting data pipelin...