Getting Started
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.
Installation
npm install covenix zod reflect-metadata expresscovenix requires Zod 4+ and TypeScript 5+ with experimental decorators.
TypeScript configuration
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "nodenext",
"types": ["reflect-metadata"],
},
}"type": "module"is required inpackage.json(ESM).emitDecoratorMetadatais not required — covenix uses explicit parameter decorators, so it never needs runtime type metadata.- Import
reflect-metadataonce at your app's entry point.
Define a controller
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)
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);
}
}Wire it up
// app.ts
import 'reflect-metadata';
import { Covenix, serve } from 'covenix';
import { UsersController } from './users.controller.js';
const api = new Covenix({ info: { title: 'My API', version: '1.0.0' } });
// You own construction — inject dependencies explicitly.
api.register(new UsersController(db));
// One call: builds an Express app (json body parser, mounted routes, docs,
// error handler) and starts listening. Returns the http.Server.
await serve(api, { port: 3000 });A single Covenix instance owns your controllers; serve is opt-in convenience that assembles a ready Express app and listens. The same instance also generates swagger.
Or own the Express app yourself
serve/toExpress are sugar, not a requirement. When you need full control, build the app and call mount directly:
import express from 'express';
import { Covenix, covenixErrorHandler } from 'covenix';
const app = express();
app.use(express.json());
const api = new Covenix({ info: { title: 'My API', version: '1.0.0' } });
api.register(new UsersController(db));
api.mount(app); // wires routes + validation
api.serveDocs(app); // optional docs UI
app.use(covenixErrorHandler());
app.listen(3000);toExpress returns the built app without listening — handy for supertest. See Route Handlers for what goes in a controller, then Validation & Errors, OpenAPI / Swagger, and the full API Reference.
