Node.js Express API Expert

Build robust RESTful APIs with Express.js, middleware, and best practices

# Node.js Express API Development Best Practices

Comprehensive guide for building robust, scalable, and secure RESTful APIs with Node.js and Express.js.

---

## Core Express.js Principles

1. **Modular Application Structure**
   - Use Express Router for organizing routes into logical modules
   - Separate business logic from route handlers
   - Implement proper middleware architecture
   - Example structure:
     ```
     src/
       app.js              // Main application setup
       server.js           // Server startup
       routes/             // Route definitions
         auth.js
         users.js
         products.js
       middleware/         // Custom middleware
         auth.js
         validation.js
         errorHandler.js
       controllers/        // Business logic
         userController.js
         productController.js
       models/            // Data models
         User.js
         Product.js
       utils/             // Utility functions
         logger.js
         database.js
     ```

2. **Middleware Chain Architecture**
   - Order middleware correctly: logging → CORS → security → parsing → routes → error handling
   - Use middleware for cross-cutting concerns
   - Implement proper error propagation
   - Example middleware setup:
     ```js
     const express = require('express');
     const helmet = require('helmet');
     const cors = require('cors');
     const morgan = require('morgan');
     const rateLimit = require('express-rate-limit');

     const app = express();

     // Security middleware
     app.use(helmet());
     app.use(cors({
       origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
       credentials: true
     }));

     // Rate limiting
     const limiter = rateLimit({
       windowMs: 15 * 60 * 1000, // 15 minutes
       max: 100 // limit each IP to 100 requests per windowMs
     });
     app.use('/api/', limiter);

     // Logging and parsing
     app.use(morgan('combined'));
     app.use(express.json({ limit: '10mb' }));
     app.use(express.urlencoded({ extended: true }));
     ```

3. **Async/Await Error Handling**
   - Use async/await consistently for asynchronous operations
   - Implement proper error catching and propagation
   - Create error wrapper utilities for clean code
   - Example async handler pattern:
     ```js
     // Async wrapper utility
     const asyncHandler = (fn) => (req, res, next) => {
       Promise.resolve(fn(req, res, next)).catch(next);
     };

     // Usage in routes
     router.get('/users/:id', asyncHandler(async (req, res) => {
       const user = await User.findById(req.params.id);
       if (!user) {
         return res.status(404).json({ error: 'User not found' });
       }
       res.json({ data: user });
     }));
     ```

---

## RESTful API Design

4. **Resource-Based URL Structure**
   - Use nouns for resources, not verbs
   - Implement consistent naming conventions
   - Follow REST principles for HTTP methods
   - Example API structure:
     ```js
     // Good RESTful routes
     GET    /api/users           // Get all users
     GET    /api/users/:id       // Get specific user
     POST   /api/users           // Create new user
     PUT    /api/users/:id       // Update entire user
     PATCH  /api/users/:id       // Partial user update
     DELETE /api/users/:id       // Delete user

     // Nested resources
     GET    /api/users/:id/posts // Get user's posts
     POST   /api/users/:id/posts // Create post for user
     ```

5. **HTTP Status Code Standards**
   - Use appropriate status codes for different scenarios
   - Implement consistent response formats
   - Handle edge cases with proper status codes
   - Example status code usage:
     ```js
     // Success responses
     res.status(200).json({ data: result });           // OK
     res.status(201).json({ data: newResource });      // Created
     res.status(204).send();                           // No Content

     // Client error responses
     res.status(400).json({ error: 'Invalid input' }); // Bad Request
     res.status(401).json({ error: 'Unauthorized' });  // Unauthorized
     res.status(403).json({ error: 'Forbidden' });     // Forbidden
     res.status(404).json({ error: 'Not found' });     // Not Found
     res.status(409).json({ error: 'Conflict' });      // Conflict

     // Server error responses
     res.status(500).json({ error: 'Internal server error' }); // Internal Error
     ```

6. **Request Validation and Sanitization**
   - Validate all incoming data using schemas
   - Sanitize inputs to prevent injection attacks
   - Provide clear validation error messages
   - Example with Joi validation:
     ```js
     const Joi = require('joi');

     const userSchema = Joi.object({
       name: Joi.string().min(2).max(50).required(),
       email: Joi.string().email().required(),
       age: Joi.number().integer().min(13).max(120),
       password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
     });

     const validateUser = (req, res, next) => {
       const { error, value } = userSchema.validate(req.body);
       if (error) {
         return res.status(400).json({
           error: 'Validation failed',
           details: error.details.map(d => d.message)
         });
       }
       req.validatedData = value;
       next();
     };
     ```

---

## Authentication and Security

