DeveloperBootstrap

Node.js

NestJS Enterprise API Guide

Build enterprise-grade APIs with NestJS, TypeScript, Prisma ORM, JWT authentication, guards, interceptors, and comprehensive testing.

What you'll learn

  • Structure a NestJS monolith with feature modules
  • Use Guards, Interceptors, and Pipes for cross-cutting concerns
  • Integrate Prisma ORM with PostgreSQL
  • Implement JWT auth with HttpOnly cookie refresh tokens
  • Validate DTOs with class-validator and class-transformer
  • Write unit and e2e tests with Jest
  • Configure health checks and Swagger documentation

Module Structure

Bash
1# Scaffold with Nest CLI
2npm i -g @nestjs/cli
3nest new my-api
4
5src/
6├── app.module.ts           # root module
7├── main.ts                 # bootstrap
8├── common/
9│   ├── guards/             # JwtAuthGuard, RolesGuard
10│   ├── interceptors/       # LoggingInterceptor, TransformInterceptor
11│   ├── filters/            # GlobalExceptionFilter
12│   └── decorators/         # @CurrentUser, @Roles
13├── auth/
14│   ├── auth.module.ts
15│   ├── auth.controller.ts
16│   ├── auth.service.ts
17│   └── strategies/         # jwt.strategy.ts
18├── users/
19│   ├── users.module.ts
20│   ├── users.controller.ts
21│   ├── users.service.ts
22│   ├── dto/
23│   │   ├── create-user.dto.ts
24│   │   └── update-user.dto.ts
25│   └── entities/
26│       └── user.entity.ts
27└── prisma/
28    ├── prisma.module.ts
29    └── prisma.service.ts

Guards & Interceptors

TypeScript
1// common/guards/jwt-auth.guard.ts
2import { AuthGuard } from '@nestjs/passport';
3import { Injectable } from '@nestjs/common';
4
5@Injectable()
6export class JwtAuthGuard extends AuthGuard('jwt') {}
7
8// common/interceptors/logging.interceptor.ts
9import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
10import { Observable, tap } from 'rxjs';
11import { PinoLogger } from 'nestjs-pino';
12
13@Injectable()
14export class LoggingInterceptor implements NestInterceptor {
15  constructor(private readonly logger: PinoLogger) {}
16
17  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
18    const req = ctx.switchToHttp().getRequest<{ method: string; url: string }>();
19    const now = Date.now();
20    return next.handle().pipe(
21      tap(() => this.logger.info({ method: req.method, url: req.url, ms: Date.now() - now }))
22    );
23  }
24}
25
26// Apply globally in main.ts
27app.useGlobalGuards(new JwtAuthGuard());
28app.useGlobalInterceptors(new LoggingInterceptor(logger));

Prisma Integration

TypeScript
1// prisma/prisma.service.ts
2import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
3import { PrismaClient } from '@prisma/client';
4
5@Injectable()
6export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
7  async onModuleInit() {
8    await this.$connect();
9  }
10  async onModuleDestroy() {
11    await this.$disconnect();
12  }
13}
14
15// users/users.service.ts
16@Injectable()
17export class UsersService {
18  constructor(private readonly prisma: PrismaService) {}
19
20  async findAll() {
21    return this.prisma.user.findMany({
22      select: { id: true, name: true, email: true, createdAt: true },
23      where: { isActive: true },
24      orderBy: { createdAt: 'desc' },
25    });
26  }
27}

JWT Auth

TypeScript
1// auth/strategies/jwt.strategy.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { ExtractJwt, Strategy } from 'passport-jwt';
5import { ConfigService } from '@nestjs/config';
6import { Request } from 'express';
7
8@Injectable()
9export class JwtStrategy extends PassportStrategy(Strategy) {
10  constructor(config: ConfigService) {
11    super({
12      jwtFromRequest: ExtractJwt.fromExtractors([
13        (req: Request) => req.cookies?.['access_token'] as string | null,
14      ]),
15      secretOrKey: config.getOrThrow<string>('JWT_SECRET'),
16    });
17  }
18
19  async validate(payload: { sub: string; email: string }) {
20    return { id: payload.sub, email: payload.email };
21  }
22}
23
24// auth.controller.ts
25@Post('login')
26async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
27  const { accessToken, refreshToken } = await this.authService.login(dto);
28  res.cookie('access_token', accessToken, {
29    httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 * 1000,
30  });
31  return { message: 'Logged in' };
32}

DTO Validation

TypeScript
1// users/dto/create-user.dto.ts
2import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
3import { Transform } from 'class-transformer';
4
5export class CreateUserDto {
6  @IsString()
7  @MinLength(2)
8  @MaxLength(100)
9  name: string;
10
11  @IsEmail()
12  @Transform(({ value }) => (value as string).toLowerCase())
13  email: string;
14
15  @IsString()
16  @MinLength(8)
17  @Matches(/^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])/, {
18    message: 'Password must contain uppercase, lowercase, and number',
19  })
20  password: string;
21}
22
23// main.ts — enable global validation pipe
24app.useGlobalPipes(new ValidationPipe({
25  whitelist: true,       // strip unknown properties
26  forbidNonWhitelisted: true,
27  transform: true,       // auto-transform primitives
28}));

Testing

TypeScript
1// users/users.service.spec.ts
2import { Test } from '@nestjs/testing';
3import { UsersService } from './users.service';
4import { PrismaService } from '../prisma/prisma.service';
5
6describe('UsersService', () => {
7  let service: UsersService;
8  const mockPrisma = { user: { findMany: jest.fn(), create: jest.fn() } };
9
10  beforeEach(async () => {
11    const module = await Test.createTestingModule({
12      providers: [
13        UsersService,
14        { provide: PrismaService, useValue: mockPrisma },
15      ],
16    }).compile();
17    service = module.get(UsersService);
18  });
19
20  it('findAll returns active users', async () => {
21    mockPrisma.user.findMany.mockResolvedValue([{ id: '1', name: 'Alice' }]);
22    const result = await service.findAll();
23    expect(result).toHaveLength(1);
24    expect(mockPrisma.user.findMany).toHaveBeenCalledWith(
25      expect.objectContaining({ where: { isActive: true } })
26    );
27  });
28});

Swagger & Health

TypeScript
1// main.ts
2import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
3import { TerminusModule } from '@nestjs/terminus';
4
5const config = new DocumentBuilder()
6  .setTitle('My API')
7  .setVersion('1.0')
8  .addCookieAuth('access_token')
9  .build();
10
11const doc = SwaggerModule.createDocument(app, config);
12SwaggerModule.setup('api/docs', app, doc);
13
14// app.module.ts — health checks
15@Module({
16  imports: [
17    TerminusModule,
18    // ...
19  ],
20})
21export class AppModule {}
22
23// health/health.controller.ts
24@Controller('health')
25export class HealthController {
26  constructor(private health: HealthCheckService, private db: PrismaHealthIndicator) {}
27
28  @Get()
29  @HealthCheck()
30  check() {
31    return this.health.check([
32      () => this.db.pingCheck('database'),
33    ]);
34  }
35}