End-to-End Workflow
A full contacts resource, from entity to a working REST API, in about 15 lines.
#A full contacts resource, in a handful of lines
This walks through everything you need for a working /contacts REST API — entity, DTOs for validation, and the module that wires it all together. No hand-written controller, service, or repository.
#Step 1 — The entity
// contact.entity.ts
import { Column, Entity } from 'typeorm';
import { VSBaseEntity } from '@void-snippets/nestjs/typeorm';
@Entity('contacts')
export class Contact extends VSBaseEntity {
// id, createdAt, updatedAt come from VSBaseEntity for free
@Column() name!: string;
@Column() email!: string;
@Column({ default: 'active' }) status!: string;
@Column({ type: 'int' }) age!: number;
@Column({ type: 'datetime', nullable: true }) deletedAt!: Date | null;
}#Step 2 — DTOs (optional, but recommended)
Attach these and the generated controller validates request bodies automatically:
// contact.dto.ts
import { IsEmail, IsInt, IsOptional, IsString } from 'class-validator';
import type { VSCreateInput } from '@void-snippets/nestjs';
import type { Contact } from './contact.entity';
export class CreateContactDto implements VSCreateInput<Contact> {
@IsString() name!: string;
@IsEmail() email!: string;
@IsString() status!: string;
@IsInt() age!: number;
}
export class UpdateContactDto {
@IsOptional() @IsString() name?: string;
@IsOptional() @IsEmail() email?: string;
}#Step 3 — The resource definition
This is where filtering, sorting, search, soft delete, and DTOs all come together:
// contacts.resource.ts
import { defineResource, defineFilters, filter } from '@void-snippets/nestjs';
import { Contact } from './contact.entity';
import { CreateContactDto, UpdateContactDto } from './contact.dto';
export const contactsResource = defineResource({
entity: Contact,
name: 'contacts',
dto: { create: CreateContactDto, update: UpdateContactDto },
query: {
filters: defineFilters<Contact>({
name: filter.like(),
status: filter.exact(),
age: filter.range(),
}),
sort: ['id', 'name', 'age'],
search: ['name', 'email'],
},
softDelete: 'deletedAt',
});#Step 4 — The module
// contacts.module.ts
import { Module } from '@nestjs/common';
import { forTypeOrmResource } from '@void-snippets/nestjs/typeorm';
import { contactsResource } from './contacts.resource';
import { Contact } from './contact.entity';
@Module({
imports: [forTypeOrmResource(contactsResource, Contact)],
})
export class ContactsModule {}#Step 5 — Register it once, app-wide
// app.module.ts
import { Module } from '@nestjs/common';
import { VoidResourceModule } from '@void-snippets/nestjs';
import { ContactsModule } from './contacts/contacts.module';
@Module({
imports: [
VoidResourceModule.forRoot({
cursorSecret: process.env.CURSOR_SECRET,
maxLimit: 200,
defaultLimit: 25,
}),
ContactsModule,
],
})
export class AppModule {}#What you get, with zero more code
GET /contacts?page=1&limit=20 paginated list
GET /contacts?name[like]=Jo&age[gte]=18 filtering
GET /contacts?sort=-createdAt sorting
GET /contacts?q=engineer full-text search across name/email
GET /contacts/count?status=active count
GET /contacts/:id single item
POST /contacts create — body validated against CreateContactDto
PATCH /contacts/:id update — body validated against UpdateContactDto
DELETE /contacts/:id soft delete — sets deletedAt instead of removing the row
PATCH /contacts/:id/restore undo a soft delete#Adding a custom endpoint
Say you need GET /contacts/recent — five most recently created contacts, admin-only. That's the point where you stop using the one-liner module and extend the generated controller instead. See Adding Custom Logic for the full pattern (repository, service, and controller subclasses) — the short version:
// 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';
@Controller('contacts')
export class ContactsController extends ResourceController(contactsResource) {
constructor(service: ContactsService) {
super(service);
}
@Get('recent')
@UseGuards(AdminGuard)
recent() {
return this.service.list({ sort: '-createdAt', limit: 5 });
}
}Every route from Step 4 is inherited automatically — you're only adding to it, not rebuilding it.