NestJS 컨트롤러와 라우팅 학습

컨트롤러(Controller)란?

컨트롤러는 클라이언트의 요청(Request)을 처리하고 응답(Response)을 반환하는 역할을 합니다. @Controller() 데코레이터로 정의됩니다.

기본 컨트롤러 생성

CLI로 컨트롤러 생성

# 기본 컨트롤러 생성
nest g controller users

# 특정 폴더에 생성
nest g controller modules/users

# 테스트 파일 없이 생성
nest g controller users --no-spec

기본 구조

import { Controller, Get } from '@nestjs/common';

@Controller('users')
export class UsersController {
@Get()
findAll() {
return 'This action returns all users';
}
}

라우팅(Routing)

기본 라우트

import { Controller, Get, Post, Put, Delete } from '@nestjs/common';

@Controller('users')
export class UsersController {
// GET /users
@Get()
findAll() {
return 'Returns all users';
}

// POST /users
@Post()
create() {
return 'Creates a new user';
}

// PUT /users
@Put()
update() {
return 'Updates a user';
}

// DELETE /users
@Delete()
remove() {
return 'Removes a user';
}
}

경로 매개변수(Path Parameters)

import { Controller, Get, Post, Delete, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
// GET /users/:id
@Get(':id')
findOne(@Param('id') id: string) {
return `Returns user with id: ${id}`;
}

// GET /users/:userId/posts/:postId
@Get(':userId/posts/:postId')
getUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return `User ${userId}'s post ${postId}`;
}

// DELETE /users/:id
@Delete(':id')
remove(@Param('id') id: string) {
return `Removes user with id: ${id}`;
}

// 전체 params 객체 받기
@Get(':id/profile')
getProfile(@Param() params: any) {
return `User profile: ${params.id}`;
}
}

쿼리 매개변수(Query Parameters)

import { Controller, Get, Query } from '@nestjs/common';

@Controller('users')
export class UsersController {
// GET /users?page=1&limit=10
@Get()
findAll(
@Query('page') page: string,
@Query('limit') limit: string,
) {
return `Page: ${page}, Limit: ${limit}`;
}

// 전체 쿼리 객체 받기
@Get('search')
search(@Query() query: any) {
return `Search with: ${JSON.stringify(query)}`;
}

// 타입 변환과 기본값
@Get('list')
list(
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
) {
return { page, limit };
}
}

요청 본문(Request Body)

import { Controller, Post, Body } from '@nestjs/common';

interface CreateUserDto {
name: string;
email: string;
age: number;
}

@Controller('users')
export class UsersController {
// POST /users
@Post()
create(@Body() createUserDto: CreateUserDto) {
return {
message: 'User created',
user: createUserDto,
};
}

// 특정 필드만 받기
@Post('register')
register(
@Body('email') email: string,
@Body('password') password: string,
) {
return { email, password };
}
}

HTTP 메서드 데코레이터

import {
Controller,
Get,
Post,
Put,
Delete,
Patch,
Options,
Head,
All,
} from '@nestjs/common';

@Controller('api')
export class ApiController {
@Get()
get() {
return 'GET request';
}

@Post()
post() {
return 'POST request';
}

@Put()
put() {
return 'PUT request';
}

@Delete()
delete() {
return 'DELETE request';
}

@Patch()
patch() {
return 'PATCH request';
}

@Options()
options() {
return 'OPTIONS request';
}

@Head()
head() {
return 'HEAD request';
}

// 모든 HTTP 메서드 처리
@All()
all() {
return 'All HTTP methods';
}
}

요청 객체(Request Object)

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('info')
export class InfoController {
@Get()
getInfo(@Req() request: Request) {
return {
headers: request.headers,
query: request.query,
params: request.params,
body: request.body,
url: request.url,
method: request.method,
};
}
}

특정 헤더 가져오기

import { Controller, Get, Headers } from '@nestjs/common';

@Controller('auth')
export class AuthController {
// 특정 헤더 가져오기
@Get('check')
checkAuth(@Headers('authorization') auth: string) {
return { authorization: auth };
}

// 모든 헤더 가져오기
@Get('headers')
getAllHeaders(@Headers() headers: Record<string, string>) {
return headers;
}
}

응답 제어

상태 코드 설정

import { Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('users')
export class UsersController {
// 기본 201 대신 200 반환
@Post()
@HttpCode(HttpStatus.OK)
create() {
return 'User created';
}

// 204 No Content
@Post('bulk')
@HttpCode(HttpStatus.NO_CONTENT)
createBulk() {
// 응답 본문 없음
}
}

커스텀 헤더 설정

import { Controller, Get, Header } from '@nestjs/common';

@Controller('files')
export class FilesController {
@Get()
@Header('Cache-Control', 'no-cache, no-store')
@Header('Content-Type', 'application/json')
getFiles() {
return { files: [] };
}
}

리다이렉트

import { Controller, Get, Redirect } from '@nestjs/common';

@Controller('redirect')
export class RedirectController {
// 정적 리다이렉트
@Get('old')
@Redirect('https://example.com', 301)
oldRoute() {}

// 동적 리다이렉트
@Get('dynamic')
@Redirect('https://example.com', 302)
dynamicRedirect() {
const shouldRedirect = true;
if (shouldRedirect) {
return { url: 'https://other-url.com', statusCode: 301 };
}
}
}

응답 객체 직접 사용

import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller('custom')
export class CustomController {
@Get()
customResponse(@Res() res: Response) {
res.status(200).json({
message: 'Custom response',
timestamp: new Date().toISOString(),
});
}

// passthrough 옵션으로 NestJS 기능과 함께 사용
@Get('mixed')
mixedResponse(@Res({ passthrough: true }) res: Response) {
res.cookie('token', 'abc123');
return { message: 'Cookie set' };
}
}

비동기 처리

import { Controller, Get, Post, Body } from '@nestjs/common';

@Controller('users')
export class UsersController {
// Promise 반환
@Get()
async findAll(): Promise<any[]> {
return await this.fetchUsers();
}

// Observable 반환 (RxJS)
@Get('stream')
findAllStream(): Observable<any[]> {
return of([]);
}

private async fetchUsers() {
// 비동기 작업
return [];
}
}

라우트 와일드카드

import { Controller, Get } from '@nestjs/common';

@Controller('files')
export class FilesController {
// /files/abc, /files/xyz 등 모두 매칭
@Get('*')
getFile() {
return 'Any file route';
}

// /files/image.jpg, /files/doc.pdf 등
@Get('*.jpg')
getImage() {
return 'JPG image';
}
}

하위 도메인 라우팅

import { Controller, Get } from '@nestjs/common';

@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index() {
return 'Admin panel';
}
}

@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return `Account: ${account}`;
}
}

실전 예제: RESTful API

import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';

interface User {
id: number;
name: string;
email: string;
}

interface CreateUserDto {
name: string;
email: string;
}

interface UpdateUserDto {
name?: string;
email?: string;
}

interface PaginationQuery {
page: number;
limit: number;
}

@Controller('api/users')
export class UsersController {
private users: User[] = [];

// GET /api/users?page=1&limit=10
@Get()
findAll(@Query() query: PaginationQuery) {
const { page = 1, limit = 10 } = query;
return {
data: this.users.slice((page - 1) * limit, page * limit),
meta: {
total: this.users.length,
page,
limit,
},
};
}

// GET /api/users/:id
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.users.find((u) => u.id === +id);
if (!user) {
throw new Error('User not found');
}
return user;
}

// POST /api/users
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
const newUser: User = {
id: this.users.length + 1,
...createUserDto,
};
this.users.push(newUser);
return newUser;
}

// PUT /api/users/:id
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const userIndex = this.users.findIndex((u) => u.id === +id);
if (userIndex === -1) {
throw new Error('User not found');
}
this.users[userIndex] = {
...this.users[userIndex],
...updateUserDto,
};
return this.users[userIndex];
}

// DELETE /api/users/:id
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
const userIndex = this.users.findIndex((u) => u.id === +id);
if (userIndex === -1) {
throw new Error('User not found');
}
this.users.splice(userIndex, 1);
}

// GET /api/users/:userId/posts
@Get(':userId/posts')
getUserPosts(@Param('userId') userId: string) {
return {
userId: +userId,
posts: [],
};
}

// POST /api/users/search
@Post('search')
@HttpCode(HttpStatus.OK)
search(@Body('keyword') keyword: string) {
return this.users.filter(
(u) =>
u.name.includes(keyword) || u.email.includes(keyword),
);
}
}

라우트 순서

라우트는 선언된 순서대로 평가되므로 순서가 중요합니다.

@Controller('users')
export class UsersController {
// 이 순서가 중요!
@Get('newest') // 먼저 선언
getNewest() {
return 'Newest users';
}

@Get(':id') // 나중에 선언
findOne(@Param('id') id: string) {
return `User ${id}`;
}
}

만약 순서가 반대라면 GET /users/newest:id 라우트에 매칭됩니다.

DTO(Data Transfer Object) 사용

// create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
age: number;
}

// update-user.dto.ts
export class UpdateUserDto {
name?: string;
email?: string;
age?: number;
}

// users.controller.ts
import { Controller, Post, Put, Body, Param } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
@Post()
create(@Body() createUserDto: CreateUserDto) {
return createUserDto;
}

@Put(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
) {
return { id, ...updateUserDto };
}
}

컨트롤러에서 서비스 사용

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-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() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}

정리

  • 컨트롤러는 @Controller() 데코레이터로 정의
  • HTTP 메서드 데코레이터로 라우트 처리
  • @Param(), @Query(), @Body()로 요청 데이터 추출
  • @HttpCode(), @Header(), @Redirect()로 응답 제어
  • DTO를 사용하여 데이터 구조 정의
  • 라우트 선언 순서가 중요
  • 서비스와 함께 사용하여 비즈니스 로직 분리

다음 단계

컨트롤러를 이해했다면:

  • 서비스에서 비즈니스 로직 구현하기
  • 의존성 주입으로 컨트롤러와 서비스 연결하기
  • 파이프로 요청 데이터 검증하기
  • 예외 필터로 에러 처리하기
Share