Software_development_department nestjs-expert

Provides NestJS patterns for modules, controllers, providers, guards, interceptors, and microservices. Use when working with NestJS TypeScript files (*.module.ts, *.controller.ts, *.service.ts) or when the user mentions NestJS, Nest.js, or NestJS modules.

install
source · Clone the upstream repo
git clone https://github.com/tranhieutt/software_development_department
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tranhieutt/software_development_department "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/nestjs-expert" ~/.claude/skills/tranhieutt-software-development-department-nestjs-expert && rm -rf "$T"
manifest: .claude/skills/nestjs-expert/SKILL.md
source content

NestJS Expert

Critical rules (non-obvious)

  • Circular dependencies: use
    forwardRef(() => ServiceB)
    in both modules; better — restructure to avoid
  • Global modules: use
    @Global()
    sparingly; prefer explicit imports to keep modules testable
  • APP_GUARD
    /
    APP_INTERCEPTOR
    : registered in AppModule providers, not in individual modules
  • Lifecycle hooks order:
    onModuleInit
    onApplicationBootstrap
    → ready;
    onModuleDestroy
    beforeApplicationShutdown
    onApplicationShutdown
  • Never use
    req.user
    without type assertion
    — it's
    any
    from Passport; extend
    Express.Request

Module structure

@Module({
  imports: [TypeOrmModule.forFeature([User]), JwtModule],
  controllers: [UserController],
  providers: [UserService, UserRepository],
  exports: [UserService],   // export what other modules need
})
export class UserModule {}

Controller with validation

@Controller("users")
@UseGuards(JwtAuthGuard)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(":id")
  @HttpCode(HttpStatus.OK)
  async findOne(@Param("id", ParseUUIDPipe) id: string, @CurrentUser() user: User) {
    return this.userService.findOneOrFail(id);
  }

  @Post()
  @Roles(Role.ADMIN)
  async create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

Service with repository pattern

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private readonly repo: Repository<User>,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async findOneOrFail(id: string): Promise<User> {
    const user = await this.repo.findOne({ where: { id } });
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }

  async create(dto: CreateUserDto): Promise<User> {
    const user = this.repo.create(dto);
    const saved = await this.repo.save(user);
    this.eventEmitter.emit("user.created", new UserCreatedEvent(saved));
    return saved;
  }
}

JWT auth guard pattern

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  canActivate(context: ExecutionContext) {
    return super.canActivate(context);
  }
  handleRequest(err: any, user: any) {
    if (err || !user) throw err ?? new UnauthorizedException();
    return user;
  }
}

// Custom decorator for current user
export const CurrentUser = createParamDecorator(
  (_, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);

Global exception filter

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;
    ctx.getResponse().status(status).json({
      statusCode: status,
      message: exception instanceof HttpException ? exception.message : "Internal server error",
      timestamp: new Date().toISOString(),
    });
  }
}
// Register: app.useGlobalFilters(new AllExceptionsFilter())

Interceptor: response transform + timing

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    return next.handle().pipe(
      map(data => ({ data, duration: Date.now() - start, timestamp: new Date() })),
    );
  }
}

Validation DTO

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  @Matches(/^(?=.*[A-Z])(?=.*\d)/, { message: "Must contain uppercase and digit" })
  password: string;

  @IsEnum(Role)
  @IsOptional()
  role?: Role = Role.USER;
}
// Global: app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }))

Config with validation

// app.module.ts
ConfigModule.forRoot({
  isGlobal: true,
  validationSchema: Joi.object({
    NODE_ENV: Joi.string().valid("development", "production", "test").required(),
    PORT: Joi.number().default(3000),
    DATABASE_URL: Joi.string().required(),
    JWT_SECRET: Joi.string().min(32).required(),
  }),
})

Common pitfalls

PitfallFix
Injecting service into wrong moduleExport service from its module; import module
Missing
async
on lifecycle hooks
async onModuleInit()
if doing DB work on startup
ValidationPipe
without
whitelist: true
Strips extra fields; prevents mass assignment
Blocking the event loop in providerUse
async/await
; never synchronous I/O
Missing
enableShutdownHooks()
Required for
onModuleDestroy
to fire in Docker