Skip to content

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

bash
npm install covenix zod reflect-metadata express

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

TypeScript configuration

jsonc
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "module": "nodenext",
    "types": ["reflect-metadata"],
  },
}
  • "type": "module" is required in package.json (ESM).
  • emitDecoratorMetadata is not required — covenix uses explicit parameter decorators, so it never needs runtime type metadata.
  • Import reflect-metadata once at your app's entry point.

Define a controller

typescript
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

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

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