Skip to content

Add a NestJS API Module

Add a new feature module to services/api following the conventions used across the codebase.

Module structure

Each feature module lives in services/api/src/modules/<feature>/ and contains:

note/
├── note.module.ts
├── note.controller.ts
├── note.service.ts
└── dto/
    ├── create-note.dto.ts
    └── update-note.dto.ts

Step 1 — Create the module files

note.module.ts

typescript
import { Module } from '@nestjs/common'
import { NoteController } from './note.controller'
import { NoteService } from './note.service'

@Module({
  controllers: [NoteController],
  providers: [NoteService],
})
export class NoteModule {}

note.service.ts

typescript
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../../prisma/prisma.service'

@Injectable()
export class NoteService {
  constructor(private readonly prisma: PrismaService) {}
  // ...
}

note.controller.ts

typescript
import { Controller, Get } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { NoteService } from './note.service'
import { CheckAbility } from '../../casl/decorators'
import { TenantId } from '../../auth/decorators'

@ApiTags('Notes')
@Controller('tenants/:tenantId/notes')
export class NoteController {
  constructor(private readonly noteService: NoteService) {}

  @Get()
  @CheckAbility({ action: 'read', subject: 'Entry' })
  findAll(@TenantId() tenantId: string) {
    return this.noteService.findAll(tenantId)
  }
}

Step 2 — Create DTOs

Use class-validator decorators to validate incoming request bodies:

typescript
// dto/create-note.dto.ts
import { IsString, MinLength } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger'

export class CreateNoteDto {
  @ApiProperty()
  @IsString()
  @MinLength(1)
  content: string
}

Step 3 — Register the module in AppModule

Open services/api/src/app.module.ts and add your new module to the imports array:

typescript
import { NoteModule } from './modules/note/note.module'

@Module({
  imports: [
    // ... existing modules
    NoteModule,
  ],
})
export class AppModule {}

Step 4 — Apply guards correctly

Every tenant-scoped route must be protected by both authentication (handled globally by AuthGuard) and authorization. Use @CheckAbility to specify which CASL ability is required:

typescript
@CheckAbility({ action: 'manage', subject: 'Entry' })

The TenantGuard (applied globally) resolves the caller's role and builds the CASL ability automatically. See the Permissions reference for available actions and subjects.

Step 5 — Regenerate the SDK

After starting the API (pnpm dev:api), regenerate the typed frontend client:

bash
pnpm openapi

Your new endpoints are now available in src/api/.

TT Time Tracker — Internal Documentation