agentsclimarketplace

Nestjs expert

Skill Inteligentsensingsolutions/tdd-dev-workflow/skills/nestjs-expert

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.From its SKILL.md

Install
npx -y skills add Inteligentsensingsolutions/tdd-dev-workflow --skill nestjs-expert

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.7 KB, as published. Nobody here has run it

NestJS Expert

Comprehensive NestJS architecture and patterns guide covering module design, dependency injection, middleware pipeline, testing strategies, and production patterns.


Module Architecture

Feature modules encapsulate domain logic. Each bounded context maps to one module.

Module Organization

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 Rules

  • One module per domain feature. The module owns its controllers, services, repositories, and DTOs.
  • Use @Module({ imports, controllers, providers, exports }) to declare boundaries.
  • Export only the services that other modules need. Keep internal providers private.
  • Use SharedModule for cross-cutting concerns (logging, validation, common pipes).
  • Apply @Global() sparingly: only for services needed everywhere (config, logger, event bus).

Avoiding Circular Dependencies

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.


Request Pipeline

NestJS processes requests through a defined pipeline. Understand execution order.

Middleware --> Guards --> Interceptors (before) --> Pipes --> Handler --> Interceptors (after) --> Filters

Guards

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

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

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

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('*');
  }
}

Repository Pattern

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) {}

DTO Validation

Define request shapes with Data Transfer Objects. Validate at the boundary, not inside services.

Zod-Based DTOs

// 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>;

Validation Rules

  • Validate at the controller layer using Pipes. Services receive already-validated data.
  • Use whitelist: true in the global ValidationPipe to strip unexpected properties.
  • Use transform: true to auto-convert query params and path params to their declared types.
  • Prefer Zod schemas over class-validator decorators for composability and type inference.
  • Share validation schemas between frontend and backend when both use TypeScript.
  • Return structured error messages that map field names to validation failures.

Global Pipe Registration

// 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,
  }),
);

Testing

Unit Tests

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);
  });
});

E2E Tests

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]');
      });
  });
});

Testing Best Practices

  • Mock at the boundary: replace repositories and external services, not internal methods.
  • Use custom providers for test doubles to keep the DI container consistent with production.
  • Test guards and pipes in isolation by mocking ExecutionContext and CallHandler.
  • For database integration tests, use a dedicated test database or Docker test containers. Reset state between test suites.
  • Name test files with .spec.ts for unit tests and .e2e-spec.ts for end-to-end tests.
  • Cover the request pipeline: verify that guards reject unauthorized requests, pipes reject invalid input, and interceptors transform responses correctly.
  • Use app.close() in afterAll to prevent connection leaks in E2E test suites.

Authentication

JWT with Passport.js

// 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,
    };
  }
}

Role-Based Access

// 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() { ... }
}

Background Jobs

BullMQ Integration

// 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
  }
}

Error Handling

Exception Filters

@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(),
    });
  }
}

Configuration

Validated Environment Config

// 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 {}

Quick Reference

ConceptPatternWhen to Use
Guards@UseGuards(Guard)Authentication and authorization checks
Pipes@UsePipes(Pipe)Input validation and transformation
InterceptorsNestInterceptorLogging, caching, response wrapping, timeouts
MiddlewareNestMiddlewareRequest 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

  • For architectural decision trees (ORM selection, module organization, testing strategy, auth method, caching), read references/decision-trees.md.
  • For common problem-solution pairs (17 patterns), read references/problem-solutions.md.

Dependencies

SkillReason
typescript-proNestJS is built on TypeScript decorators, generics, and strong typing throughout the DI system

Used By

SkillReason
bullmq-patternsBullMQ queue integration relies on NestJS module architecture and dependency injection patterns

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.