Skip to main content

NestJS Architecture

NestJS brings an opinionated, Angular-inspired architecture to Node.js backends. Understanding the pieces helps you structure large applications that teams can navigate and extend.

The Big Picture

Request → Middleware → Guards → Interceptors → Pipes → Controller → Service → Repository

Response ← Interceptors ← Controller ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← Service

Each piece has a single responsibility:

LayerResponsibilityDecorator
ModuleGroups related components@Module()
ControllerRoute handling, HTTP layer@Controller()
ServiceBusiness logic@Injectable()
RepositoryData access@Injectable()
GuardAuthorization@UseGuards()
PipeValidation/transformation@UsePipes()
InterceptorCross-cutting concerns@UseInterceptors()
MiddlewareRequest preprocessingconfigure()

Modules

Modules are the fundamental organizational unit. Every NestJS app has at least one: AppModule.

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
imports: [PrismaModule], // other modules this depends on
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService], // what other modules can use
})
export class PostsModule {}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';

@Module({
imports: [PostsModule, UsersModule, AuthModule],
})
export class AppModule {}

Feature Module Structure

NestJS convention — one directory per feature:

src/
posts/
dto/
create-post.dto.ts
update-post.dto.ts
posts.controller.ts
posts.service.ts
posts.module.ts
posts.controller.spec.ts
users/
...
auth/
...
prisma/
prisma.service.ts
prisma.module.ts
main.ts

Controllers

Controllers handle HTTP. Keep them thin — no business logic.

// src/posts/posts.controller.ts
import {
Controller, Get, Post, Patch, Delete,
Param, Body, Query, UseGuards, HttpCode, HttpStatus
} from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';

@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}

@Get()
findAll(@Query('page') page = '1', @Query('limit') limit = '10') {
return this.postsService.findAll({ page: +page, limit: +limit });
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.postsService.findOne(id);
}

@Post()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreatePostDto, @CurrentUser() userId: string) {
return this.postsService.create(dto, userId);
}

@Patch(':id')
@UseGuards(JwtAuthGuard)
update(@Param('id') id: string, @Body() dto: UpdatePostDto) {
return this.postsService.update(id, dto);
}

@Delete(':id')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.postsService.remove(id);
}
}

Services

Services contain business logic and interact with the database.

// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Injectable()
export class PostsService {
constructor(private readonly prisma: PrismaService) {}

async findAll({ page, limit }: { page: number; limit: number }) {
const [posts, total] = await this.prisma.$transaction([
this.prisma.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
include: { author: { select: { name: true } } },
}),
this.prisma.post.count(),
]);

return {
data: posts,
meta: { total, page, limit, pages: Math.ceil(total / limit) },
};
}

async findOne(id: string) {
const post = await this.prisma.post.findUnique({
where: { id },
include: { author: { select: { name: true, email: true } } },
});
if (!post) throw new NotFoundException(`Post ${id} not found`);
return post;
}

async create(dto: CreatePostDto, authorId: string) {
return this.prisma.post.create({
data: { ...dto, authorId },
});
}

async update(id: string, dto: UpdatePostDto) {
await this.findOne(id); // throws 404 if not found
return this.prisma.post.update({ where: { id }, data: dto });
}

async remove(id: string) {
await this.findOne(id);
await this.prisma.post.delete({ where: { id } });
}
}

DTOs with Validation

// src/posts/dto/create-post.dto.ts
import { IsString, IsNotEmpty, MaxLength, IsOptional, IsBoolean } from 'class-validator';

export class CreatePostDto {
@IsString()
@IsNotEmpty()
@MaxLength(200)
title: string;

@IsString()
@IsNotEmpty()
content: string;

@IsBoolean()
@IsOptional()
published?: boolean;
}

Enable global validation pipe in main.ts:

import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Exception Filters

NestJS has built-in HTTP exceptions:

import {
NotFoundException,
BadRequestException,
UnauthorizedException,
ForbiddenException,
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';

throw new NotFoundException('User not found');
throw new ConflictException('Email already in use');
throw new ForbiddenException('You cannot edit this post');

These automatically return the correct HTTP status codes with structured error bodies.