Add custom endpoints
Generated routes cover normal create, read, update, and delete operations. Add a custom endpoint when one request represents a product workflow, combines entities, or calls another service.
Keep custom controllers outside src/autogen/. This preserves them when the schema changes.
Example: project summary
This service reads generated entities through TypeORM and returns one response for a dashboard.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Project } from '../autogen/Project/Project.entity';
import { Task } from '../autogen/Task/Task.entity';
@Injectable()
export class ProjectSummaryService {
constructor(
@InjectRepository(Project)
private readonly projects: Repository<Project>,
@InjectRepository(Task)
private readonly tasks: Repository<Task>,
) {}
async forWorkspace(workspaceId: string) {
const [projectCount, taskCount] = await Promise.all([
this.projects.count({ where: { workspaceId } }),
this.tasks.count({ where: { workspaceId } }),
]);
return { workspaceId, projectCount, taskCount };
}
}Expose the service through a route that describes the workflow.
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ProjectSummaryService } from './project-summary.service';
@ApiTags('Project summaries')
@Controller('project-summaries')
export class ProjectSummaryController {
constructor(private readonly summaries: ProjectSummaryService) {}
@Get(':workspaceId')
@ApiOperation({ summary: 'Get project and task totals for a workspace' })
getSummary(
@Param('workspaceId', new ParseUUIDPipe()) workspaceId: string,
) {
return this.summaries.forWorkspace(workspaceId);
}
}Register the module
The custom module must register every generated entity repository it injects.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Project } from '../autogen/Project/Project.entity';
import { Task } from '../autogen/Task/Task.entity';
import { ProjectSummaryController } from './project-summary.controller';
import { ProjectSummaryService } from './project-summary.service';
@Module({
imports: [TypeOrmModule.forFeature([Project, Task])],
controllers: [ProjectSummaryController],
providers: [ProjectSummaryService],
})
export class ExtensionsModule {}Import ExtensionsModule in src/app.module.rest.ts and add it to the root imports array. Start the service, then open http://localhost:3100/_docs to confirm that the route appears.
Validate request bodies
Use a DTO and a validation pipe for custom input. The generated application does not imply that every custom body is validated automatically.
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common';
import { IsIn, IsUUID } from 'class-validator';
class ArchiveRequest {
@IsUUID()
projectId: string;
@IsIn(['user-request', 'retention-policy'])
reason: string;
}
@Controller('project-workflows')
export class ProjectWorkflowController {
@Post('archive')
archive(@Body(new ValidationPipe({ transform: true })) body: ArchiveRequest) {
return this.workflows.archive(body.projectId, body.reason);
}
}Apply authentication and scope
Use the guard generated for your selected authentication configuration or a custom guard registered by your application. Derive the workspace or organization identifier from the authenticated request. Do not accept a tenant identifier from the body and trust it without checking the caller’s membership.
For multi-tenant endpoints, test that a caller from workspace B cannot read or change data owned by workspace A.
Route design
- Use generated plural routes such as
/Projectsfor resource operations. - Use purpose-specific routes such as
/project-summaries/:workspaceIdfor workflows and projections. - Return standard HTTP status codes and stable response shapes.
- Document the route with Swagger decorators so it appears in
/_docs. - Keep provider calls and database access in services so you can test controllers in isolation.