@void-snippets/nestjs
v0.3.2
Published
Typed NestJS REST resources from one config object — defineResource() gives you CRUD, filters, sort, search, offset+cursor pagination, soft delete, tenant scoping, route toggles and per-route guards over TypeORM or Mongoose
Maintainers
Readme
@void-snippets/nestjs
Describe a REST resource once, in one typed config object, and get a full NestJS controller + service + repository — CRUD, pagination, filtering, sorting, search, soft delete, tenant scoping — without writing the boilerplate by hand.
Works over TypeORM or Mongoose. You never import either one into this package; you point it at the repository/model you already have.
pnpm add @void-snippets/nestjs @void-snippets/core
# plus whichever ORM you use:
pnpm add @nestjs/typeorm typeorm # SQL
# or
pnpm add @nestjs/mongoose mongoose # MongoDB
# only if you validate request bodies with DTOs:
pnpm add class-validator class-transformerTable of contents
- The 60-second version
- What you get
- Mental model — three levels of control
- Level 1 — zero classes with
forFeature - Level 2 — configure the resource
- Level 3 — add your own logic
- Validating request bodies (DTOs)
- Filtering, sorting, search, pagination — the query API
- Turning routes on and off
- Per-route guards
- App-wide defaults with
forRoot - The response envelope
- Entity / document types
- Mongoose setup
- Full API reference
- Upgrading from 0.1
The 60-second version
You have an entity. You want REST endpoints for it. Here's the whole thing:
// contact.entity.ts
import { Entity, Column } from "typeorm";
import { VSBaseEntity } from "@void-snippets/nestjs/typeorm"; // gives id + timestamps
@Entity()
export class Contact extends VSBaseEntity {
@Column() name!: string;
@Column() email!: string;
}// contacts.module.ts
import { Module } from "@nestjs/common";
import { forTypeOrmResource } from "@void-snippets/nestjs/typeorm";
import { Contact } from "./contact.entity";
@Module({
imports: [forTypeOrmResource(Contact)], // name "contacts" and id "id" are derived
})
export class ContactsModule {}That's it. You now have these live endpoints:
| Method & path | What it does |
| ------------------------- | ------------------------------------------ |
| GET /contacts | Paginated list |
| GET /contacts/count | Count matching rows |
| GET /contacts/:id | One item |
| POST /contacts | Create |
| POST /contacts/bulk | Create many |
| PATCH /contacts/:id | Update |
| PATCH /contacts/:id/restore | Restore a soft-deleted row (if enabled) |
| DELETE /contacts/:id | Delete |
| DELETE /contacts/bulk | Delete many (body: { "ids": [...] }) |
No ContactsController, no ContactsService, no ContactsRepository classes to write.
Every defineResource option (filters, hooks, scoping, route toggles…) can be
passed inline as the second argument:
forTypeOrmResource(Contact, {
softDelete: "deletedAt",
query: { search: ["name", "email"] },
routes: { remove: false },
});What you get
- One config object (
defineResource) is the single source of truth. - Type safety end to end, with zero manual generics — pass
entityand DTO classes and every type is inferred: the id's value type, hook parameters, filter/sort/search field names. A typo is a compile error, not a silently-ignored param. - Offset and cursor pagination, switchable per resource.
- Filtering DSL with a per-field operator allowlist (safe by default).
- Soft delete + restore, optimistic locking, sparse fieldsets
(
?fields=). - First-class tenant scoping —
scope: { field: "orgId", value: (ctx) => ctx.orgId }fences every operation, failing closed by default. - Twelve lifecycle hooks — from
beforeCreatetobeforeList/afterList,afterGet, and aserializeresponse shaper — inline or as subclass methods. - Route toggles + per-route customization — read-only resources, custom paths, HTTP status codes, pipes, per-route DTOs, and arbitrary method decorators (Swagger, throttling…) without overriding anything.
- Guards at both levels — on the whole controller or per route, typed.
- A pluggable response envelope (default
{ data: ... }), set once.
Mental model — three levels of control
Everything is progressive. Start at Level 1; drop down only when you need to.
| Level | You write | Use it when |
| ----- | -------------------------------------------- | ---------------------------------------- |
| 1 | defineResource + forFeature | Plain CRUD. No custom behaviour. |
| 2 | ...plus config (filters, routes, guards…) | You need to shape the endpoints. |
| 3 | ...plus extends ResourceService(def) | You need real logic (side effects, deps). |
You never lose access to the lower levels — Level 3 is just Level 1 with a class.
Level 1 — zero classes with forFeature
This is the 60-second version above. forTypeOrmResource
(and forMongooseResource) build the repository, service, and controller for you
and wire them together:
import { forTypeOrmResource } from "@void-snippets/nestjs/typeorm";
// inline: imports: [forTypeOrmResource(Contact)]
// imports: [forTypeOrmResource(Contact, { ...any defineResource option })]
// definition: imports: [forTypeOrmResource(contactsResource, Contact)]
// options: forTypeOrmResource(Contact, { dataSource: "reporting", imports: [...] })
import { forMongooseResource } from "@void-snippets/nestjs/mongoose";
// inline: imports: [forMongooseResource({ name: "Contact", schema: ContactSchema })]
// definition: imports: [forMongooseResource(contactsResource, { name: "Contact", schema: ContactSchema })]In the inline form name defaults to the pluralised class/model name
("Contact" → /contacts, "Category" → /categories) and id to "id" for
TypeORM / "_id" for Mongoose. Reach for a separate defineResource when the
definition is shared — e.g. a controller subclass also references it.
Under the hood these call VoidResourceModule.forFeature, which is also usable
directly when you need full control over the wiring:
import { VoidResourceModule } from "@void-snippets/nestjs";
import { TypeOrmModule, getRepositoryToken } from "@nestjs/typeorm";
VoidResourceModule.forFeature({
definition: contactsResource,
imports: [TypeOrmModule.forFeature([Contact])], // provides the handle in this scope
repository: getRepositoryToken(Contact), // the DI token to wrap
});Why the token indirection? The core module never depends on
@nestjs/typeorm/@nestjs/mongoose. The/typeormand/mongoosesubpath helpers do that wiring for you;forFeaturestays ORM-agnostic underneath.
Level 2 — configure the resource
Everything is a field on the defineResource config. Here's a fuller example
with comments:
export const contactsResource = defineResource({
entity: Contact, // ← gives the config (and everything built from it) its types
name: "contacts",
id: "id", // its VALUE type (number here) becomes the id type everywhere
// Pagination
pagination: {
mode: "both", // "offset" | "cursor" | "both"
defaultLimit: 20, // page size when ?limit= is omitted
maxLimit: 100, // hard ceiling on ?limit=
},
// What clients may filter / sort / search / select on
query: {
filters: defineFilters<Contact>({
email: filter.like(), // ?email[like]=acme
createdAt: filter.dateRange(), // ?createdAt[gte]=2026-01-01
}),
sort: { allowedFields: ["name", "createdAt"], default: "-createdAt" },
search: ["name", "email"], // ?q=john
select: ["id", "name", "email"], // ?fields=id,name
},
softDelete: "deletedAt", // enables soft delete + /restore
optimisticLock: "version", // 409 on stale updates
// Multi-tenancy in one line — ANDed into EVERY operation. A request without
// an orgId gets a 403 (fail closed; set onMissing: "skip" to allow).
scope: { field: "orgId", value: (ctx) => ctx.orgId },
guards: [JwtAuthGuard], // applied to every route (class-level)
routes: { remove: false }, // no DELETE for this resource
});Every key is optional. sort, search, and select accept either a plain
array of field names or a full spec object if you need more control — and every
field name (including the sort default) is checked against the entity at
compile time.
Level 3 — add your own logic
When you need side effects, extra injected services, or custom endpoints, stop
using forFeature and write thin classes that extend the generated bases.
Everything stays typed to your entity.
// contacts.repository.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { ResourceRepository } from "@void-snippets/nestjs/advanced";
import { Contact } from "./contact.entity";
@Injectable()
export class ContactsRepository extends ResourceRepository<Contact, number> {
constructor(@InjectRepository(Contact) repo: Repository<Contact>) {
super(repo);
}
}// contacts.service.ts
import { Injectable } from "@nestjs/common";
import { ResourceService, type RequestContext } from "@void-snippets/nestjs";
import { contactsResource } from "./contacts.resource";
import { ContactsRepository } from "./contacts.repository";
import { MailerService } from "../mailer/mailer.service";
@Injectable()
export class ContactsService extends ResourceService(contactsResource) {
constructor(repo: ContactsRepository, private mailer: MailerService) {
super(repo);
}
// Hooks run around the built-in CRUD. All are optional. `dto` and `entity`
// are fully typed — no casting needed.
protected override async beforeCreate(dto: Contact, ctx: RequestContext) {
return { ...dto, orgId: ctx.orgId }; // enrich the payload before insert
}
protected override async afterCreate(entity: Contact) {
await this.mailer.sendWelcome(entity.email);
}
}// contacts.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import { ResourceController } from "@void-snippets/nestjs";
import { contactsResource } from "./contacts.resource";
import { ContactsService } from "./contacts.service";
// Put @Controller(name) on your subclass. It sets the route path AND — because
// a class needs at least one decorator for TypeScript to emit constructor
// metadata (emitDecoratorMetadata) — it's what lets Nest inject your service.
@Controller("contacts")
export class ContactsController extends ResourceController(contactsResource) {
constructor(service: ContactsService) {
super(service);
}
// All the generated routes are inherited. Add your own freely — `this.service`
// is the generated service, fully typed:
@Get("recent")
@UseGuards(AdminGuard)
recent() {
return this.service.list({ sort: "-createdAt", limit: 5 });
}
}Why
@Controlleris required here. In a hand-wired module, Nest injects your constructor fromdesign:paramtypes, which TypeScript only emits for a class that carries a decorator.@Injectable()on the service and@Controller(name)on the controller provide it. (TheforTypeOrmResource/forFeaturepath doesn't need this — it injects by explicit token internally.)
// contacts.module.ts — classic NestJS wiring
@Module({
imports: [TypeOrmModule.forFeature([Contact])],
controllers: [ContactsController],
providers: [ContactsRepository, ContactsService],
})
export class ContactsModule {}Prefer hooks without a subclass? You can also pass
hooks: { beforeCreate, afterCreate, scope, ... }directly indefineResourceand keep usingforFeature. A subclass method always wins over an inline hook of the same name.
The full hook set
All hooks exist both as inline hooks: { ... } entries and as protected
override methods on a service subclass. All are optional; every parameter is
typed from your entity/DTOs.
| Hook | Runs on | Return to transform |
| --------------------------- | -------------------- | ------------------------------ |
| beforeCreate(dto, ctx) | create, createMany | replacement dto |
| afterCreate(entity, ctx) | create, createMany | — |
| beforeUpdate(id, dto, ctx)| update | replacement dto |
| afterUpdate(entity, ctx) | update | — |
| beforeDelete(entity, ctx) | remove, removeMany | — |
| afterDelete(entity, ctx) | remove, removeMany | — |
| afterRestore(entity, ctx) | restore | — |
| beforeList(params, ctx) | list and count | replacement query params |
| afterList(payload, ctx) | list | replacement payload |
| beforeGet(id, ctx) | get | — |
| afterGet(entity, ctx) | get | replacement entity |
| serialize(entity, ctx) | every response entity| the wire representation |
| scope(ctx) | every operation | extra FilterCondition[] |
serialize is the response shaper: it runs last, on every entity leaving
the API (list items, get, create, update, restore, createMany), and never on
internal reads or the entities other hooks receive:
hooks: {
serialize: (contact) => ({ id: contact.id, name: contact.name }), // drop internals
}Validating request bodies (DTOs)
By default the create/update body is accepted as-is. To validate it, give the
resource DTO classes. They're validated with class-validator and become the
real request types.
import { IsEmail, IsString, IsOptional } from "class-validator";
import { VSCreateInput } from "@void-snippets/nestjs";
// `implements VSCreateInput<Contact>` keeps the DTO in sync with the entity:
// it must cover exactly the fields the server doesn't manage.
export class CreateContactDto implements VSCreateInput<Contact> {
@IsString() name!: string;
@IsEmail() email!: string;
}
export class UpdateContactDto {
@IsOptional() @IsString() name?: string;
@IsOptional() @IsEmail() email?: string;
}defineResource({
entity: Contact,
name: "contacts",
id: "id",
dto: { create: CreateContactDto, update: UpdateContactDto },
});Now POST /contacts with an invalid email returns 400 automatically, and
unknown fields are stripped. No @Body() or ValidationPipe wiring on your side.
The DTO classes also become the request types everywhere: beforeCreate(dto)
receives a CreateContactDto, service.update() expects an
UpdateContactDto — inferred, no generics. A single route can override the
resource-level DTO with routes: { update: { dto: PatchContactDto } }.
Filtering, sorting, search, pagination — the query API
These are the query-string parameters your list endpoints understand. What's
allowed is whatever you declared in query.
Filtering — ?field[op]=value
GET /contacts?email[like]=acme
GET /contacts?age[gte]=18&age[lte]=65
GET /contacts?status[in]=active,trial
GET /contacts?deletedAt[isNull]=true
GET /contacts?status=active ← shorthand for status[eq]=activeYou declare which fields and operators are allowed with defineFilters + the
filter builders:
import { defineFilters, filter } from "@void-snippets/nestjs";
filters: defineFilters<Contact>({
name: filter.like(), // like, ilike, contains, eq
age: filter.range(), // gt, gte, lt, lte, between, eq (coerced to number)
createdAt: filter.dateRange(), // same ops, coerced to Date
isActive: filter.bool(), // eq, ne (coerced to boolean)
status: filter.exact(), // eq, ne, in, nin
deletedAt: filter.nullable(), // isNull, eq, ne
});A filter on a field you didn't declare is ignored. An operator you didn't
allow returns 400. And because defineFilters<Contact> is typed, writing a
field name that isn't on Contact won't even compile:
defineFilters<Contact>({ emial: filter.like() });
// ^^^^^ compile error — did you mean "email"?Sorting — ?sort=
GET /contacts?sort=name → name ASC
GET /contacts?sort=-createdAt → createdAt DESC (leading "-" = descending)
GET /contacts?sort=-priority,namesort: ["name", "createdAt", "priority"]; // allowlist; or { allowedFields, default }Search — ?q=
GET /contacts?q=john → case-insensitive match across the configured fieldssearch: ["name", "email"]; // or { fields, mode: "contains" | "prefix" }Sparse fieldsets — ?fields=
GET /contacts?fields=id,name → only these fields are fetched and returnedselect: ["id", "name", "email"]; // allowlist for ?fields=Pagination
Offset (classic pages):
GET /contacts?page=2&limit=20{ "data": { "items": [...], "page": 2, "limit": 20, "totalPages": 5, "totalDocuments": 92 } }Cursor (infinite scroll / big tables — stays fast at any depth):
GET /contacts?limit=20 ← first page
GET /contacts?limit=20&cursor=<nextCursor> ← next page{ "data": { "items": [...], "limit": 20, "nextCursor": "…", "prevCursor": null, "hasNextPage": true } }Pick per resource with pagination.mode: "offset", "cursor", or "both"
(with "both", sending ?page= gives offset, otherwise you get cursor).
Tip: For production cursors, set a
cursorSecret(per resource or globally viaforRoot). It signs each cursor so clients can't tamper with it.
Turning routes on and off
Every route is on by default. Turn things off in routes:
routes: {
readOnly: true, // shorthand: only list, get, count survive
}routes: {
remove: false, // no DELETE /:id
bulk: false, // no POST /bulk and no DELETE /bulk
restore: false, // no /:id/restore
}A disabled route is never registered — it 404s, it isn't a hidden method that throws.
Guards & per-route customization
Guards (and interceptors) attach at two levels — the whole controller, or an individual route — without overriding any method:
import { JwtAuthGuard } from "../auth/jwt.guard";
defineResource({
entity: Contact,
name: "contacts",
guards: [JwtAuthGuard], // ← every route
routes: {
remove: { guards: [AdminGuard] }, // ← stacks on top for DELETE only
},
});Each route entry accepts the full option set:
routes: {
create: {
guards: [JwtAuthGuard], // @UseGuards on this route
interceptors: [AuditInterceptor], // @UseInterceptors
pipes: [TrimPipe], // @UsePipes
httpCode: 202, // @HttpCode — e.g. 202 Accepted
path: "submit", // POST /contacts/submit instead of POST /contacts
dto: SubmitContactDto, // per-route body DTO override
decorators: [ApiOperation({ summary: "Create a contact" })], // any method decorator
},
}decorators is the escape hatch: Swagger annotations, @Throttle,
@CacheTTL — anything that decorates a method lands on the generated handler.
App-wide defaults with forRoot
Set things once at the root instead of repeating them in every resource. Import
forRoot a single time in your AppModule:
@Module({
imports: [
VoidResourceModule.forRoot({
cursorSecret: process.env.CURSOR_SECRET, // signs all cursors
maxLimit: 200, // global ceiling on ?limit=
defaultLimit: 25,
paginationMode: "both",
envelope: (data) => ({ success: true, data }), // house response shape
}),
ContactsModule,
// ...other feature modules
],
})
export class AppModule {}Any value a resource sets in its own defineResource config wins over the
global default.
The response envelope
Every successful response is wrapped by an envelope function. The default is:
(data) => ({ data });So a single item comes back as { "data": { ...entity } } and a list as
{ "data": { "items": [...], "page": 1, ... } }. This is the exact shape the
@void-snippets/react and @void-snippets/angular clients consume with zero
configuration.
To change it, set envelope — globally in forRoot, or per resource in
defineResource:
envelope: (data) => ({ ok: true, result: data, ts: Date.now() });Entity / document types
Helpers that describe what your ORM actually returns, so your types are honest.
TypeORM — base entity classes (import from @void-snippets/nestjs/typeorm):
import { VSBaseEntity, VSBaseUuidEntity, VSBaseVersionedEntity } from "@void-snippets/nestjs/typeorm";
class Contact extends VSBaseEntity {} // id: number + createdAt + updatedAt
class User extends VSBaseUuidEntity {} // id: string (uuid) + timestamps
class Order extends VSBaseVersionedEntity {} // + version column for optimistic lockMongoose — see the Mongoose section for createBaseSchema /
VSDocOf.
Derived input types — write the entity once, keep DTO types in lockstep:
type ContactCreate = VSCreateInput<Contact>; // entity minus id/timestamps/version/deletedAt
type ContactUpdate = VSUpdateInput<Contact>; // Partial of the aboveRelations — Ref<T> is "id when lean, document when populated";
VSPopulated flips the populated paths to their real type:
class Post {
author!: Ref<User>; // string | User
tags!: Ref<Tag>[];
}
type PostWithAuthor = VSPopulated<Post, "author">; // author is now UserMongoose setup
Same config — only the wiring differs. Build a schema, register the model, and
point forFeature at the model token.
// contact.schema.ts
import { createBaseSchema, type VSDocOf } from "@void-snippets/nestjs/mongoose";
export const ContactSchema = createBaseSchema(
{
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
},
{ softDelete: true }, // adds an indexed deletedAt field
);
// The honest lean type: your fields + _id + createdAt + updatedAt + deletedAt
export type Contact = VSDocOf<typeof ContactSchema, { softDelete: true }>;// contacts.module.ts
import { Module } from "@nestjs/common";
import { forMongooseResource } from "@void-snippets/nestjs/mongoose";
@Module({
imports: [
forMongooseResource(
{ name: "Contact", schema: ContactSchema }, // schema is typed → T = Contact
{ softDelete: "deletedAt", query: { search: ["name", "email"] } },
),
],
})
export class ContactsModule {}name derives to /contacts and id defaults to "_id". A shared
defineResource<Contact>({ name: "contacts", ... }) definition plus
forMongooseResource(definition, { name, schema }) works too.
populate is available for Mongoose resources:
defineResource<Post>({ name: "posts", populate: ["author", "tags"] });Full API reference
defineResource(config)
The one config object. Returns a frozen definition consumed by the factories
and the module. All generics are inferred from the config — T from entity,
the id's value type from id, TCreate/TUpdate from dto. Key config fields:
| Field | Type | Notes |
| ---------------- | --------------------------------------------------- | ------------------------------------------------- |
| name | string | REST base path + default error name. |
| entity | Type<T> | Entity class — the source of all typing. |
| id | keyof T | Primary key. Defaults to _id, else id. |
| resourceName | string | Singular name for 404/409 messages. |
| dto | { create?, update? } | class-validator DTO classes → become TCreate/TUpdate. |
| pagination | { mode?, defaultLimit?, maxLimit?, cursorField? } | mode: offset | cursor | both. |
| query | { filters?, sort?, search?, select? } | Allowlists. sort/search/select accept arrays. |
| softDelete | keyof T | Field to mark deleted; enables /restore. |
| optimisticLock | keyof T | Numeric version field → 409 on stale writes. |
| maxBulk | number | Max items per POST /bulk / DELETE /bulk. Default 500. |
| populate | string \| string[] \| PopulateOptions | Mongoose only. |
| cursorSecret | string | Signs cursors (overrides global). |
| envelope | (data) => unknown | Response shape (overrides global). |
| guards | VSGuard[] | Controller-level guards (every route). |
| interceptors | VSInterceptor[] | Controller-level interceptors. |
| scope | { field, value(ctx), onMissing? } | First-class tenant scoping; fails closed. |
| routes | VSRoutesConfig | Enable/disable + per-route options. |
| hooks | VSResourceHooks | Inline lifecycle hooks + raw scope. |
ResourceService(definition) → class
A service base class configured from the definition. extends it to add logic.
Overridable hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate,
beforeDelete, afterDelete, afterRestore, beforeList, afterList,
beforeGet, afterGet, serialize, and scope(ctx) for tenant isolation.
See the full hook set.
ResourceController(definition) → class
A controller base class exposing the enabled routes. extends it to add custom
routes. @Resource(definition) is the decorator equivalent for annotating your
own class.
@Resource(definition) decorator
Decorator form of ResourceController. Apply it to your own controller class;
the class must expose the injected service on a property named service
(the generated route handlers call this.service):
@Resource(contactsResource)
export class ContactsController {
constructor(readonly service: ContactsService) {}
}VoidResourceModule and the ORM helpers
VoidResourceModule.forRoot(options)— app-wide defaults. Import once.VoidResourceModule.forFeature({ definition, imports, repository })— the ORM-agnostic registration primitive.forTypeOrmResource(Entity, config?)— from@void-snippets/nestjs/typeorm. The one-liner most TypeORM apps want;configtakes every defineResource option inline plus{ dataSource?, imports? }. Also accepts the definition-first formforTypeOrmResource(definition, Entity, options?).forMongooseResource({ name, schema }, config?)— from@void-snippets/nestjs/mongoose; same inline config plus{ connectionName?, imports? }. Also acceptsforMongooseResource(definition, { name, schema }, options?).
Query DSL (re-exported from @void-snippets/nestjs)
defineFilters, filter (.like, .range, .dateRange, .bool, .exact,
.nullable), defineSort, defineSearch, defineSelect — all also available
from @void-snippets/core.
Unsigned cursors. If a resource uses cursor pagination without a
cursorSecret(per resource or viaforRoot), it logs a one-time dev warning — unsigned cursors are tamperable. Set a secret in production.
The @Ctx() decorator & RequestContext
Every hook receives a RequestContext — { user?, orgId?, traceId, request? }.
user comes from your auth guard (req.user); orgId from the x-org-id
header; traceId from x-request-id/x-trace-id or auto-generated. Use it in
scope() for multi-tenancy:
protected override scope(ctx: RequestContext): FilterCondition<Contact>[] {
return ctx.orgId ? [{ field: "orgId", op: "eq", value: ctx.orgId }] : [];
}field is constrained to keyof Contact, so a mistyped column is a compile
error. A row outside the scope behaves exactly like one that doesn't exist
(404), so cross-tenant ids can't be probed.
The /advanced subpath
The low-level engine the high-level API is built on lives behind
@void-snippets/nestjs/advanced: ResourceNestService,
ResourceNestController, ResourceNestRepository, ResourceRepository, the
cursor codec, and the repository query types. Reach for it only when you're
hand-writing a service against the engine or building a custom ORM adapter.
Upgrading
0.2 → 0.3
defineResourcegenerics changed — stop passing them. Where you wrotedefineResource<Contact, number>({ name, id: "id" }), writedefineResource({ entity: Contact, name, id: "id" }); the id's value type is read off the entity. (If you must pass generics, the second parameter is now the id key —defineResource<Contact, "id">— not the id type.)- Low-level engine moved — import
ResourceNestService,ResourceNestController,ResourceRepository, and the cursor codec from@void-snippets/nestjs/advancedinstead of the main entry. - ORM one-liners flipped — the primary form is now
forTypeOrmResource(Entity, config?)/forMongooseResource(model, config?). The definition-first form still works unchanged. - New (non-breaking): first-class
scope, controller-levelguards/interceptors,beforeList/afterList/beforeGet/afterGet/serializehooks, and per-routepath/httpCode/pipes/dto/decorators.
0.1 → 0.2
0.2 added the defineResource + factory + module API and changed service
return values: list(), count(), and removeMany() return the raw
payload (e.g. { items, page, ... }), not a pre-wrapped { data: ... }. The
HTTP responses are unchanged — the controller applies the (now configurable)
envelope. If you called those service methods directly, drop the .data hop.
