This document provides detailed information about the middleware, utilities, and helper functions available in your scaffolded backend project.
The validation middleware provides powerful, type-safe request validation using Zod schemas.
Validates multiple parts of the request (body, params, query, headers) in a single middleware.
Usage:
import { validate } from './middleware/validate';
import { z } from 'zod';
// Define schemas
const schemas = {
params: z.object({ id: z.string().uuid() }),
body: z.object({ title: z.string(), content: z.string() }),
query: z.object({ publish: z.enum(['true', 'false']).optional() }),
headers: z.object({ 'x-api-key': z.string() }).partial()
};
// Use in routes
router.post('/posts/:id', validate(schemas), updatePost);Parameters:
schemas- Object containing Zod schemas for different request partsbody?- Validatereq.bodyparams?- Validatereq.paramsquery?- Validatereq.queryheaders?- Validatereq.headers
Features:
- ✅ Validates in order: params → query → headers → body
- ✅ Stops at first validation error
- ✅ Returns which part failed with
errorInfield - ✅ Assigns validated data back to
req(type-safe!)
Error Response:
{
"success": false,
"errorIn": "body",
"errors": [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"message": "String must contain at least 1 character(s)",
"path": ["title"]
}
]
}router.post('/users',
validate({
body: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional()
})
}),
createUser
);router.get('/users/:id',
validate({
params: z.object({
id: z.string().uuid()
}),
query: z.object({
include: z.enum(['posts', 'comments']).optional()
})
}),
getUser
);router.patch('/posts/:id',
validate({
params: z.object({
id: z.string().uuid()
}),
body: z.object({
title: z.string().optional(),
content: z.string().optional(),
published: z.boolean().optional()
}),
query: z.object({
notify: z.enum(['true', 'false']).default('true')
}),
headers: z.object({
'x-user-id': z.string().uuid()
}).partial() // Makes all fields optional
}),
updatePost
);The validated data is automatically typed and assigned back to the request:
const updatePostSchema = {
params: z.object({ id: z.string().uuid() }),
body: z.object({ title: z.string() })
};
router.post('/posts/:id',
validate(updatePostSchema),
(req, res) => {
// req.params.id is string (validated UUID)
// req.body.title is string
const { id } = req.params; // Type: string
const { title } = req.body; // Type: string
}
);import { z } from 'zod';
// String validations
z.string() // Any string
z.string().min(3).max(50) // Length constraints
z.string().email() // Email format
z.string().url() // URL format
z.string().uuid() // UUID format
z.string().regex(/^[a-z]+$/) // Custom regex
// Number validations
z.number() // Any number
z.number().int() // Integer only
z.number().min(0).max(100) // Range
z.number().positive() // > 0
z.number().nonnegative() // >= 0
// Boolean
z.boolean()
// Enum
z.enum(['admin', 'user', 'guest'])
// Optional fields
z.string().optional() // string | undefined
z.string().nullable() // string | null
z.string().nullish() // string | null | undefined
z.string().default('hello') // Has default value
// Arrays
z.array(z.string()) // Array of strings
z.array(z.number()).min(1).max(10) // Length constraints
// Objects
z.object({
name: z.string(),
age: z.number()
})
// Transformations
z.string().transform(s => s.toLowerCase())
z.string().transform(Number) // Convert string to number
// Refinements (custom validation)
z.string().refine(
val => val !== 'admin',
{ message: 'Username cannot be admin' }
)Global error handling middleware that catches all errors.
Location: src/middleware/errorHandler.ts
- Catches all errors from async routes
- Formats errors consistently
- Logs errors with Pino logger
- Handles Mongoose validation errors
- Handles MongoDB duplicate key errors
- Provides different responses for development vs production
{
"success": false,
"message": "Error message here",
"code": "NOT_FOUND",
"stack": "Error stack (development only)"
}code is only present when the error is an AppError subclass.
Location: src/utils/errors.ts
Throw these in services or controllers so the global error handler returns the correct HTTP status:
| Class | Status | Use case |
|---|---|---|
ValidationError |
400 | Invalid input |
UnauthorizedError |
401 | Not authenticated |
ForbiddenError |
403 | Not allowed |
NotFoundError |
404 | Resource not found |
ConflictError |
409 | Duplicate or conflict |
AppError |
(custom) | Base class; set statusCode |
Example:
import { NotFoundError, ValidationError } from '@/utils/errors';
const user = await User.findById(id);
if (!user) throw new NotFoundError('User not found');
if (!isValid(data)) throw new ValidationError('Invalid email');Already configured in src/app.ts:
// Error handler must be last middleware
app.use(errorHandler);Wrapper for async route handlers that automatically catches errors.
Location: src/utils/asyncHandler.ts
Wraps async functions and passes errors to the error handler.
Usage:
import { asyncHandler } from '../utils/asyncHandler';
export const getUsers = asyncHandler(async (req, res) => {
const users = await userService.findAll();
res.json({ success: true, data: users });
});
// Errors are automatically caught and passed to error handler
export const createUser = asyncHandler(async (req, res) => {
const user = await userService.create(req.body);
// If service throws error, it's automatically caught
res.status(201).json({ success: true, data: user });
});Without asyncHandler:
// ❌ Need try/catch boilerplate
export const getUsers = async (req, res, next) => {
try {
const users = await userService.findAll();
res.json({ success: true, data: users });
} catch (error) {
next(error);
}
};With asyncHandler:
// ✅ Clean and concise
export const getUsers = asyncHandler(async (req, res) => {
const users = await userService.findAll();
res.json({ success: true, data: users });
});Pino logger configured for development and production.
Location: src/utils/logger.ts
import { logger } from '../utils/logger';
// Log levels
logger.info('Server started on port 3000');
logger.error('Database connection failed', error);
logger.warn('Deprecated API endpoint used');
logger.debug('Debugging information');
// With context
logger.info({ userId: user.id }, 'User created');
logger.error({ error, userId }, 'Failed to create user');- Development: Pretty-printed, colorful logs
- Production: JSON format for log aggregation
Base class for all services providing common CRUD operations.
Location: src/services/base.service.ts
Your services can extend BaseService to get common operations:
import { BaseService } from './base.service';
import { User } from '../models/User.model';
class UserService extends BaseService<typeof User> {
// Inherited methods from BaseService:
// - findAll(filter?, options?)
// - findPaginated(filter, page, limit, sortBy, order, search?)
// - findById(id)
// - findOne(filter)
// - create(data)
// - update(id, data)
// - delete(id)
// - count(filter?)
// findPaginated optional search: case-insensitive text across fields
// await this.findPaginated({}, 1, 10, 'createdAt', 'desc', {
// fields: ['name', 'email'],
// term: req.query.q as string,
// });
// Add your custom methods
async findByEmail(email: string) {
return await User.findOne({ email });
}
}Centralized configuration from environment variables.
Location: src/config/config.ts
import { config } from '../config/config';
// Access config values
console.log(config.port); // 3000
console.log(config.nodeEnv); // 'development'
console.log(config.mongoUri); // MongoDB connection string- Add to
.env.example:
MY_API_KEY=your-api-key-here- Add to
config.ts:
export const config = {
// ... existing config
myApiKey: process.env.MY_API_KEY || ''
};- Add validation:
const requiredEnvVars = [
'MONGO_URI',
'MY_API_KEY' // Add here
];MongoDB connection setup with error handling.
Location: src/config/database.ts
Connects to MongoDB using Mongoose.
Features:
- Auto-reconnect on connection loss
- Connection error logging
- Success confirmation
Usage:
Already configured in src/server.ts:
import { connectDB } from './config/database';
connectDB();Previous: Project Structure | Next: Adding Features Guide