본문으로 건너뛰기 NestJS Complete Guide | Build Scalable Node.js APIs

NestJS Complete Guide | Build Scalable Node.js APIs

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

ConceptPurpose
ModuleOrganizes related controllers, services, and providers
ControllerMaps HTTP routes to service methods
ServiceContains business logic, injected via DI
DTO + PipeValidates and transforms incoming data
GuardBlocks unauthorized requests
InterceptorWraps request/response for logging, transforms
FilterHandles 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.


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.