Nest + TypeORM
Kavo's engine (@kavo/core) is ORM-agnostic. It talks to your data through a small adapter seam. @kavo/nest generates the routes, and @kavo/typeorm adapts Kavo to a TypeORM DataSource. Below is the complete, minimal wiring for that combination.
If you haven't yet, read Introduction first. This page assumes you already know what @Kavo() does and just needs the app wiring.
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/typeorm expects typeorm (^0.3.20 || ^1.0.0) as a peer. Add it to the command above if your app doesn't already have it. @kavo/nest expects the Nest runtime your app already has. See Peer dependencies for the full list with versions, and Requirements for the Node and TypeScript prerequisites.
Zero-config wiring
A plain TypeORM entity:
// book.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class Book {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
}@Kavo(Book) needs no config object. That's zero-config: the full CRUD surface for free.
// 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 {}// app.module.ts
import { Module } from "@nestjs/common";
import { KavoModule } from "@kavo/nest";
import { createInfrastructure } from "@kavo/typeorm";
import { DataSource } from "typeorm";
import { Book } from "./book.entity.js";
import { BookController } from "./book.controller.js";
const dataSource = await new DataSource({
type: "postgres",
url: process.env.DATABASE_URL,
entities: [Book],
}).initialize();
@Module({
imports: [
KavoModule.forRoot({
infrastructure: createInfrastructure(dataSource),
}),
],
controllers: [BookController],
})
export class AppModule {}createInfrastructure(dataSource) derives both Kavo's entity metadata and its repository adapter from the DataSource's own TypeORM metadata. Nothing to declare twice.
Virtual fields
A @VirtualColumn is a real TypeORM column with no backing storage — its query function is the same alias-parameterized SQL fragment TypeORM itself uses to populate the property on load. Kavo reads it off the entity's metadata and inlines it into WHERE/ORDER BY too, so it works like any other column once opted in:
// book.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, VirtualColumn } from "typeorm";
@Entity()
export class Book {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@VirtualColumn({ query: (alias) => `LOWER(${alias}.title)` })
titleLower!: string;
}@Kavo(Book, {
filter: { fields: ["titleLower"] },
sort: { fields: ["titleLower"] },
select: { fields: ["id", "title", "titleLower"] },
})GET /books?filter[titleLower][eq]=dune&sort=titleLowerA derived field is opt-in to filter.fields/sort.fields/select.fields, the same rule a relation follows — leave it off all three and it never appears, is never filterable, and is never sortable. SELECT needs no extra config beyond select.fields: since it's a real TypeORM column, ordinary entity hydration already includes it.
The other way: a plain class getter
TypeORM's QueryBuilder hands back real entity class instances, not plain objects, so an ordinary JavaScript getter also reaches the response, with no decorator and no ORM involvement at all:
// book.entity.ts
@Entity()
export class Book {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column()
year!: number;
get displayTitle(): string {
return `${this.title} (${this.year})`;
}
}A getter carries no FieldMetadata — Kavo's metadata seam only ever sees @Column/@VirtualColumn, so there is nothing to opt into filter.fields/sort.fields/select.fields and no way to filter or sort on it. It reaches a response only through a registered schema that names it:
class BookItemSchema {
id = 0;
title = "";
displayTitle = ""; // the initializer's value is never used — see below
}
@Kavo(Book, { schema: { output: { item: BookItemSchema } } })The schema's own displayTitle = "" initializer only registers the key: DefaultSerializer reads the value straight off the real Book instance at response time (source.displayTitle), which is what invokes the getter. Leave displayTitle off the schema and it never appears — with no schema at all, the entity-derived default projection is metadata.fields only, and a getter is never in metadata.fields.
Reach for @VirtualColumn when the value needs to be filterable/sortable, or you want it without hand-writing an item/list schema. Reach for a plain getter when it's genuinely response-only and you already have (or want) an explicit schema. See Virtual fields for the full picture (including a correlated-subquery example for a relation count) and ADR-0050 for the design.
A complete, runnable app using all of the above lives in examples/nest-typeorm.