Adding Custom Logic
Extend the generated Repository/Service/Controller when you need side effects or extra endpoints.
#When the one-liner isn't enough
forTypeOrmResource/forMongooseResource cover the common case: standard CRUD with some filters and pagination. The moment you need a side effect, an extra dependency injected (a mailer, a queue), or an endpoint the generated set doesn't have, stop using the one-liner and write three thin classes that extends the generated bases instead. You keep every generated route and every bit of typing — you're only adding to it.
#Repository
// contacts.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ResourceRepository } from '@void-snippets/nestjs';
import { Contact } from './contact.entity';
@Injectable()
export class ContactsRepository extends ResourceRepository<Contact, number> {
constructor(@InjectRepository(Contact) repo: Repository<Contact>) {
super(repo); // the ORM is auto-detected from whatever handle you pass in
}
}#Service
// 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';
@Injectable()
export class ContactsService extends ResourceService(contactsResource) {
constructor(repo: ContactsRepository, private mailer: MailerService) {
super(repo);
}
// Override any hook from the defineResource reference — fully typed to your entity.
protected override async beforeCreate(dto: Contact, ctx: RequestContext) {
return { ...dto, orgId: ctx.orgId };
}
protected override async afterCreate(entity: Contact) {
await this.mailer.sendWelcome(entity.email);
}
}ResourceService(contactsResource) is a function that returns a class — you extend the class it returns, the same way you'd extend any base class. This is what lets the base class already know your entity's shape without you writing any generics.
#Controller
// 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);
}
// Every generated route (list, get, create, ...) is inherited automatically.
// Add your own routes freely — they live alongside the generated ones:
@Get('recent')
@UseGuards(AdminGuard)
recent() {
return this.service.list({ sort: '-createdAt', limit: 5 });
}
}
@Controller('contacts')on the subclass is required. Nest reads a class's constructor parameter types to know what to inject, and TypeScript only emits that information for a class with at least one decorator — so@Controller(...)here (and@Injectable()on the service) is what makes the injection work.
#Wiring the module
// contacts.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Contact } from './contact.entity';
import { ContactsRepository } from './contacts.repository';
import { ContactsService } from './contacts.service';
import { ContactsController } from './contacts.controller';
@Module({
imports: [TypeOrmModule.forFeature([Contact])],
controllers: [ContactsController],
providers: [ContactsRepository, ContactsService],
})
export class ContactsModule {}#Prefer hooks without writing a subclass?
If the only thing you need is a hook (no extra injected dependency, no extra endpoint), you don't have to write any of the three classes above — pass hooks directly in defineResource() and keep using the one-liner ORM helper. See the Lifecycle hooks section on the defineResource page. A subclass method, if you do write one, always takes priority over an inline hook of the same name.
#The raw() escape hatch
For a join or aggregation the generated CRUD layer can't express, drop to the native ORM handle directly from a repository subclass:
// TypeORM — a left join with a count
const rows = await this.repo.raw((native) => {
const qb = (native as Repository<Contact>).createQueryBuilder('c');
return qb.leftJoin('c.orders', 'o')
.select('c.id').addSelect('COUNT(o.id)', 'orders')
.groupBy('c.id').getRawMany();
});
// Mongoose — an aggregation pipeline
const agg = await this.repo.raw((native) =>
(native as Model<Contact>).aggregate([{ $group: { _id: '$status', n: { $sum: 1 } } }]),
);