PlusExpress is a powerful library that enhances Express.js with:
- Type-safe validation - Validate requests using Zod schemas
- Automatic OpenAPI documentation - Generate OpenAPI/Swagger docs from your routes
- Enhanced route definitions - Cleaner, more intuitive API for defining routes
npm install plus-expressPlusExpress has express as a peer dependency, so make sure you have it installed:
npm install expressNote: Zod is included as a dependency, so you don't need to install it separately. PlusExpress re-exports Zod for your convenience.
import express from 'express';
import { plus, z } from 'plus-express';
// Initialize PlusExpress with an existing Express app
const { app, registry } = plus(express());
// Configure the registry using the builder pattern
registry
.setInfo({
title: 'My API',
version: '1.0.0',
description: 'My awesome API'
})
.addServer({
url: 'http://localhost:3000',
description: 'Development server'
})
.registerSecurityScheme('ApiKeyAuth', {
type: 'apiKey',
in: 'header',
name: 'X-API-KEY'
});
// Define routes with validation
app.get({
path: '/users/:id',
summary: 'Get a user by ID',
params: z.object({
id: z.string().uuid()
}),
responses: {
200: {
description: 'User retrieved successfully',
content: {
'application/json': {
schema: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
})
}
}
}
}
}, (req, res) => {
// TypeScript knows that req.parsed.params.id is a valid UUID string
const userId = req.parsed.params.id;
// Your route handler logic
res.json({ id: userId, name: 'John Doe', email: 'john@example.com' });
});
// Generate OpenAPI documentation
app.get('/api-docs.json', (req, res) => {
res.json(registry.generateOpenAPIDocument());
});
// Start the server
app.listen(3000, () => {
console.log('Server running on port 3000');
});PlusExpress uses a fluent builder pattern for configuration:
// Get the registry from plus
const { app, registry } = plus(express());
// Configure using chainable methods
registry
.setInfo({
title: 'My API',
version: '1.0.0',
description: 'My awesome API'
})
.addServer({
url: 'http://localhost:3000',
description: 'Development server'
})
.addServer({
url: 'https://api.example.com',
description: 'Production server'
})
.setDefaultQuerySchema(z.object({
format: z.enum(['json', 'xml']).optional()
}))
.setDefaultHeaderSchema(z.object({
'x-api-key': z.string().optional()
}))
.setDefaultResponses({
400: {
description: 'Bad Request',
content: {
'application/json': {
schema: z.object({
message: z.string(),
errors: z.array(z.string())
})
}
}
}
})
.registerSecurityScheme('ApiKeyAuth', {
type: 'apiKey',
in: 'header',
name: 'X-API-KEY'
});PlusExpress extends Express's route methods with additional signatures that support validation and documentation:
// Standard Express route definition
app.get('/users', (req, res) => { /* ... */ });
// PlusExpress route with validation and docs
app.get({
path: '/users',
summary: 'Get all users',
query: z.object({
limit: z.number().optional(),
offset: z.number().optional()
})
}, (req, res) => {
// TypeScript knows req.parsed.query.limit is a number or undefined
const { limit, offset } = req.parsed.query;
// Your route handler logic
});
// Alternative syntax with path as first argument
app.get('/users/:id', {
summary: 'Get a user by ID',
params: z.object({
id: z.string()
})
}, (req, res) => {
// TypeScript knows req.parsed.params.id is a string
const userId = req.parsed.params.id;
// Your route handler logic
});PlusExpress automatically validates incoming requests against your Zod schemas:
app.post({
path: '/users',
summary: 'Create a new user',
body: z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18)
})
}, (req, res) => {
// req.parsed.body is guaranteed to match the schema
// If validation fails, an error response is automatically sent
const { name, email, age } = req.parsed.body;
// Your route handler logic
res.json({ id: '123', name, email, age });
});PlusExpress adds a parsed namespace to the request object that contains all validated data:
app.get({
path: '/users/:id',
params: z.object({ id: z.string().uuid() }),
query: z.object({ details: z.boolean().optional() }),
headers: z.object({ 'x-api-key': z.string() })
}, (req, res) => {
// Access validated data through the parsed namespace
const { id } = req.parsed.params; // Typed as string & UUID
const { details } = req.parsed.query; // Typed as boolean | undefined
const apiKey = req.parsed.headers['x-api-key']; // Typed as string
// Your route handler logic
});This keeps all validated data organized in a single namespace while maintaining compatibility with the original Express properties (req.body, req.params, etc.).
PlusExpress provides a unified plus() function that works for both apps and routers:
import { plus } from 'plus-express';
// Create a new enhanced router (plus() with no arguments)
const { router, registry } = plus();
// Configure the router's registry
registry.setInfo({
title: 'User API',
version: '1.0.0'
});
// Use the enhanced router like the app
router.get({
path: '/products',
summary: 'Get all products',
query: z.object({
category: z.string().optional()
})
}, (req, res) => {
// Your handler logic
});
// Add the router to your app
app.use('/api', router);PlusExpress automatically handles router composition, correctly combining OpenAPI specifications from multiple routers and respecting mount paths:
// Create multiple routers using the unified plus() function
const { router: usersRouter } = plus();
const { router: productsRouter } = plus();
const { router: adminRouter } = plus();
// Define routes on each router
usersRouter.get('/profile', /* ... */);
productsRouter.get('/catalog', /* ... */);
adminRouter.get('/stats', /* ... */);
// Compose routers with nesting
usersRouter.use('/admin', adminRouter); // Nested router
// Mount routers at different paths
app.use('/api/v1/users', usersRouter);
app.use('/api/v1/products', productsRouter);
// The OpenAPI documentation will automatically include all routes
// with correct paths:
// - /api/v1/users/profile
// - /api/v1/users/admin/stats
// - /api/v1/products/catalogPlusExpress uses a smart tracking system to handle router composition:
- Each
RouterPlusinstance is marked with its registry - When a router is mounted (via
app.use()orrouter.use()), the mount path is registered - When generating OpenAPI documentation, all registries are combined
- Paths are automatically adjusted to include the mount paths
This works with any level of nesting, allowing you to organize your API however you prefer while maintaining correct documentation.
PlusExpress automatically handles validation errors and provides a standardized error format. When validation fails, the middleware automatically sends an error response without reaching your route handler.
{
"status": 400,
"message": "Validation failed",
"errors": [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": ["body", "name"],
"message": "Expected string, received number"
}
]
}You can add your own error handler to customize error responses:
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
const status = err.status || 500;
const message = err.message || 'Internal Server Error';
// Custom error logging
console.error(`Error ${status}:`, message);
res.status(status).json({
status,
message,
errors: err.errors,
// Add custom fields
timestamp: new Date().toISOString(),
path: req.path
});
});PlusExpress automatically generates OpenAPI documentation based on your route definitions and registry configuration:
// Serve OpenAPI documentation
app.get('/api-docs.json', (req, res) => {
res.json(registry.generateOpenAPIDocument());
});
// Optional: Serve Swagger UI (requires swagger-ui-express)
import swaggerUi from 'swagger-ui-express';
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(null, {
swaggerUrl: '/api-docs.json'
}));The unified plus() function works with both Express applications and routers. It automatically detects the type and applies the appropriate enhancements:
// Enhance an Express application
const { app, registry } = plus(express());
// Create a new enhanced router
const { router, registry } = plus();
// Enhance an existing router
const existingRouter = express.Router();
const { router, registry } = plus(existingRouter);For those who prefer explicit naming, plusRouter() is also available as an alias:
import { plusRouter } from 'plus-express';
const { router, registry } = plusRouter();The registry provides these chainable configuration methods:
setInfo(info)- Set API title, version, and descriptionaddServer(server)- Add a server to the OpenAPI documentsetDefaultQuerySchema(schema)- Set default query schema for all routessetDefaultHeaderSchema(schema)- Set default header schema for all routessetDefaultResponses(responses)- Set default responses for all routesregisterSecurityScheme(name, scheme)- Add a security schemegenerateOpenAPIDocument(config?)- Generate the OpenAPI documentgetRawRegistry()- Get the underlying OpenAPI registry
PlusExpress also exports convenience factory functions:
Creates an enhanced Express application, optionally creating a new Express app if none is provided:
import { createExpressPlus } from 'plus-express';
// Creates a new Express app automatically
const { app, registry } = createExpressPlus();
// Or enhance an existing app
import express from 'express';
const existingApp = express();
const { app, registry } = createExpressPlus(existingApp);Note: This requires Express to be installed as it uses require('express') internally when no app is provided.
Creates an enhanced Express router:
import { createRouterPlus } from 'plus-express';
// Create a new router
const { router, registry } = createRouterPlus();
// Or enhance an existing router
import express from 'express';
const existingRouter = express.Router();
const { router, registry } = createRouterPlus(existingRouter);PlusExpress is written in TypeScript and exports all its types for your use:
import {
// Main types
ExpressPlusApplication,
RouterPlus,
Registry,
// Configuration types
ApiOptions,
OpenAPIConfig,
EndpointOptions,
ResponseObject,
// Request types
ValidatedRequest,
TypedExpressHandler,
// Utility types
HttpMethod,
InferZodType,
// Return types
ExpressPlusReturn,
RouterPlusReturn
} from 'plus-express';The ValidatedRequest type is particularly useful for adding custom middleware:
import { ValidatedRequest, TypedExpressHandler } from 'plus-express';
import { z } from 'plus-express';
// Custom middleware with type safety
const authMiddleware: TypedExpressHandler<
undefined, // body
undefined, // params
z.ZodObject<{ token: z.ZodString }>, // query
undefined // headers
> = (req, res, next) => {
// TypeScript knows req.parsed.query.token is a string
const token = req.parsed.query.token;
// Your auth logic
next();
};If you see a peer dependency warning, ensure you have Express installed:
npm install expressPlusExpress supports Express 4.17.1+ and Express 5.x.
If you encounter TypeScript errors with Express methods, ensure you have the latest types:
npm install --save-dev @types/expressMake sure you're accessing validated data through req.parsed rather than the original req.body, req.params, etc.:
// ✅ Correct - uses validated data
const { id } = req.parsed.params;
// ❌ Wrong - bypasses validation
const { id } = req.params;Note: PlusExpress does update the original properties for backward compatibility, but using req.parsed is recommended for clarity and type safety.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT