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
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
Implement a clean project structure with separation of concerns (controllers, services, models).
Integrate essential middleware for security, rate limiting, logging, and request parsing.
Establish a comprehensive error handling system with custom error classes and global handlers.
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
Writes source code or scripts from a description.
Traces errors to their root cause and suggests fixes.
Analyzes code for bugs, style issues, and improvements.
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, anddatafields - 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
/healthendpoint 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.