From nestjs-clean-arch
Step-by-step workflow for implementing entirely new features from scratch in the NestJS Clean Architecture project. Use this skill when the user asks to "add a new feature", "create a new resource/entity", "implement [Entity] from scratch", "new entity", "greenfield", or needs to build something that doesn't exist yet — including all layers from domain entity to tests.
npx claudepluginhub tuannguyen151/foxdemon-plugins --plugin nestjs-clean-archThis skill uses the workspace's default tool permissions.
A step-by-step workflow for implementing entirely new features from scratch in this NestJS Clean Architecture project. Follow the 12 steps in order — each step builds on the previous one.
Guides NestJS API development with architecture, modules, DI, guards, interceptors, pipes, MongoDB/Mongoose integration, auth, DTOs, error handling, and production patterns.
Applies opinionated NestJS conventions for backends: module structure, dependency injection, controllers/services, DTOs with class-validator, guards/interceptors/pipes, JWT auth, TypeORM/Prisma.
Provides NestJS patterns for scalable Node.js/TypeScript backends: modular architecture, dependency injection, DTO validation, repositories, and events.
Share bugs, ideas, or general feedback.
A step-by-step workflow for implementing entirely new features from scratch in this NestJS Clean Architecture project. Follow the 12 steps in order — each step builds on the previous one.
For a complete worked example (Comment entity through all 12 steps), read
references/complete-greenfield-example.md.
Comment, Tag, Category)Before writing any code, confirm these with the user or infer from context:
Comment, Tag)publish, archive)?If any of these are unclear, ask the user before proceeding.
Execute these steps IN ORDER. Each step produces one or more files.
File: src/domain/entities/{entity}.entity.ts
Create the domain entity as a pure TypeScript class with zero framework imports.
readonly for id, createdAt, updatedAt — these are immutableprivate _field + public getter for fields with business rules (e.g., status transitions)public for freely mutable fields (e.g., title, description)complete(), publish())Follow the pattern in the existing TaskEntity:
constructor(
public readonly id: number,
public userId: number,
public content: string,
private _status: StatusEnum,
public readonly createdAt: Date,
public readonly updatedAt: Date,
)
File: src/domain/repositories/{entity}.repository.interface.ts
Define the contract for data access. This file exports both a Symbol and an interface with the same name.
export const I{Entity}Repository = Symbol('I{Entity}Repository')ISearch{Entities}Params)find{Entities}, create{Entity}, findOne{Entity}, update{Entity}, delete{Entity}Directory: src/use-cases/{entities}/ (plural, kebab-case)
Create one file per operation. Each file contains exactly one class with one execute() method.
create-{entity}.use-case.ts, get-list-{entities}.use-case.ts, get-detail-{entity}.use-case.ts, update-{entity}.use-case.ts, delete-{entity}.use-case.ts@Injectable()@Inject(I{Entity}Repository) private readonly {entity}Repository: I{Entity}Repositoryexecute() method that takes typed input and returns a PromiseFile: src/infrastructure/databases/postgresql/entities/{entity}.entity.ts
Create the database mapping for the domain entity.
@Entity('{entities}') — table name is plural, lowercase@PrimaryGeneratedColumn({ type: 'bigint', primaryKeyConstraintName: 'PK_{entities}_id' })@Column() with explicit types: 'bigint', 'varchar', 'text', 'smallint', 'timestamp'@CreateDateColumn({ name: 'created_at' }) and @UpdateDateColumn({ name: 'updated_at' })@Column('bigint', { name: 'user_id' })@Index() decorators for foreign key columns@ManyToOne / @OneToMany relations if the entity references other entitiesComment, not CommentEntity)File: src/infrastructure/databases/postgresql/repositories/{entity}.repository.ts
Implement the repository interface from Step 2.
implements I{Entity}Repository@InjectRepository({TypeORMEntity}) private readonly {entity}Repository: Repository<{TypeORMEntity}>@Injectable()private toEntity() method that converts TypeORM entity → domain entitytoEntity() before returningthis.{entity}Repository.find(), .findOne(), .save(), .update(), .delete()Directory: src/adapters/controllers/{entities}/dto/
Create one DTO per operation that accepts client input.
create-{entity}.dto.ts — fields the client sends to createget-list-{entities}.dto.ts — query/filter parametersupdate-{entity}.dto.ts — fields the client can update (usually all optional)class-validator decorators: @IsNotEmpty(), @IsString(), @IsOptional(), @MaxLength(), @IsEnum(), etc.@ApiProperty() from @nestjs/swagger for every field (Swagger docs)Directory: src/adapters/controllers/{entities}/presenters/
Create one presenter per operation that returns data to the client.
create-{entity}.presenter.ts, get-list-{entities}.presenter.ts, get-detail-{entity}.presenter.ts@ApiProperty() for every exposed fieldfromList(entities: {Entity}Entity[]): {Presenter}[] method for list endpoints_status) — use the getter valueFile: src/adapters/controllers/{entities}/{entities}.controller.ts
Create the HTTP controller. Keep it thin — no business logic.
@Controller('{entities}') + @ApiTags('{Entities}')@UseGuards(JwtAuthGuard, PoliciesGuard) at class level@ApiResponse decorators for 401, 403, 500@User('id') to get the authenticated user's ID from the JWT@CheckPolicies({ action, subject }) for CASL authorization on each endpoint@ApiExtraModels(), @ApiOperation(), @ApiResponseType() / @ApiCreatedResponseType() for SwaggerFile: src/modules/{entity}.module.ts
Wire everything together with NestJS dependency injection.
TypeOrmModule.forFeature([{TypeORMEntity}]) for the database entityCaslModule and ExceptionsModule{ provide: I{Entity}Repository, useClass: {Entity}Repository }
controllerssrc/app.module.ts — add it to the imports arrayUpdate CASL if the new entity needs authorization rules.
Add subject to TSubject in src/domain/services/ability.interface.ts:
export type TSubject = 'all' | 'Task' | '{Entity}'
Add rules in src/infrastructure/services/casl/casl-ability.factory.ts:
can('manage', 'all') — no change neededcan('read', '{Entity}')
can(['create', 'update', 'delete'], '{Entity}')
Generate and run the migration inside the Docker container.
docker exec -it app-api npm run migration:generate --name=create-{entities}-table
docker exec -it app-api npm run migration:run
Review the generated migration file in database/migrations/ before running. Verify:
@Entity() decorator@Column() decoratorsCreate tests following the project's Arrange → Act → Assert pattern.
File: test/stubs/{entity}.stub.ts
Create a factory function with an overrides parameter:
export const create{Entity}Stub = (overrides: Partial<{...}> = {}): {Entity}Entity => {
return new {Entity}Entity(
overrides.id ?? 1,
overrides.userId ?? 1,
// ... defaults for all fields
)
}
Directory: test/use-cases/{entities}/
One spec file per use case. Each test:
TestingModule with the use case and a mocked repositoryjest.fn() for repository methodsjest.spyOn().mockResolvedValue() for specific test casesinputX, mockX, actualX, expectedXDirectory: test/adapters/controllers/{entities}/
{ execute: jest.fn() }PoliciesGuard with { canActivate: () => true }'ABILITY_FACTORY_INTERFACE' as empty objectUse these to handle common variations:
| Question | If Yes | If No |
|---|---|---|
| Does this entity belong to a user? | Add userId: number field, filter by userId in repository queries, use @User('id') in controller | Skip userId, queries don't filter by user |
| Does it need authorization? | Add to TSubject, update casl-ability.factory.ts, use @CheckPolicies | Skip CASL steps, may not need PoliciesGuard |
| Does it have state transitions? | Use private _status + getter + transition methods in domain entity | Use plain public field |
| Does it relate to existing entities? | Add @ManyToOne/@OneToMany in TypeORM entity, add foreign key column | No relation decorators needed |
| Does it need list filtering? | Create search params interface in repository, add GetList{Entities}Dto with filter fields | Simple findAll without params |
Run these commands inside the Docker container to verify everything works:
# All tests pass
docker exec -it app-api npm run test
# Build succeeds (TypeScript compilation)
docker exec -it app-api npm run build
# Lint clean
docker exec -it app-api npm run lint
# Migration generated and applied
docker exec -it app-api npm run migration:run
Also verify manually:
app.module.tsforFeature([]) array@domain/*, @use-cases/*, @adapters/*, @infrastructure/*)For a complete worked example implementing a Comment entity through all 12 steps with full TypeScript code, see:
references/complete-greenfield-example.md
For layer-by-layer architecture details, see the nestjs-clean-architecture skill.
Use these specialized skills for deeper guidance on specific steps:
testing-patterns — Step 12 (Tests): exact TestingModule setup, Symbol token mocking, stub factory patterns, AAA namingcasl-authorization — Step 10 (CASL): adding TSubject, ability factory rules, @CheckPolicies, CASL vs ownershipexception-handling — Step 3 (Use Cases): IException injection, error codes, when to use each exception typeswagger-api-docs — Steps 6-8 (DTOs/Presenters/Controller): custom ApiResponseType decorators, @ApiExtraModels, @ApiProperty