Skip to Content
Apso is in public beta. Get started

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.

src/extensions/project-summary.service.ts
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.

src/extensions/project-summary.controller.ts
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.

src/extensions/extensions.module.ts
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 /Projects for resource operations.
  • Use purpose-specific routes such as /project-summaries/:workspaceId for 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.
Last updated on