npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

covenix

v0.1.0

Published

Zod-powered decorators for Express APIs — typed routes, runtime validation, and accurate OpenAPI from a single source of truth.

Readme

Zod-powered decorators for Express APIs — typed routes, runtime validation, and accurate OpenAPI from a single source of truth.

Documentation · API Reference

covenix lets you describe each endpoint with explicit Zod schemas and ergonomic decorators. From that one description it wires Express routes, validates every request, and generates a swagger.json that always matches what the code actually does — no separate build step, no config file, no code generation.

import { z } from 'zod';
import createError from 'http-errors';
import { Route, Tags, Get, Post, Params, Body, Returns, Param, BodyParam } from 'covenix';

const UserSchema = z.object({ id: z.string().uuid(), username: z.string() }).meta({ id: 'User' });
const CreateUserSchema = z.object({ username: z.string() }).meta({ id: 'CreateUser' });

@Route('users')
@Tags('Users')
export class UsersController {
  @Get('{id}')
  @Params(z.object({ id: z.string().uuid() }))
  @Returns(200, UserSchema)
  @Returns(404, ErrorSchema)
  async getUser(@Param('id') id: string) {
    const user = await db.users.findById(id);
    if (!user) throw new createError.NotFound();
    return user;
  }

  @Post()
  @Body(CreateUserSchema)
  @Returns(201, UserSchema)
  async createUser(@BodyParam() body: z.infer<typeof CreateUserSchema>) {
    return db.users.create(body);
  }
}

Why covenix

The Zod schema is the contract. The same schema that validates a request also produces its OpenAPI definition, so the two can never drift.

  • One source of truth. Request validation and the generated spec come from the same schemas.
  • Runtime validation included. Zod parses, coerces, and defaults every request before your handler runs. Handlers receive clean, typed data.
  • No build magic. Just tsc. Decorators run at class-definition time and store metadata via reflect-metadata; swagger is generated at startup.
  • Zero extra deps for schema conversion. Zod 4 ships z.toJSONSchema() natively, so schema → JSON Schema needs no helper library.
  • A typed client, generated. The same metadata produces a standalone, dependency-free TypeScript client — replacing a tsoa → swagger → openapi-generator pipeline with one accurate hop.

Installation

npm install covenix zod reflect-metadata express

covenix requires Zod 4+ and TypeScript 5+ with experimental decorators.

TypeScript configuration

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "module": "nodenext",
    "types": ["reflect-metadata"],
  },
}

Notes:

  • "type": "module" is required in package.json (ESM) to coexist with verbatimModuleSyntax + module: nodenext.
  • emitDecoratorMetadata is not required — covenix uses explicit parameter decorators and explicit instance registration, so it never needs runtime type metadata.
  • Import reflect-metadata once at your app's entry point.

Getting started

// app.ts
import 'reflect-metadata';
import express from 'express';
import { Covenix } from 'covenix';
import { UsersController } from './users.controller.js';

const app = express();
app.use(express.json());

const api = new Covenix({ info: { title: 'My API', version: '1.0.0' } });

// You own construction — inject dependencies explicitly.
api.register(new UsersController(db));

api.mount(app);

app.listen(3000);

That's it. A single Covenix instance owns your controllers; mount wires the Express routes and validation middleware, and the same instance generates swagger (see below). Request data is parsed by Zod before each handler runs.

Grouping & versioning

Give a set of controllers a shared base path — typically an API version segment — with a registration prefix or a group. It composes with each controller's own @Route prefix and shows up in both the routes and the spec:

api.register(new UsersController(svc), { prefix: '/v1' }); // → /v1/users

api.group('/v1', (v1) => {
  v1.register(new UsersController(svc)); // → /v1/users
  v1.register(new AuthController(auth)); // → /v1/auth/...
});

Groups nest, and the same controller can be mounted under more than one version. See Grouping & Versioning.


Decorator reference

Class decorators

