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.tsGuards & 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}