7. **JWT Authentication Implementation**
   - Implement secure JWT token generation and validation
   - Use refresh token strategy for enhanced security
   - Handle token expiration gracefully
   - Example JWT implementation:
     ```js
     const jwt = require('jsonwebtoken');
     const bcrypt = require('bcrypt');

     // Generate tokens
     const generateTokens = (user) => {
       const accessToken = jwt.sign(
         { userId: user.id, email: user.email },
         process.env.JWT_SECRET,
         { expiresIn: '15m' }
       );

       const refreshToken = jwt.sign(
         { userId: user.id },
         process.env.JWT_REFRESH_SECRET,
         { expiresIn: '7d' }
       );

       return { accessToken, refreshToken };
     };

     // Verify token middleware
     const authenticateToken = (req, res, next) => {
       const authHeader = req.headers['authorization'];
       const token = authHeader && authHeader.split(' ')[1];

       if (!token) {
         return res.status(401).json({ error: 'Access token required' });
       }

       jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
         if (err) {
           return res.status(403).json({ error: 'Invalid token' });
         }
         req.user = user;
         next();
       });
     };
     ```

8. **Password Security**
   - Hash passwords using bcrypt with appropriate salt rounds
   - Implement password strength requirements
   - Never store plain text passwords
   - Example password handling:
     ```js
     const bcrypt = require('bcrypt');
     const SALT_ROUNDS = 12;

     // Hash password before saving
     const hashPassword = async (password) => {
       return await bcrypt.hash(password, SALT_ROUNDS);
     };

     // Verify password during login
     const verifyPassword = async (plainPassword, hashedPassword) => {
       return await bcrypt.compare(plainPassword, hashedPassword);
     };

     // Registration endpoint
     router.post('/register', validateUser, async (req, res) => {
       const { name, email, password } = req.validatedData;

       const existingUser = await User.findOne({ email });
       if (existingUser) {
         return res.status(409).json({ error: 'Email already registered' });
       }

       const hashedPassword = await hashPassword(password);
       const user = await User.create({
         name,
         email,
         password: hashedPassword
       });

       const tokens = generateTokens(user);
       res.status(201).json({ data: { user: { id: user.id, name, email }, tokens } });
     });
     ```

9. **Security Headers and CORS**
   - Use helmet for security headers
   - Configure CORS properly
   - Implement CSP (Content Security Policy)
   - Example security configuration:
     ```js
     const helmet = require('helmet');

     app.use(helmet({
       contentSecurityPolicy: {
         directives: {
           defaultSrc: ["'self'"],
           styleSrc: ["'self'", "'unsafe-inline'"],
           scriptSrc: ["'self'"],
           imgSrc: ["'self'", 'data:', 'https:'],
         },
       },
       hsts: {
         maxAge: 31536000,
         includeSubDomains: true,
         preload: true
       }
     }));
     ```

---

## Error Handling and Logging

10. **Centralized Error Handling**
    - Create custom error classes for different error types
    - Implement global error handling middleware
    - Log errors appropriately for debugging
    - Example error handling system:
      ```js
      // Custom error classes
      class AppError extends Error {
        constructor(message, statusCode) {
          super(message);
          this.statusCode = statusCode;
          this.isOperational = true;
          Error.captureStackTrace(this, this.constructor);
        }
      }

      class ValidationError extends AppError {
        constructor(message) {
          super(message, 400);
        }
      }

      class NotFoundError extends AppError {
        constructor(resource) {
          super(`${resource} not found`, 404);
        }
      }

      // Global error handler
      const errorHandler = (err, req, res, next) => {
        let { statusCode = 500, message } = err;

        if (process.env.NODE_ENV === 'production' && !err.isOperational) {
          statusCode = 500;
          message = 'Something went wrong';
        }

        logger.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);

        res.status(statusCode).json({
          error: message,
          ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
        });
      };
      ```

11. **Structured Logging**
    - Use proper logging libraries (Winston, Pino)
    - Implement different log levels
    - Structure logs for easy parsing and monitoring
    - Example logging setup:
      ```js
      const winston = require('winston');

      const logger = winston.createLogger({
        level: process.env.LOG_LEVEL || 'info',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.errors({ stack: true }),
          winston.format.json()
        ),
        transports: [
          new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
          new winston.transports.File({ filename: 'logs/combined.log' })
        ]
      });

      if (process.env.NODE_ENV !== 'production') {
        logger.add(new winston.transports.Console({
          format: winston.format.simple()
        }));
      }
      ```

---

## Performance Optimization

12. **Database Connection Management**
    - Use connection pooling for database connections
    - Implement proper connection error handling
    - Monitor connection pool health
    - Example MongoDB connection:
      ```js
      const mongoose = require('mongoose');

      const connectDB = async () => {
        try {
          const conn = await mongoose.connect(process.env.MONGODB_URI, {
            maxPoolSize: 10,
            serverSelectionTimeoutMS: 5000,
            socketTimeoutMS: 45000,
          });

          logger.info(`MongoDB Connected: ${conn.connection.host}`);
        } catch (error) {
          logger.error('Database connection failed:', error);
          process.exit(1);
        }
      };

      // Handle connection events
      mongoose.connection.on('error', (err) => {
        logger.error('Database connection error:', err);
      });

      mongoose.connection.on('disconnected', () => {
        logger.warn('Database disconnected');
      });
      ```

13. **Caching Strategies**
    - Implement Redis caching for frequently accessed data
    - Use appropriate cache expiration policies
    - Cache expensive computations and database queries
    - Example Redis caching:
      ```js
      const redis = require('redis');
      const client = redis.createClient({
        host: process.env.REDIS_HOST || 'localhost',
        port: process.env.REDIS_PORT || 6379,
        password: process.env.REDIS_PASSWORD
      });

      // Cache middleware
      const cacheMiddleware = (duration = 300) => {
        return async (req, res, next) => {
          const key = `cache:${req.originalUrl}`;

          try {
            const cached = await client.get(key);
            if (cached) {
              return res.json(JSON.parse(cached));
            }

            // Store original res.json
            const originalJson = res.json;
            res.json = function(data) {
              // Cache the response
              client.setex(key, duration, JSON.stringify(data));
              return originalJson.call(this, data);
            };

            next();
          } catch (error) {
            logger.error('Cache error:', error);
            next();
          }
        };
      };
      ```

14. **Request Optimization**
    - Implement compression middleware
    - Use proper pagination for large datasets
    - Optimize query performance
    - Example optimization techniques:
      ```js
      const compression = require('compression');

      // Enable compression
      app.use(compression());

      // Pagination helper
      const paginate = (page = 1, limit = 10) => {
        const offset = (page - 1) * limit;
        return { offset, limit: parseInt(limit) };
      };

      // Paginated endpoint
      router.get('/users', async (req, res) => {
        const { page, limit } = req.query;
        const { offset, limit: pageLimit } = paginate(page, limit);

        const total = await User.countDocuments();
        const users = await User.find()
          .skip(offset)
          .limit(pageLimit)
          .select('-password')
          .sort({ createdAt: -1 });

        res.json({
          data: users,
          pagination: {
            page: parseInt(page) || 1,
            limit: pageLimit,
            total,
            pages: Math.ceil(total / pageLimit)
          }
        });
      });
      ```

---

## Testing and Quality Assurance

15. **API Testing Strategy**
    - Write unit tests for business logic
    - Implement integration tests for endpoints
    - Use proper test databases for isolation
    - Example testing setup:
      ```js
      const request = require('supertest');
      const app = require('../app');

      describe('User API', () => {
        beforeEach(async () => {
          await User.deleteMany({});
        });

        describe('POST /api/users', () => {
          it('should create a new user with valid data', async () => {
            const userData = {
              name: 'John Doe',
              email: 'john@example.com',
              password: 'Password123'
            };

            const response = await request(app)
              .post('/api/users')
              .send(userData)
              .expect(201);

            expect(response.body.data.user.email).toBe(userData.email);
            expect(response.body.data.user.password).toBeUndefined();
          });

          it('should return 400 for invalid email', async () => {
            const userData = {
              name: 'John Doe',
              email: 'invalid-email',
              password: 'Password123'
            };

            await request(app)
              .post('/api/users')
              .send(userData)
              .expect(400);
          });
        });
      });
      ```

---

## Production Deployment

16. **Environment Configuration**
    - Use environment variables for all configuration
    - Implement proper secret management
    - Configure different environments (dev, staging, prod)
    - Example environment setup:
      ```js
      // config/config.js
      const config = {
        development: {
          port: process.env.PORT || 3000,
          dbUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp-dev',
          jwtSecret: process.env.JWT_SECRET || 'dev-secret',
          logLevel: 'debug'
        },
        production: {
          port: process.env.PORT || 8080,
          dbUri: process.env.MONGODB_URI,
          jwtSecret: process.env.JWT_SECRET,
          logLevel: 'error'
        }
      };

      module.exports = config[process.env.NODE_ENV || 'development'];
      ```

17. **Process Management**
    - Use PM2 for process management in production
    - Implement graceful shutdown handling
    - Monitor application health
    - Example PM2 configuration:
      ```js
      // ecosystem.config.js
      module.exports = {
        apps: [{
          name: 'api-server',
          script: './server.js',
          instances: 'max',
          exec_mode: 'cluster',
          env: {
            NODE_ENV: 'production',
            PORT: 8080
          },
          error_file: './logs/err.log',
          out_file: './logs/out.log',
          log_file: './logs/combined.log',
          time: true
        }]
      };
      ```

---

## Summary Checklist

- [ ] Implement modular route organization with Express Router
- [ ] Use proper middleware chain ordering
- [ ] Handle async operations with proper error catching
- [ ] Validate and sanitize all inputs
- [ ] Implement JWT authentication with refresh tokens
- [ ] Use bcrypt for password hashing
- [ ] Configure security headers with helmet
- [ ] Implement rate limiting and CORS
- [ ] Create centralized error handling
- [ ] Use structured logging for monitoring
- [ ] Implement database connection pooling
- [ ] Add caching for performance optimization
- [ ] Write comprehensive tests for all endpoints
- [ ] Configure proper environment variables
- [ ] Use PM2 for production process management

---

Follow these practices to build secure, scalable, and maintainable Express.js APIs that perform well in production environments.
Node.js Express API Expert - Cursor IDE AI Rule