NestJS 11 Framework
Overviewβ
The application backend is built with NestJS 11, a progressive Node.js framework for building efficient, reliable server-side applications. NestJS provides opinionated architecture with dependency injection, modular structure, and excellent TypeScript support.
What is NestJS?β
NestJS provides:
- Progressive Framework: Scalable and testable from ground up
- Dependency Injection: Built-in IoC container
- Modular Architecture: Organize code into logical modules
- Decorators: TypeScript decorator patterns for clean code
- ORM Support: Works seamlessly with Prisma, TypeORM, etc.
- Built-in Validation: Works with class-validator for DTOs
- Middleware Support: Cross-cutting concerns easily
- Exception Handling: Consistent error handling
The application Backend Structureβ
Project Layoutβ
apps/api/
βββ src/
β βββ main.ts # Application entry point
β βββ app.module.ts # Root application module
β βββ app.controller.ts # App-level controller
β βββ app.service.ts # App-level service
β βββ auth/ # Authentication module
β β βββ auth.controller.ts
β β βββ auth.service.ts
β β βββ auth.module.ts
β β βββ strategies/ # JWT, Azure strategies
β β βββ guards/ # Auth guards
β βββ employees/ # Employee feature module
β β βββ employees.controller.ts
β β βββ employees.service.ts
β β βββ employees.module.ts
β β βββ entities/ # Database entities
β βββ systems/ # System management module
β βββ common/ # Shared utilities
β β βββ decorators/ # Custom decorators
β β βββ filters/ # Exception filters
β β βββ guards/ # Auth guards
β β βββ middleware/ # Global middleware
β β βββ pipes/ # Validation pipes
β βββ config/ # Configuration
β βββ prisma/ # Database service
β βββ health/ # Health check endpoint
β βββ generated/ # Auto-generated files
βββ test/
β βββ app.e2e-spec.ts
βββ prisma/
β βββ schema.prisma # Database schema
βββ nest-cli.json # NestJS CLI config
βββ tsconfig.json # TypeScript config
Application Setupβ
Main Entry Pointβ
src/main.tsβ
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global prefix
app.setGlobalPrefix('api/v1');
// Enable CORS
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
});
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw error on unknown properties
transform: true, // Auto-transform to DTO class
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Swagger/OpenAPI documentation
const config = new DocumentBuilder()
.setTitle('QMS API')
.setDescription('Quality Management System API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
// Start server
const port = process.env.PORT || 3001;
await app.listen(port);
console.log(`Application running on http://localhost:${port}`);
}
bootstrap();
Root Application Moduleβ
src/app.module.tsβ
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { EmployeesModule } from './employees/employees.module';
import { SystemsModule } from './systems/systems.module';
import { HealthModule } from './health/health.module';
import { PrismaModule } from './prisma/prisma.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
PrismaModule,
AuthModule,
EmployeesModule,
SystemsModule,
HealthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Module Architectureβ
Feature Module Exampleβ
src/employees/employees.module.tsβ
import { Module } from '@nestjs/common';
import { EmployeesController } from './employees.controller';
import { EmployeesService } from './employees.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [EmployeesController],
providers: [EmployeesService],
exports: [EmployeesService], // Export for use in other modules
})
export class EmployeesModule {}
Controller Patternβ
src/employees/employees.controller.tsβ
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ZodValidationPipe } from 'nestjs-zod';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { EmployeesService } from './employees.service';
import { CreateEmployeeSchema } from '@qms/schemas/employees/create-employee.schema';
import { UpdateEmployeeSchema } from '@qms/schemas/employees/update-employee.schema';
@Controller('employees')
@ApiTags('Employees')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class EmployeesController {
constructor(private readonly employeesService: EmployeesService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Body(new ZodValidationPipe(CreateEmployeeSchema))
createEmployeeDto: CreateEmployeeSchema,
) {
return this.employeesService.create(createEmployeeDto);
}
@Get()
async findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
@Query('department') department?: string,
) {
return this.employeesService.findAll({
page,
limit,
department,
});
}
@Get(':id')
async findById(@Param('id') id: string) {
return this.employeesService.findById(id);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body(new ZodValidationPipe(UpdateEmployeeSchema))
updateEmployeeDto: UpdateEmployeeSchema,
) {
return this.employeesService.update(id, updateEmployeeDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(@Param('id') id: string) {
return this.employeesService.delete(id);
}
}
Service Patternβ
src/employees/employees.service.tsβ
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class EmployeesService {
constructor(private prisma: PrismaService) {}
async create(data: CreateEmployeeRequest) {
// Check for duplicate email
const existing = await this.prisma.employee.findUnique({
where: { email: data.email },
});
if (existing) {
throw new ConflictException('Employee with this email already exists');
}
// Create employee
const employee = await this.prisma.employee.create({
data: {
name: data.name,
email: data.email,
department: data.department,
role: data.role,
},
});
return { data: employee };
}
async findAll(filters: { page: number; limit: number; department?: string }) {
const skip = (filters.page - 1) * filters.limit;
const [employees, total] = await Promise.all([
this.prisma.employee.findMany({
where: {
...(filters.department && { department: filters.department }),
},
skip,
take: filters.limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.employee.count({
where: {
...(filters.department && { department: filters.department }),
},
}),
]);
return {
data: employees,
meta: {
page: filters.page,
limit: filters.limit,
total,
totalPages: Math.ceil(total / filters.limit),
},
};
}
async findById(id: string) {
const employee = await this.prisma.employee.findUnique({
where: { id },
});
if (!employee) {
throw new NotFoundException(`Employee with ID ${id} not found`);
}
return { data: employee };
}
async update(id: string, data: Partial<UpdateEmployeeRequest>) {
await this.findById(id); // Verify exists
const updated = await this.prisma.employee.update({
where: { id },
data,
});
return { data: updated };
}
async delete(id: string) {
await this.findById(id); // Verify exists
await this.prisma.employee.delete({
where: { id },
});
return { statusCode: 204 };
}
}
Dependency Injectionβ
Provider Registrationβ
// Service provider
@Module({
providers: [EmployeesService],
})
// Factory provider
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
return await createConnection();
},
},
],
})
// Value provider
@Module({
providers: [
{
provide: 'CONFIG',
useValue: { apiKey: 'secret' },
},
],
})
Injecting Dependenciesβ
export class EmployeesService {
constructor(
private prisma: PrismaService,
@Inject('CONFIG') private config: ConfigType,
) {}
}
Custom Decoratorsβ
Authentication Decoratorβ
src/common/decorators/current-user.decorator.tsβ
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user; // Set by auth guard
});
Usageβ
@Get('profile')
getProfile(@CurrentUser() user: AuthContext) {
return { data: user };
}
Exception Handlingβ
Custom Exception Filterβ
src/common/filters/http-exception.filter.tsβ
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error =
typeof exceptionResponse === 'object' ? exceptionResponse : { message: exceptionResponse };
response.status(status).json({
statusCode: status,
error: {
code: this.mapStatusToCode(status),
message: error['message'] || 'An error occurred',
details: error['details'] || [],
},
});
}
private mapStatusToCode(status: number): string {
const codeMap = {
[HttpStatus.BAD_REQUEST]: 'VALIDATION_ERROR',
[HttpStatus.UNAUTHORIZED]: 'UNAUTHORIZED',
[HttpStatus.FORBIDDEN]: 'FORBIDDEN',
[HttpStatus.NOT_FOUND]: 'NOT_FOUND',
[HttpStatus.CONFLICT]: 'CONFLICT',
[HttpStatus.INTERNAL_SERVER_ERROR]: 'INTERNAL_SERVER_ERROR',
};
return codeMap[status] || 'UNKNOWN_ERROR';
}
}
Middlewareβ
Logging Middlewareβ
src/common/middleware/logging.middleware.tsβ
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Logger } from '@nestjs/common';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
private logger = new Logger();
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl, ip } = req;
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
this.logger.log(`${method} ${originalUrl} - ${statusCode} - ${duration}ms - ${ip}`);
});
next();
}
}
Register in Moduleβ
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('api');
}
}
Environment Configurationβ
Configuration Serviceβ
src/config/configuration.tsβ
import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({
port: parseInt(process.env.PORT, 10) || 3001,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
},
jwt: {
secret: process.env.JWT_ACCESS_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
},
auth: {
azureTenantId: process.env.AZURE_TENANT_ID,
azureClientId: process.env.AZURE_CLIENT_ID,
},
}));
Usageβ
constructor(
@Inject(ConfigService)
private configService: ConfigService,
) {}
getPort() {
return this.configService.get('app.port');
}
Testingβ
Unit Test Exampleβ
describe('EmployeesService', () => {
let service: EmployeesService;
let prisma: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EmployeesService,
{
provide: PrismaService,
useValue: {
employee: {
findMany: jest.fn(),
create: jest.fn(),
},
},
},
],
}).compile();
service = module.get<EmployeesService>(EmployeesService);
prisma = module.get<PrismaService>(PrismaService);
});
describe('create', () => {
it('should create an employee', async () => {
const createDto = {
name: 'John',
email: 'john@example.com',
department: 'Engineering',
role: 'Developer',
};
jest.spyOn(prisma.employee, 'create').mockResolvedValue({
id: '1',
...createDto,
createdAt: new Date(),
updatedAt: new Date(),
});
const result = await service.create(createDto);
expect(result.data.email).toBe('john@example.com');
});
});
});
Best Practicesβ
1. Keep Controllers Thinβ
// β
GOOD: Controller delegates to service
@Post()
async create(@Body() dto: CreateEmployeeDto) {
return this.employeesService.create(dto);
}
// β AVOID: Business logic in controller
@Post()
async create(@Body() dto: CreateEmployeeDto) {
const employee = new Employee();
employee.name = dto.name;
// ... lots of logic
}
2. Consistent Response Formatβ
All responses should follow a standard structure:
// β
GOOD
{
"data": { /* actual data */ },
"meta": { "timestamp": "2024-01-01T00:00:00Z" }
}
// β
GOOD (error)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": []
}
}
3. Use Type-Safe ORMsβ
Prisma provides type safety:
// β
GOOD: Type-safe queries
const employee = await this.prisma.employee.findUnique({
where: { id: '1' },
});
// β AVOID: Raw SQL or unsafe ORM
const employee = await raw('SELECT * FROM employees WHERE id = ?', ['1']);
4. Validate at Module Boundariesβ
// β
GOOD: Validate incoming data
@Post()
async create(
@Body(new ZodValidationPipe(CreateEmployeeSchema))
dto,
) {
// Type-safe here
}