NestJS Complete Guide | Build Scalable Node.js APIs
이 글의 핵심
NestJS brings Angular-style architecture to Node.js backend development ??modules, dependency injection, decorators, and a strong opinions on project structure. This guide covers everything you need to build production-ready APIs.
What This Guide Covers
NestJS is a structured Node.js framework that scales ??it enforces modules, dependency injection, and decorators, making large codebases maintainable. This guide covers the core building blocks with a real REST API example.
Real-world insight: Migrating from a flat Express codebase to NestJS made onboarding new developers 3× faster ??the project structure is immediately predictable to anyone who’s seen one NestJS app.
Setup
npm install -g @nestjs/cli
nest new my-api
cd my-api
npm run start:dev
Default app runs on http://localhost:3000.
Project Structure
src/
app.module.ts ??root module
main.ts ??bootstrap
users/
users.module.ts
users.controller.ts
users.service.ts
users.entity.ts
dto/
create-user.dto.ts
update-user.dto.ts
1. Modules
Modules are the organizational unit of a NestJS app. Every feature gets its own module.
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // expose to other modules
})
export class UsersModule {}
// app.module.ts
@Module({
imports: [UsersModule, PostsModule],
})
export class AppModule {}
2. Controllers
Controllers handle incoming HTTP requests and return responses.
// users/users.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete, HttpCode } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Put(':id')
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.update(+id, dto);
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}
3. Services
Services contain business logic and are injected via dependency injection.
// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './users.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private readonly repo: Repository<User>
) {}
findAll() {
return this.repo.find();
}
async findOne(id: number) {
const user = await this.repo.findOneBy({ id });
if (!user) throw new NotFoundException(`User #${id} not found`);
return user;
}
async create(dto: CreateUserDto) {
const user = this.repo.create(dto);
return this.repo.save(user);
}
async update(id: number, dto: UpdateUserDto) {
await this.findOne(id); // throws NotFoundException if not found
await this.repo.update(id, dto);
return this.findOne(id);
}
async remove(id: number) {
await this.findOne(id);
await this.repo.delete(id);
}
}
4. DTOs and Validation
DTOs define the shape of request data. Add automatic validation with class-validator:
npm install class-validator class-transformer
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsString()
role?: string;
}
Enable global validation pipe in main.ts:
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen(3000);
}
whitelist: true strips any properties not in the DTO. transform: true auto-converts query string numbers from string to number.
5. Guards (Authentication & Authorization)
Guards determine whether a request should proceed.
// auth/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) throw new UnauthorizedException();
try {
request.user = this.jwtService.verify(token);
return true;
} catch {
throw new UnauthorizedException();
}
}
}
// Apply globally
app.useGlobalGuards(new JwtAuthGuard(jwtService));
// Or per controller / route
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
6. Interceptors
Interceptors wrap request/response handling ??useful for logging, response transformation, and caching.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
map(data => ({ data, success: true })), // wrap all responses
tap(() => console.log(`Request took ${Date.now() - start}ms`)),
);
}
}
7. TypeORM Integration
npm install @nestjs/typeorm typeorm pg
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
}),
TypeOrmModule.forFeature([User]),
],
})
// users/users.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@CreateDateColumn()
createdAt: Date;
}
8. Exception Filters
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } 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();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
9. Testing
// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './users.entity';
const mockRepo = {
find: jest.fn(),
findOneBy: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should return all users', async () => {
mockRepo.find.mockResolvedValue([{ id: 1, name: 'Alice' }]);
const users = await service.findAll();
expect(users).toHaveLength(1);
});
});
Key Takeaways
| Concept | Purpose |
|---|---|
| Module | Organizes related controllers, services, and providers |
| Controller | Maps HTTP routes to service methods |
| Service | Contains business logic, injected via DI |
| DTO + Pipe | Validates and transforms incoming data |
| Guard | Blocks unauthorized requests |
| Interceptor | Wraps request/response for logging, transforms |
| Filter | Handles exceptions and formats error responses |
NestJS’s structure pays dividends in large teams and long-lived projects. The DI system makes testing straightforward ??swap any dependency with a mock in TestingModule. Start with modules and services, add guards when auth is needed, and use interceptors for cross-cutting concerns like logging.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. NestJS brings Angular-style architecture to Node.js backend development ??modules, dependency injection, decorators, and a strong opinions on project structure. This guide covers everything you need to build production-ready APIs.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
Keywords Covered in This Article (Related Search Terms)
This article covers NestJS, Node.js, TypeScript, Web-API, Backend, TypeORM.