From tdd-dev-workflow
This skill should be used when the user asks about "NestJS", "Nest.js", "NestJS modules", "dependency injection", "guards", "interceptors", "pipes", "middleware", "NestJS testing", "TypeORM", "Prisma with NestJS", "NestJS authentication", "JWT guards", "NestJS validation", or needs guidance on NestJS architecture and patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tdd-dev-workflow:nestjs-expertThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive NestJS architecture and patterns guide covering module design, dependency injection, middleware pipeline, testing strategies, and production patterns.
Comprehensive NestJS architecture and patterns guide covering module design, dependency injection, middleware pipeline, testing strategies, and production patterns.
Feature modules encapsulate domain logic. Each bounded context maps to one module.
src/
auth/
auth.module.ts
auth.controller.ts
auth.service.ts
auth.guard.ts
dto/
entities/
users/
users.module.ts
users.controller.ts
users.service.ts
users.repository.ts
dto/
entities/
shared/
shared.module.ts
filters/
interceptors/
pipes/
@Module({ imports, controllers, providers, exports }) to declare boundaries.SharedModule for cross-cutting concerns (logging, validation, common pipes).@Global() sparingly: only for services needed everywhere (config, logger, event bus).Circular dependencies between modules cause runtime errors. Address them structurally.
// Option 1: forwardRef (quick fix, not ideal)
@Module({
imports: [forwardRef(() => UsersModule)],
})
export class AuthModule {}
// Option 2: Extract shared logic (preferred)
// Move the shared interface/service to a third module both can import
@Module({
imports: [SharedIdentityModule],
})
export class AuthModule {}
Prefer restructuring over forwardRef(). Circular dependencies usually signal a design problem.
NestJS processes requests through a defined pipeline. Understand execution order.
Middleware --> Guards --> Interceptors (before) --> Pipes --> Handler --> Interceptors (after) --> Filters
Guards determine whether a request is authorized to proceed.
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) return true;
const request = context.switchToHttp().getRequest();
return roles.includes(request.user.role);
}
}
// Usage
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/dashboard')
getAdminDashboard() { ... }
Pipes validate and transform input data before it reaches the handler.
// Zod validation pipe
@Injectable()
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown) {
const result = this.schema.safeParse(value);
if (!result.success) {
throw new BadRequestException(result.error.format());
}
return result.data;
}
}
// Usage
@Post()
create(@Body(new ZodValidationPipe(CreateUserSchema)) dto: CreateUserDto) { ... }
Interceptors wrap handler execution for logging, caching, transformation, and timeouts.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => this.logger.log(`${context.getHandler().name} - ${Date.now() - now}ms`)),
);
}
}
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(5000));
}
}
Middleware runs before the route handler for cross-cutting request processing.
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
}
// Register in module
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
}
}
Abstract database access behind repository interfaces. This enables testing with in-memory doubles and swapping ORM implementations.
// Interface
export interface IUserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(data: CreateUserData): Promise<User>;
update(id: string, data: UpdateUserData): Promise<User>;
}
// Implementation (TypeORM)
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(UserEntity)
private readonly repo: Repository<UserEntity>,
) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOneBy({ id });
}
}
// Register with custom provider
{
provide: 'USER_REPOSITORY',
useClass: UserRepository,
}
// Inject
constructor(@Inject('USER_REPOSITORY') private userRepo: IUserRepository) {}
Define request shapes with Data Transfer Objects. Validate at the boundary, not inside services.
// create-user.dto.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['user', 'admin']).default('user'),
});
export type CreateUserDto = z.infer<typeof CreateUserSchema>;
// update-user.dto.ts
export const UpdateUserSchema = CreateUserSchema.partial().omit({ role: true });
export type UpdateUserDto = z.infer<typeof UpdateUserSchema>;
whitelist: true in the global ValidationPipe to strip unexpected properties.transform: true to auto-convert query params and path params to their declared types.class-validator decorators for composability and type inference.// main.ts
app.useGlobalPipes(
new ZodValidationPipe(), // Custom pipe for Zod schemas
);
// Or with class-validator
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
Test individual services and controllers in isolation with mocked dependencies.
describe('UsersService', () => {
let service: UsersService;
let repository: jest.Mocked<IUserRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{
provide: 'USER_REPOSITORY',
useValue: {
findById: jest.fn(),
findByEmail: jest.fn(),
create: jest.fn(),
},
},
],
}).compile();
service = module.get(UsersService);
repository = module.get('USER_REPOSITORY');
});
it('returns user when found', async () => {
const user = { id: '1', email: '[email protected]' };
repository.findById.mockResolvedValue(user);
const result = await service.findById('1');
expect(result).toEqual(user);
});
it('throws NotFoundException when user not found', async () => {
repository.findById.mockResolvedValue(null);
await expect(service.findById('999')).rejects.toThrow(NotFoundException);
});
});
Test actual HTTP requests through the full NestJS pipeline.
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider('USER_REPOSITORY')
.useValue(createMockRepository())
.compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
it('POST /users creates a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: '[email protected]', name: 'New User' })
.expect(201)
.expect((res) => {
expect(res.body.email).toBe('[email protected]');
});
});
});
ExecutionContext and CallHandler..spec.ts for unit tests and .e2e-spec.ts for end-to-end tests.app.close() in afterAll to prevent connection leaks in E2E test suites.// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKeyProvider: jwksRsa.passportJwtSecret({
cache: true,
rateLimit: true,
jwksUri: configService.get('JWKS_URI'),
}),
algorithms: ['RS256'],
});
}
validate(payload: JwtPayload): AuthUser {
return {
id: payload.sub,
email: payload.email,
role: payload.role,
};
}
}
// roles.decorator.ts
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Apply to controllers
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Roles('admin')
@Get('users')
listUsers() { ... }
}
// Register queue
@Module({
imports: [
BullModule.registerQueue({ name: 'email' }),
],
providers: [EmailProcessor],
})
export class EmailModule {}
// Producer
@Injectable()
export class EmailService {
constructor(@InjectQueue('email') private emailQueue: Queue) {}
async sendWelcome(userId: string) {
await this.emailQueue.add('welcome', { userId }, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
}
}
// Consumer
@Processor('email')
export class EmailProcessor {
@Process('welcome')
async handleWelcome(job: Job<{ userId: string }>) {
// Send email logic
}
}
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
this.logger.error(exception);
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}
// env.validation.ts
import { z } from 'zod';
export const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
});
export type Env = z.infer<typeof envSchema>;
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
validate: (config) => envSchema.parse(config),
isGlobal: true,
}),
],
})
export class AppModule {}
| Concept | Pattern | When to Use |
|---|---|---|
| Guards | @UseGuards(Guard) | Authentication and authorization checks |
| Pipes | @UsePipes(Pipe) | Input validation and transformation |
| Interceptors | NestInterceptor | Logging, caching, response wrapping, timeouts |
| Middleware | NestMiddleware | Request logging, CORS, rate limiting |
| Exception Filters | @Catch(ExceptionType) | Unified error response formatting |
| Custom Providers | { provide, useClass/useValue } | Dependency injection with interfaces |
forwardRef() | forwardRef(() => Module) | Circular dependency workaround (restructure if possible) |
references/decision-trees.md.references/problem-solutions.md.| Skill | Reason |
|---|---|
typescript-pro | NestJS is built on TypeScript decorators, generics, and strong typing throughout the DI system |
| Skill | Reason |
|---|---|
bullmq-patterns | BullMQ queue integration relies on NestJS module architecture and dependency injection patterns |
npx claudepluginhub inteligentsensingsolutions/tdd-dev-workflow --plugin tdd-dev-workflowGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.