JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 197
  • Score
    100M100P100Q34495F
  • License ISC

Production-ready Express 5 toolkit with auto-generated OpenAPI docs, structured error handling, and logging

Package Exports

  • @extk/expressive

Readme

extk/expressive logo

Express 5 toolkit

Auto-generated OpenAPI docs, structured error handling, and logging — out of the box.

npm version node version license


What is this?

@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.

Install

npm install @extk/expressive express

Requires Node.js >= 22 and Express 5.

Quick Start

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.

Error Handling

Throw typed errors anywhere in your handlers. The error middleware catches them and returns a consistent JSON response.

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:

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
}));

OpenAPI / Swagger

Routes registered with addRoute are automatically added to the OpenAPI spec. Use the SWG helper to define parameters and schemas:

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:

swaggerBuilder()
  .withSecuritySchemes({
    BearerAuth: SWG.securitySchemes.BearerAuth(),
  })
  .withDefaultSecurity([SWG.security('BearerAuth')]);

Using Zod schemas for OpenAPI

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:

// 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:

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:

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.

Logging

Winston-based logging with daily rotating files in production and console output in development.

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);

Utilities

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'

API Response Format

All responses follow a consistent shape:

// Success
{ "status": "ok", "result": { /* ... */ } }

// Error
{ "status": "error", "message": "Not found", "errorCode": "NOT_FOUND", "errors": null }

License

ISC