| Decorator | Purpose | | ---------------- | --------------------------------------------------- | | @Route(prefix) | Path prefix for every route in the controller | | @Tags(...tags) | OpenAPI tags applied to the controller's operations |

Method decorators

| Decorator | Purpose | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | @Get(path?) @Post(path?) @Put(path?) @Patch(path?) @Delete(path?) | HTTP method + path (use {id} for path params) | | @Params(schema) | Validate req.params against a Zod object | | @Query(schema) | Validate req.query | | @Body(schema) | Validate req.body | | @Returns(status, schema?) | Declare a response — stackable; omit schema for no body (e.g. 204) | | @ReturnsFile(status, options?) | Declare a binary/file response (return a FileResponse) | | @Sse(schema?, options?) | Stream a Server-Sent Events (text/event-stream) response | | @Security(scheme, scopes?) | Require a named auth scheme — class or method, stackable = OR | | @Use(...middleware) | Run Express middleware before the handler — class or method | | @Summary(text) | Operation summary in swagger | | @Description(text) | Operation description (longer prose) in swagger | | @OperationId(id) | Operation id (defaults to the handler method name) | | @Deprecated() | Marks the operation deprecated in swagger | | @Example(value, status?) | Example for the request body, or a response (status) — stackable |

Parameter decorators

| Decorator | Injects | | ------------------- | ------------------------------------------------- | | @Param(name) | req.params[name] | | @QueryParam(name) | req.query[name] | | @BodyParam(name?) | the whole validated body, or one field of it | | @File(name) | an uploaded file as a web File (multipart) | | @Files(name) | uploaded files as File[] (multipart) | | @Header(name) | req.headers[name] | | @Principal() | the principal from the @Security handler | | @Req() / @Res() | raw Express Request / Response (escape hatch) |

To return a file or binary stream instead of JSON, pair @ReturnsFile with a FileResponse return value — covenix streams the body and sets Content-Type/Content-Disposition (plus optional disposition/headers). For HTTP Range / partial content, return a RangeFileResponse instead. See File downloads.

To receive files, put a z.file() (or z.array(z.file())) field in a @Body schema: covenix auto-detects the route as multipart/form-data, parses it with multer, and injects each file via @File / @Files. See File uploads.

To protect a route, register named schemes on the instance (new Covenix({ security: { bearerAuth: bearer(handler) } })) and mark routes with @Security('bearerAuth', scopes?); the handler's result is injected via @Principal(). See Authentication.

For arbitrary Express middleware (rate limiting, caching, logging, custom auth), use @Use(...middleware) on a method or controller. covenix runs the chain @Security → @Use → multipart → handler; class-level @Use runs before method-level, and middleware that sends a response short-circuits the handler.


Validation

Each route gets validation middleware generated from its schemas:

| Source | Validated against | Failure status | | -------------- | ----------------- | -------------- | | req.params | @Params schema | 400 | | req.query | @Query schema | 400 | | req.body | @Body schema | 422 | | handler return | @Returns schema | 500 |

On success, the original request values are replaced with the parsed (coerced, defaulted) output so handlers always receive clean data. Responses are validated the same way: the handler's return value is checked against the matching @Returns schema, and a mismatch always throws a ValidationError through next(err) — exactly like a request failure, in every environment. covenix never decides what to do about it; your error middleware does (log it and respond 500, swallow it, or surface the drift however you like).

Errors flow through Express

