Skip to main content

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
}

Resources​