@void-snippets/nestjs

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

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

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

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);
  }

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

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

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