covenix never sends an error response itself. A failed validation calls next(err) with a ValidationError that carries the Zod issues and a status (400 for params/query, 422 for body, 500 for a response that doesn't match its @Returns schema), so it travels the same Express error pipeline as anything your handlers throw. ValidationError and SecurityError both extend CovenixError (carrying .status), so you stay in control of the response shape via your own error middleware:

import { CovenixError } from 'covenix';

app.use((err, req, res, next) => {
  if (err instanceof CovenixError) {
    return res.status(err.status).json({ status: err.status, message: err.message });
  }
  next(err); // your http-errors / fallback handling
});

If you don't want to write that, covenix ships an optional covenixErrorHandler that renders errors as RFC 9457 Problem Details (application/problem+json) — the standard error shape, overridable via formatError:

import { covenixErrorHandler } from 'covenix';

app.use(covenixErrorHandler());
// 422 → { type: 'about:blank', title: 'Unprocessable Entity', status: 422,
//         errors: [{ path: ['name'], message: '...' }] }

OpenAPI / Swagger

Name your top-level schemas so they become reusable components:

const UserSchema = z
  .object({
    /* ... */
  })
  .meta({ id: 'User' });
//   → referenced as #/components/schemas/User in swagger

Anonymous inline schemas are allowed but produce inlined swagger (no $ref).

The same Covenix instance you registered controllers on generates the spec — no need to list controllers a second time:

app.get('/swagger.json', (_req, res) => res.json(api.swagger()));

Or mount a browsable docs UI in one line with api.serveDocs(app) (Scalar by default; { ui: 'swagger-ui' | 'redoc' }). The UI is self-hosted from an optional peer dependency, or { cdn: true } for no install. See OpenAPI / Swagger.

api.swagger() builds the OpenAPI document from the registered controllers' metadata. It doesn't depend on routes being mounted, so for CI or frontend client generation a generate-swagger script can register controllers and call api.swagger() headlessly, writing swagger.json to disk without starting the server.

Static generation — no instances required

Swagger is derived entirely from class-level metadata, so if you only need the spec you don't have to construct controllers (or their dependencies) at all. generateSwagger(controllers) takes the controller classes directly and returns the OpenAPI document:

// generate-swagger-static.ts
import { writeFile } from 'node:fs/promises';
import { generateSwagger } from 'covenix';
import { HealthController } from './controllers/HealthController.js';
import { UsersController } from './controllers/UsersController.js';
import { AuthController } from './controllers/AuthController.js';

const swagger = generateSwagger([HealthController, UsersController, AuthController]);
await writeFile('swagger.json', JSON.stringify(swagger, null, 2));

This is the lightest path for CI spec checks and client codegen: no Covenix instance, no service wiring, no Express — just the classes and their decorators. Use api.swagger() instead when you already have a configured instance on hand (e.g. serving the spec from the running app).

Spec version: 3.1 by default

covenix emits OpenAPI 3.1.0 — its native form, since Zod 4's z.toJSONSchema() is JSON Schema draft 2020-12 (what 3.1 uses). For tooling with only partial 3.1 support (e.g. openapi-generator's typescript-fetch), pass specVersion: '3.0' to down-convert nullables, exclusive bounds, const, and binary annotations:

api.swagger({ specVersion: '3.0' });
generateSwagger([UsersController], info, { specVersion: '3.0' });

See OpenAPI / Swagger for the full conversion table.


Typed client

covenix can generate a standalone, fully-typed TypeScript client — the modern replacement for a tsoa → swagger → openapi-generator-cli pipeline. The generated file has no runtime dependency, so a front end imports it and calls your API with full types:

import { generateContract, generateTypeScriptClient } from 'covenix';

// controllers → contract (a codegen-oriented IR) → standalone client
const contract = generateContract([UsersController, AuthController]); // or api.contract()
await writeFile('api.gen.ts', generateTypeScriptClient(contract));
import { createClient, CovenixClientError } from './api.gen'; // no covenix import

const api = createClient({ baseUrl: 'https://api.example.com' });

const user = await api.users.get({ params: { id } }); // → User (throws on non-2xx)
const res = await api.users.get.raw({ params: { id } }); // → { status, body } union
for await (const event of await api.health.events()) {
  /* @Sse → AsyncIterable */
}

Operations are grouped by tag; methods return the success body and throw a typed CovenixClientError on non-2xx (use .raw() for the status-discriminated union). File responses come back as Blob (with HTTP Range), SSE as an AsyncIterable. See Typed Client.


Migrating

Coming from another tool? See the migration guides:


License

MIT © Joe Ferner