This file provides guidance for AI agents working with this NestJS boilerplate codebase.
A production-ready NestJS boilerplate using Fastify, TypeORM (PostgreSQL), and comprehensive tooling for building REST APIs.
Tech Stack:
- NestJS 11 with Fastify (not Express)
- TypeORM with PostgreSQL
- SWC compiler for fast builds
- Winston logging with OpenTelemetry support
- Jest for testing
npm run start:dev # Development with hot reload
npm run build # Build the project
npm run test # Run unit tests
npm run test:e2e:local # Run E2E tests locally
npm run lint # Check linting
npm run lint:fix # Fix linting issues
npm run codegen # Generate new data model (interactive)npm run migration generate migration_name # Generate migration from schema changes
npm run migration create migration_name # Create empty migration
npm run migration up # Run migrations
npm run migration down # Revert last migrationsrc/
├── main.ts # Application entry point
├── modules/ # Feature modules
│ ├── app/ # Root AppModule
│ ├── _db/ # Database configuration
│ ├── auth/ # Auth service (global)
│ ├── user/ # Example: User module
│ └── {feature}/ # Other feature modules
├── models/ # Entities and DTOs
│ ├── _base/ # BaseEntity class
│ ├── _shared/ # Shared DTOs (ApiResponse, ApiError)
│ └── {entity}/ # Entity + CreateDto + UpdateDto
├── db/
│ ├── crud/ # Generic Base/Crud Service & Controller
│ ├── migrations/ # TypeORM migrations
│ └── query/ # Query parsing utilities
├── guards/ # Auth guard
├── interceptors/ # Global response interceptor
├── filters/ # Exception filter
├── decorators/ # @Serialize, @ReqCtx
├── pipes/ # Validation pipe
├── logging/ # Winston logger
├── cls/ # Request context (trace IDs)
├── config/ # Config loading and secrets manager
└── utils/ # HttpException, error codes, helpers
config/
├── default.json # Default config values
├── custom-environment-variables.json # Env var mappings
└── local.json # Local overrides (gitignored)
Run npm run codegen to scaffold a new data model with:
- Entity class extending BaseEntity
- CrudService (optional)
- CrudController (optional)
- Module with proper imports
After generation:
- Add fields to the entity in
src/models/{name}/{name}.entity.ts - Add validation decorators to the DTOs
- Generate migration:
npm run migration generate add_{name} - Review and run the migration
-
Entity (
src/models/{name}/{name}.entity.ts):- Extend
BaseEntityfrom@/models/_base/_base.entity.ts - Implement
idPrefix()returning a 3-4 char prefix (e.g.,"usr_") - Add
@Entity("table_name")decorator
- Extend
-
DTOs (same file or separate):
- Create
Create{Name}Dtowithclass-validatordecorators - Create
Update{Name}Dtowith optional fields
- Create
-
Service (
src/modules/{name}/{name}.service.ts):- Extend
CrudService<Entity, CreateDto, UpdateDto> - Inject repository via
@InjectRepository(Entity)
- Extend
-
Controller (
src/modules/{name}/{name}.controller.ts):- Extend
CrudController<Entity, CreateDto, UpdateDto> - Add
@Controller("{route}")and@ApiTags("{Tag}")
- Extend
-
Module (
src/modules/{name}/{name}.module.ts):- Import in
AppModuleatsrc/modules/app/app.module.ts - Add entity to
DbModuleexports atsrc/modules/_db/db.module.ts
- Import in
-
Migration: Generate after entity changes
All entities extend BaseEntity which provides:
id: ULID-based string with entity prefixcreatedAt,updatedAt: Auto-managed timestamps
@Entity("users")
export class User extends BaseEntity {
idPrefix(): string {
return "usr_";
}
@Column()
username: string;
}The service layer uses inheritance to separate read and write operations:
- BaseService - Read-only operations:
count,list,get,getById - CrudService extends BaseService - Adds write operations:
create,createBulk,update,upsert,deleteById, etc.
@Injectable()
export class UserService extends CrudService<User, CreateUserDto, UpdateUserDto> {
constructor(@InjectRepository(User) repo: Repository<User>) {
super("User", repo);
}
}Use BaseService for read-only access, CrudService for full CRUD.
TransactionService wraps DataSource.transaction() for atomic multi-entity operations. Combined with withTransaction(manager) on BaseService, existing service methods can be reused inside transactions without duplicating logic.
@Injectable()
export class PostService extends CrudService<Post, PostCreateDto, PostUpdateDto> {
constructor(
@InjectRepository(Post) repo: Repository<Post>,
private readonly transactionService: TransactionService,
private readonly commentService: CommentService,
) {
super("Post", repo);
}
async createPostWithComment(dto: CreatePostWithCommentDto): Promise<Post> {
return this.transactionService.run(async manager => {
const txPostService = this.withTransaction(manager);
const txCommentService = this.commentService.withTransaction(manager);
const post = await txPostService.create({ title: dto.title, content: dto.content, userId: dto.userId });
const comment = await txCommentService.create({ content: dto.comment.content, userId: dto.userId, postId: post.id });
return Object.assign(post, { comments: [comment] });
});
}
}Key points:
withTransaction(manager)returns a lightweight clone of the service that uses a transaction-scoped repository- All existing service methods (
create,update,list, etc.) work on the clone without modification TransactionService.run()accepts an optionalisolationLevel(READ COMMITTED,SERIALIZABLE, etc.)- On error, TypeORM automatically rolls back the transaction
The controller layer mirrors the service inheritance:
- BaseController - Read-only endpoints (no auth required):
GET /count- Count entitiesGET /- List with query supportGET /first- Get first matchGET /:id- Get by ID
- CrudController extends BaseController - Adds write endpoints (requires auth):
POST /- CreatePOST /bulk- Create bulkPUT /- UpsertPATCH /:id- UpdateDELETE /:id- Delete
Both factory functions accept an optional options parameter with an exclude array to skip specific routes:
// BaseController — skip specific read-only routes
BaseController(entityType, { exclude: ["listCursor"] })
// CrudController — skip any base or crud route
CrudController(entityType, CreateDto, UpdateDto, { exclude: ["deleteById", "createBulk"] })Available route names:
- Base:
count,list,get,listCursor,getById - Crud:
create,createBulk,upsert,upsertBulk,updateIndexed,update,deleteIndexed,deleteById
The API supports advanced queries via URL parameters:
select=field1,field2- Field selectioninclude=relation- Eager load relationsfilter=(field,operator,value)- Filteringsort=(field,ASC)- Sortinglimit=10&offset=0- Pagination
Operators: eq, ne, like, ilike, gt, lt, gte, lte, in, notin, isnull, isnotnull, between, notbetween
Bearer token auth via AuthGuard and AuthService. Token is checked against config.apiKey.
@UseGuards(AuthGuard)
@Post()
create(@Body() dto: CreateUserDto) {}Access authenticated user via @ReqCtx():
@UseGuards(AuthGuard)
@Get()
handler(@ReqCtx() ctx: IReqCtx) {
console.log(ctx.user); // { userId: "sample-user-id" }
}The auth flow:
AuthGuardextracts Bearer token from Authorization headerAuthService.validateToken()validates the token- Authenticated user is stored in CLS (request context)
@ReqCtx()decorator retrieves user from CLS
Use the custom HttpException for consistent error responses:
import { HttpException } from "@/utils/HttpException";
import { ErrorCodes } from "@/utils/error-codes";
throw new HttpException(404, "User not found", ErrorCodes.INVALID_USER, { id });import { Logger } from "@/logging/Logger";
const logger = new Logger("ServiceName");
logger.info("Message", { data: { key: "value" } });Access trace ID and request info:
@Get()
handler(@ReqCtx() ctx: IReqCtx) {
console.log(ctx.traceId);
}Configuration hierarchy (later overrides earlier):
config/default.json- Base defaultsconfig/{NODE_ENV}.json- Environment-specificconfig/local.json- Local overrides (gitignored).envfile - Environment variables- System environment variables
- Secrets from
loadSecrets()- Async secrets (e.g., from AWS Secrets Manager)
Access config via:
import { config } from "./config";
config.portFor external secrets (e.g., AWS Secrets Manager, Vault), implement loadSecrets() in src/config/secrets-manager.ts. The returned object is deep-merged with the base config at startup.
export async function loadSecrets(): Promise<any> {
const secrets = await fetchFromVault();
return { db: { url: secrets.DB_URL } };
}Located alongside source files as *.spec.ts. Run with npm run test.
Located in test/app.e2e-spec.ts. Tests all CRUD operations, auth, validation, and queries.
npm run test:e2e:local # Local execution
npm run test:e2e # Docker-based- Use
@testcontainers/postgresqlfor integration tests requiring a database - Mock dependencies using Jest's
jest.mock() - Test files use the pattern
{name}.spec.ts
| File | Purpose |
|---|---|
src/main.ts |
Bootstrap, middleware, Swagger setup |
src/modules/app/app.module.ts |
Root module, import all feature modules here |
src/modules/_db/db.module.ts |
TypeORM config, entity exports |
src/db/crud/base.service.ts |
Read-only service base class |
src/db/crud/crud.service.ts |
Full CRUD service (extends BaseService) |
src/db/crud/base.controller.ts |
Read-only controller base class |
src/db/crud/crud.controller.ts |
Full CRUD controller (extends BaseController) |
src/models/_base/_base.entity.ts |
Base entity with id, timestamps |
src/filters/all-exceptions.filter.ts |
Global exception handling |
src/guards/auth.guard.ts |
Bearer token authentication |
plopfile.ts |
Code generation configuration |
config/default.json |
Default configuration values |
src/config/secrets-manager.ts |
Async secrets loading (customize for your secrets backend) |
src/config/index.ts |
Config initialization and secrets merging |
src/db/transaction/transaction.service.ts |
Reusable transaction wrapper |
- Add the column to the entity class
- Update CreateDto and UpdateDto if needed
- Generate migration:
npm run migration generate add_field_to_table - Review the generated migration
- Run:
npm run migration upor restart the server
- Add method to the service if custom logic needed
- Add route handler to the controller with appropriate decorators
- Add Swagger decorators (
@ApiOperation,@ApiResponse)
Use class-validator decorators in DTOs:
import { IsEmail, IsNotEmpty, MinLength } from "class-validator";
export class CreateUserDto {
@IsNotEmpty()
@MinLength(3)
username: string;
@IsEmail()
email: string;
}- Don't use Express-specific code - This uses Fastify, not Express
- Always generate migrations - Don't rely on
synchronize: true - Use the custom HttpException - Not the NestJS one, for consistent error format
- Import from
@/- Use path aliases, not relative paths like../../ - Add entities to DbModule - New entities must be added to
src/modules/_db/db.module.ts - Implement
idPrefix()- Required for all entities extending BaseEntity - Don't skip validation - Always add class-validator decorators to DTOs
- Don't skip Swagger - Always add swagger decorators to DTOs