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

typescript
// 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;
}

Attach these and the generated controller validates request bodies automatically:

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

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

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

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

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