Production-ready Express 5 toolkit with auto-generated OpenAPI docs, structured error handling, and logging
npm install @extk/expressive
Auto-generated OpenAPI docs, structured error handling, and logging — out of the box.
---
@extk/expressive is an opinionated toolkit for Express 5 that wires up the things every API needs but nobody wants to set up from scratch:
- Auto-generated OpenAPI 3.1 docs from your route definitions
- Structured error handling with typed error classes and consistent JSON responses
- Winston logging with daily file rotation and dev/prod modes
- Security defaults via Helmet, safe query parsing, and morgan request logging
- Standardized responses (ApiResponse / ApiErrorResponse) across your entire API
You write routes. Expressive handles the plumbing.
``bash`
npm install @extk/expressive express
> Requires Node.js >= 22 and Express 5.
`ts
import express from 'express';
import { bootstrap, getDefaultFileLogger, ApiResponse, NotFoundError, SWG } from '@extk/expressive';
// 1. Bootstrap with a logger
const {
expressiveServer,
expressiveRouter,
swaggerBuilder,
notFoundMiddleware,
getErrorHandlerMiddleware,
} = bootstrap({
logger: getDefaultFileLogger('my-api'),
});
// 2. Configure swagger metadata
const swaggerDoc = swaggerBuilder()
.withInfo({ title: 'My API', version: '1.0.0' })
.withServers([{ url: 'http://localhost:3000' }])
.get();
// 3. Build the Express app with sensible defaults (helmet, morgan, swagger UI)
const app = expressiveServer()
.withDefaults({ path: '/api-docs', doc: swaggerDoc });
// 4. Define routes — they auto-register in the OpenAPI spec
const { router, addRoute } = expressiveRouter({
oapi: { tags: ['Users'] },
});
addRoute(
{
method: 'get',
path: '/users/:id',
oapi: {
summary: 'Get user by ID',
responses: { 200: { description: 'User found' } },
},
},
async (req, res) => {
const user = await findUser(req.params.id);
if (!user) throw new NotFoundError('User not found');
res.json(new ApiResponse(user));
},
);
// 5. Mount the router and error handling
app.use(router);
app.use(getErrorHandlerMiddleware());
app.use(notFoundMiddleware);
app.listen(3000);
`
Visit http://localhost:3000/api-docs to see the auto-generated Swagger UI.
Throw typed errors anywhere in your handlers. The error middleware catches them and returns a consistent JSON response.
`ts
import { NotFoundError, BadRequestError, ForbiddenError } from '@extk/expressive';
// Throws -> { status: "error", message: "User not found", errorCode: "NOT_FOUND" }
throw new NotFoundError('User not found');
// Attach extra data (e.g. validation details)
throw new BadRequestError('Invalid input').setData({ field: 'email', issue: 'required' });
`
Built-in error classes:
| Class | Status | Code |
| ------------------------ | ------ | ------------------------ |
| BadRequestError | 400 | BAD_REQUEST |SchemaValidationError
| | 400 | SCHEMA_VALIDATION_ERROR|FileTooBigError
| | 400 | FILE_TOO_BIG |InvalidFileTypeError
| | 400 | INVALID_FILE_TYPE |InvalidCredentialsError
| | 401 | INVALID_CREDENTIALS |TokenExpiredError
| | 401 | TOKEN_EXPIRED |UserUnauthorizedError
| | 401 | USER_UNAUTHORIZED |ForbiddenError
| | 403 | FORBIDDEN |NotFoundError
| | 404 | NOT_FOUND |DuplicateError
| | 409 | DUPLICATE_ENTRY |TooManyRequestsError
| | 429 | TOO_MANY_REQUESTS |InternalError
| | 500 | INTERNAL_ERROR |
You can also map external errors (e.g. Zod) via getErrorHandlerMiddleware:
`ts`
app.use(getErrorHandlerMiddleware((err) => {
if (err.name === 'ZodError') {
return new SchemaValidationError('Validation failed').setData(err.issues);
}
return null; // let the default handler deal with it
}));
Routes registered with addRoute are automatically added to the OpenAPI spec. Use the SWG helper to define parameters and schemas:
`ts`
addRoute(
{
method: 'get',
path: '/posts',
oapi: {
summary: 'List posts',
queryParameters: [
SWG.queryParam('page', { type: 'integer' }, false, 'Page number'),
SWG.queryParam('limit', { type: 'integer' }, false, 'Items per page'),
],
responses: {
200: { description: 'List of posts', ...SWG.jsonSchemaRef('PostList') },
},
},
},
listPostsHandler,
);
Configure security schemes via the swagger builder:
`ts`
swaggerBuilder()
.withSecuritySchemes({
BearerAuth: SWG.securitySchemes.BearerAuth(),
})
.withDefaultSecurity([SWG.security('BearerAuth')]);
You can use Zod's global registry to define your schemas once and have them appear in both validation and OpenAPI docs automatically.
1. Define schemas with .meta({ id }) to register them globally:
`ts
// schema/userSchema.ts
import z from 'zod';
export const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
firstName: z.string(),
lastName: z.string(),
role: z.enum(['admin', 'user']),
}).meta({ id: 'createUser' });
export const patchUserSchema = createUserSchema.partial().meta({ id: 'patchUser' });
export const loginSchema = z.object({
username: z.string().email(),
password: z.string(),
}).meta({ id: 'login' });
`
2. Pass all registered schemas to the swagger builder:
`ts
import z from 'zod';
const app = expressiveServer()
.withDefaults({
doc: swaggerBuilder()
.withInfo({ title: 'My API' })
.withServers([{ url: 'http://localhost:3000/api' }])
.withSchemas(z.toJSONSchema(z.globalRegistry).schemas) // all Zod schemas -> OpenAPI
.withSecuritySchemes({ auth: SWG.securitySchemes.BearerAuth() })
.withDefaultSecurity([SWG.security('auth')])
.get(),
});
`
3. Reference them in routes with SWG.jsonSchemaRef:
`ts
addRoute({
method: 'post',
path: '/user',
oapi: {
summary: 'Create a user',
requestBody: SWG.jsonSchemaRef('createUser'),
},
}, async (req, res) => {
const body = createUserSchema.parse(req.body); // validate with the same schema
const result = await userController.createUser(body);
res.status(201).json(new ApiResponse(result));
});
addRoute({
method: 'patch',
path: '/user/:id',
oapi: {
summary: 'Update a user',
requestBody: SWG.jsonSchemaRef('patchUser'),
},
}, async (req, res) => {
const id = parseIdOrFail(req.params.id);
const body = patchUserSchema.parse(req.body);
const result = await userController.updateUser(id, body);
res.json(new ApiResponse(result));
});
`
This way your Zod schemas serve as the single source of truth for both runtime validation and API documentation.
Winston-based logging with daily rotating files in production and console output in development.
`ts
import { getDefaultFileLogger, getDefaultConsoleLogger } from '@extk/expressive';
const logger = getDefaultFileLogger('my-service'); // logs to ./logs/my-service-YYYY-MM-DD.log
logger.info('Server started on port %d', 3000);
logger.error('Something went wrong: %s', err.message);
`
`ts
import {
slugify,
parseDefaultPagination,
parseIdOrFail,
getEnvVar,
isDev,
isProd,
} from '@extk/expressive';
slugify('Hello World!'); // 'hello-world!'
parseDefaultPagination({ page: '2', limit: '25' }); // { offset: 25, limit: 25 }
parseIdOrFail('42'); // 42 (throws on invalid)
getEnvVar('DATABASE_URL'); // string (throws if missing)
isDev(); // true when ENV !== 'prod'
`
All responses follow a consistent shape:
`jsonc
// Success
{ "status": "ok", "result": { / ... / } }
// Error
{ "status": "error", "message": "Not found", "errorCode": "NOT_FOUND", "errors": null }
``
ISC