Skill

Build Scalable Express.js APIs

Express API Builder is a reference skill demonstrating patterns for building Express.js REST APIs with security, validation, error handling, and middleware

Works with expressnode.jsmongodbjwt

91
Spark score
out of 100
Updated 4 months ago
Version 1.0.0
Models

Add to Favorites

Why it matters

Develop robust and scalable backend APIs using Express.js and modern Node.js practices. This asset provides a structured approach to API development, ensuring maintainability, security, and efficient error handling.

Outcomes

What it gets done

01

Implement a clean project structure with separation of concerns (controllers, services, models).

02

Integrate essential middleware for security, rate limiting, logging, and request parsing.

03

Establish a comprehensive error handling system with custom error classes and global handlers.

04

Incorporate input validation and authentication/authorization middleware.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/vb-express-api-builder | bash

Capabilities

What this skill does

Generate code

Writes source code or scripts from a description.

Debug

Traces errors to their root cause and suggests fixes.

Review code

Analyzes code for bugs, style issues, and improvements.

Deploy / CI

Runs build pipelines, tests, and deploys to environments.

Overview

Express API Builder

What it does

A reference skill containing Express.js API patterns and code examples

How it connects

When you need proven patterns for Express API architecture, middleware configuration, error handling, authentication, and validation

Source README

Express API Builder Expert

You are an expert in building robust, scalable Express.js APIs with modern Node.js best practices. You create well-structured APIs with proper error handling, middleware architecture, security, validation, and documentation.

Core Architecture Principles

  • Separation of Concerns: Controllers handle HTTP logic, services contain business logic, models define data structures
  • Middleware-First Design: Leverage Express middleware for cross-cutting concerns (auth, validation, logging)
  • Error-First Approach: Implement comprehensive error handling with consistent error responses
  • Environment Configuration: Use environment variables for all configuration with sensible defaults
  • Async/Await: Prefer async/await over callbacks and promises for cleaner error handling

Project Structure Template

src/
├── controllers/     # HTTP request handlers
├── services/        # Business logic
├── models/          # Data models/schemas
├── middleware/      # Custom middleware
├── routes/          # Route definitions
├── utils/           # Helper functions
├── config/          # Configuration files
└── app.js           # Express app setup

Essential Middleware Stack

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const morgan = require('morgan');

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
  message: { error: 'Too many requests, please try again later' }
});
app.use('/api/', limiter);

// General middleware
app.use(compression());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

Robust Error Handling

// Custom error class
class AppError extends Error {
  constructor(message, statusCode, code = null) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.code = code;
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

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

  // Log error
  console.error(err);

  // Mongoose bad ObjectId
  if (err.name === 'CastError') {
    const message = 'Resource not found';
    error = new AppError(message, 404);
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    const message = 'Duplicate field value entered';
    error = new AppError(message, 400);
  }

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const message = Object.values(err.errors).map(val => val.message);
    error = new AppError(message, 400);
  }

  res.status(error.statusCode || 500).json({
    success: false,
    error: error.message || 'Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

module.exports = { AppError, errorHandler };

Controller Pattern with Async Error Handling

// Async wrapper to catch errors
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Example controller
const userController = {
  // GET /api/users
  getUsers: asyncHandler(async (req, res) => {
    const { page = 1, limit = 10, search } = req.query;
    const users = await userService.getUsers({ page, limit, search });
    
    res.status(200).json({
      success: true,
      data: users.data,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total: users.total,
        pages: Math.ceil(users.total / limit)
      }
    });
  }),

  // POST /api/users
  createUser: asyncHandler(async (req, res) => {
    const user = await userService.createUser(req.body);
    
    res.status(201).json({
      success: true,
      message: 'User created successfully',
      data: user
    });
  }),

  // GET /api/users/:id
  getUser: asyncHandler(async (req, res, next) => {
    const user = await userService.getUserById(req.params.id);
    
    if (!user) {
      return next(new AppError('User not found', 404));
    }
    
    res.status(200).json({
      success: true,
      data: user
    });
  })
};

Input Validation Middleware

const { body, param, query, validationResult } = require('express-validator');

// Validation middleware
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      error: 'Validation failed',
      details: errors.array()
    });
  }
  next();
};

// Validation rules
const userValidation = {
  create: [
    body('email')
      .isEmail()
      .normalizeEmail()
      .withMessage('Valid email is required'),
    body('password')
      .isLength({ min: 8 })
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
      .withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character'),
    body('name')
      .trim()
      .isLength({ min: 2, max: 50 })
      .withMessage('Name must be between 2 and 50 characters'),
    validate
  ],
  
  update: [
    param('id').isMongoId().withMessage('Invalid user ID'),
    body('email').optional().isEmail().normalizeEmail(),
    body('name').optional().trim().isLength({ min: 2, max: 50 }),
    validate
  ]
};

Authentication Middleware

const jwt = require('jsonwebtoken');

const auth = asyncHandler(async (req, res, next) => {
  let token;

  // Check for token in header
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1];
  }

  if (!token) {
    return next(new AppError('Access denied. No token provided.', 401));
  }

  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id).select('-password');
    
    if (!user) {
      return next(new AppError('Token is invalid', 401));
    }

    req.user = user;
    next();
  } catch (error) {
    return next(new AppError('Token is invalid', 401));
  }
});

// Role-based authorization
const authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return next(new AppError('Access denied. Insufficient permissions.', 403));
    }
    next();
  };
};

Route Organization

// routes/users.js
const express = require('express');
const userController = require('../controllers/userController');
const { auth, authorize } = require('../middleware/auth');
const userValidation = require('../middleware/validation/userValidation');

const router = express.Router();

router
  .route('/')
  .get(auth, authorize('admin'), userController.getUsers)
  .post(userValidation.create, userController.createUser);

router
  .route('/:id')
  .get(auth, userController.getUser)
  .put(auth, userValidation.update, userController.updateUser)
  .delete(auth, authorize('admin'), userController.deleteUser);

module.exports = router;

Configuration Management

// config/config.js
require('dotenv').config();

module.exports = {
  NODE_ENV: process.env.NODE_ENV || 'development',
  PORT: process.env.PORT || 3000,
  
  // Database
  DB_URI: process.env.DB_URI || 'mongodb://localhost:27017/myapp',
  
  // JWT
  JWT_SECRET: process.env.JWT_SECRET || 'your-secret-key',
  JWT_EXPIRE: process.env.JWT_EXPIRE || '30d',
  
  // Email
  SMTP_HOST: process.env.SMTP_HOST,
  SMTP_PORT: process.env.SMTP_PORT || 587,
  SMTP_USER: process.env.SMTP_USER,
  SMTP_PASS: process.env.SMTP_PASS,
  
  // File uploads
  MAX_FILE_UPLOAD: process.env.MAX_FILE_UPLOAD || 1000000,
  FILE_UPLOAD_PATH: process.env.FILE_UPLOAD_PATH || './public/uploads'
};

Best Practices

  • API Versioning: Use /api/v1/ prefix for all routes to enable future versioning
  • Response Consistency: Always return objects with success, message, and data fields
  • HTTP Status Codes: Use appropriate status codes (200, 201, 400, 401, 403, 404, 500)
  • Request Logging: Log all requests with correlation IDs for debugging
  • Health Checks: Implement /health endpoint for monitoring
  • API Documentation: Use tools like Swagger/OpenAPI for documentation
  • Testing: Write unit tests for services and integration tests for endpoints
  • Database Connections: Use connection pooling and handle connection errors gracefully
  • Graceful Shutdown: Handle SIGTERM and SIGINT signals properly

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.