Getting started
Kavo turns your ORM entities into a full REST CRUD API. Define the entity once, add one decorator, and you get create, read, update, delete, filtering, sorting, pagination, nested includes, and field selection — with no hand-written controller methods.
Today Kavo supports NestJS as the framework, over TypeORM, Prisma, Mongoose, or MikroORM as the ORM. This guide uses Nest + TypeORM as its example stack; see Nest + Prisma, Nest + Mongoose, and Nest + MikroORM for the equivalents.
Requirements
- Node.js 20 or newer — the minimum this project declares. Note that the published packages carry no
enginesfield, so your package manager will not warn you on an older release. - ESM — every
@kavo/*package ships as ESM only, with no CommonJS entry point. Your app must be ESM too ("type": "module"in itspackage.json), which the defaultnest newscaffold is not. - Decorator metadata —
experimentalDecoratorsandemitDecoratorMetadatamust be on.@Kavo()reads your entity's decorator metadata, and so do TypeORM's columns and Nest's DI.
A tsconfig.json that satisfies all three:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"strict": true,
"skipLibCheck": true
}
}useDefineForClassFields: false is load-bearing at ES2022 and above. With it on, every declared field is emitted as a real class field, so a fresh entity carries an own key for every column — set to undefined — whether the adapter hydrated it or not. A partially-hydrated entity then looks fully populated: Kavo projects a response with Object.keys(entity) when no field selection narrows it, so those columns surface in the body as undefined rather than being absent, and TypeORM's persistence diffing reads them as explicit values. With it off, only fields with an initializer are assigned and the rest are left to the prototype — which is what Kavo's own packages and both example apps compile with.
Install
pnpm add @kavo/core @kavo/nest @kavo/typeormnpm install @kavo/core @kavo/nest @kavo/typeormyarn add @kavo/core @kavo/nest @kavo/typeormbun add @kavo/core @kavo/nest @kavo/typeorm@kavo/core— the engine.@kavo/nest— generates the NestJS routes.@kavo/typeorm— adapts Kavo to a TypeORMDataSource. See Nest + TypeORM for the full wiring.
Peer dependencies
Kavo never bundles your framework or your ORM. Each package declares what it expects as a peer — a Nest app already has most of them:
@kavo/core— none. It has zero runtime dependencies.@kavo/nest—@nestjs/commonand@nestjs/core(^10.0.0 || ^11.0.0),reflect-metadata(^0.1.13 || ^0.2.0),rxjs(^7.8.0).@kavo/typeorm—typeorm(^0.3.20 || ^1.0.0).@kavo/prisma—@prisma/client(^5.0.0 || ^6.0.0).@kavo/mongoose—mongoose(^7.0.0 || ^8.0.0).@kavo/mikroorm—@mikro-orm/core(^7.0.0), plus the MikroORM driver package your database needs.@kavo/graphql—graphql(^17.0.0).@kavo/mcp—@modelcontextprotocol/sdk(^1.0.0).
Three of @kavo/nest's own peers are declared optional — @nestjs/swagger (^8.0.0 || ^11.0.0) for generated OpenAPI docs, graphql (^17.0.0) for the GraphQL controller, and @modelcontextprotocol/sdk (^1.0.0) for the MCP controller — so nothing makes you configure a protocol you don't serve. They may still land in your dependency tree either way: @kavo/nest depends on @kavo/graphql and @kavo/mcp, and those two declare graphql and @modelcontextprotocol/sdk as required peers. npm, pnpm, and bun install required peers automatically, so both arrive even in a REST-only app; yarn does not, and reports them as unmet peers instead. Optional here means you never have to import or wire them.
GraphQL and MCP
The same entities can be served over GraphQL and the Model Context Protocol, through the same engine.
Inside a Nest app you never install the Kavo bindings directly — @kavo/nest depends on @kavo/graphql, and on @kavo/mcp as of the next release. Add only the protocol library you intend to use:
pnpm add graphql # GraphQL
pnpm add @modelcontextprotocol/sdk # MCPnpm install graphql # GraphQL
npm install @modelcontextprotocol/sdk # MCPyarn add graphql # GraphQL
yarn add @modelcontextprotocol/sdk # MCPbun add graphql # GraphQL
bun add @modelcontextprotocol/sdk # MCPBoth bindings are host-framework-agnostic, so they also work without Nest. Installed that way you add the Kavo package yourself, alongside @kavo/core and whichever ORM adapter you use:
pnpm add @kavo/core @kavo/graphql graphql
pnpm add @kavo/core @kavo/mcp @modelcontextprotocol/sdknpm install @kavo/core @kavo/graphql graphql
npm install @kavo/core @kavo/mcp @modelcontextprotocol/sdkyarn add @kavo/core @kavo/graphql graphql
yarn add @kavo/core @kavo/mcp @modelcontextprotocol/sdkbun add @kavo/core @kavo/graphql graphql
bun add @kavo/core @kavo/mcp @modelcontextprotocol/sdkZero-config @Kavo()
Given a plain TypeORM entity:
// book.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class Book {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column()
author!: string;
}put @Kavo(Book) on an empty Nest controller:
// book.controller.ts
import { Controller } from "@nestjs/common";
import { Kavo } from "@kavo/nest";
import { Book } from "./book.entity.js";
@Kavo(Book)
@Controller("books")
export class BookController {}That's it — no config object, no service, no repository wiring in the controller. This generates:
| Method | Route | What it does |
|---|---|---|
POST | /books | Create a book |
GET | /books | List books — filtering, sorting, pagination |
GET | /books/:id | Get one book |
PUT | /books/:id | Replace a book |
PATCH | /books/:id | Partially update a book |
DELETE | /books/:id | Delete a book |
Requests and responses are shaped straight from Book's own columns — there's no DTO to write until you want to narrow or reshape what's exposed. The list route (GET /books) already understands query-string filtering and sorting out of the box, for example:
GET /books?filter[author][eq]=Tolkien&sort=-title&limit=10&offset=0Wiring it into a Nest app
@Kavo-decorated controllers need one thing from the app: a KavoModule that hands them infrastructure (a DataSource, in the TypeORM case).
// app.module.ts
import { Module } from "@nestjs/common";
import { KavoModule } from "@kavo/nest";
import { createInfrastructure } from "@kavo/typeorm";
import { DataSource } from "typeorm";
import { BookController } from "./book.controller.js";
const dataSource = await new DataSource({/* ...your TypeORM connection options... */}).initialize();
@Module({
imports: [
KavoModule.forRoot({
infrastructure: createInfrastructure(dataSource),
}),
],
controllers: [BookController],
})
export class AppModule {}KavoModule discovers every @Kavo-decorated controller registered in controllers: [...] and binds each one's generated service — no per-entity registration step.
Soft delete
Give an entity a delete-marker column and Kavo stops actually deleting rows on DELETE /books/:id — it stamps the marker instead, and every read (GET /books, GET /books/:id, includes) automatically excludes stamped rows, with no query changes on your side:
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from "typeorm";
@Entity()
export class Book {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@DeleteDateColumn()
deletedAt!: Date | null;
}That column alone is enough for deleteOne to soft-delete and for reads to hide deleted rows. Two more capabilities are opt-in, one config line each, because each is a piece of public API worth stating on purpose rather than getting for free:
- Restore —
@Kavo(Book, { softDelete: { strategy: "soft" } })turns onPATCH /books/:id/restore, which clears the marker and returns the row again. - Purge —
@Kavo(Book, { operations: { purgeOne: true } })turns onDELETE /books/:id/purge, which permanently removes an already-soft-deleted row.
Both can be combined. Attempting to restore a row that isn't deleted, or purge one that is still live, returns a 409, not a silent no-op. Pass ?withDeleted=true on a read to opt back into seeing soft-deleted rows for that request. See Soft delete, restore & purge for the full behavior — unique-index caveats, cascades, and what's deliberately not built (bulk restore/purge).