Route Handler 생성
App Router의 route.ts에서 GET·POST 핸들러를 만들고 동적 세그먼트로 사용자 API 엔드포인트를 구성합니다.
웹 애플리케이션은 클라이언트와 데이터를 주고받기 위해 서버 측 API가 필요합니다.
Next.js는 프론트엔드 렌더링뿐 아니라, App Router의 Route Handler로 HTTP 엔드포인트를 만들 수 있습니다.
덕분에 별도 백엔드를 따로 띄우지 않고도 프로젝트 내부에서 API 엔드포인트를 직접 만들고 관리할 수 있습니다.
이 절에서는 Route Handler가 필요한 경계와 GET·POST 요청을 처리하는 방법을 알아봅니다.
먼저 파일 경로, URL, HTTP 메서드 핸들러가 어떻게 하나의 API 엔드포인트로 연결되는지 보겠습니다.
Route Handler란 무엇인가요?
Route Handler는 App Router의 app 디렉터리 아래에 route.ts 파일로 만드는 HTTP 엔드포인트입니다.
Pages Router의 pages/api는 API Routes라는 별도 규칙이며 이 절에서는 다루지 않습니다.
각 파일은 서버리스 함수로 취급되며, 해당 파일 경로가 API 엔드포인트의 URL이 됩니다.
주요 특징- 파일 시스템 기반 라우팅: 파일 이름과 디렉토리 구조가 API 엔드포인트의 URL 경로에 직접 매핑됩니다. (예:
app/api/users/route.ts는/api/users경로로 접근 가능) - 서버 실행: Route Handler는 배포 플랫폼의 서버 함수나 자체 호스팅 Node.js 프로세스에서 실행됩니다.
- Node.js 환경: 기본 Node.js 런타임에서는 데이터베이스 연결과 외부 API 호출 같은 서버 작업을 수행할 수 있습니다.
- HTTP 메서드 지원:
GET,POST,PUT,DELETE등 다양한 HTTP 메서드에 대한 핸들러를 정의할 수 있습니다. - Request 및 Response 객체: Express.js와 유사하게 요청(
NextRequest) 객체와 응답(NextResponse) 객체를 통해 HTTP 요청 및 응답을 처리합니다.
왜 Route Handler가 필요한가요?
Route Handler는 HTTP 자체가 필요한 경계를 애플리케이션 안에 만들 때 사용합니다.
- 풀스택 개발 용이성: 프론트엔드와 백엔드 로직을 하나의 Next.js 프로젝트 내에서 관리할 수 있어 개발 과정을 간소화하고 생산성을 높입니다.
- 빠른 프로토타이핑: 별도의 백엔드 서버 설정 없이 빠르게 API를 만들고 테스트할 수 있습니다.
- 외부 클라이언트 연동: 모바일 앱, 웹훅과 타 서비스가 호출할 안정적인 HTTP 계약을 제공할 수 있습니다.
- 서버 컴포넌트와 역할 분리: 같은 애플리케이션의 서버 컴포넌트는 불필요하게 자기 Route Handler를 호출하지 않고 데이터 계층을 직접 사용합니다.
- 인증 및 보안: Auth.js의
auth()와 연동하여 보호된 API 엔드포인트를 구축할 수 있습니다. - 쉬운 배포: Next.js 프로젝트와 함께 Route Handler도 배포 대상에 포함됩니다.
Route Handler 생성하기
app 디렉터리 안에 API 경로를 만들고 그 끝에 route.ts를 둡니다.
각 route.ts 또는 route.js 파일은 해당 경로의 API 요청을 처리합니다.
파일 내에서는 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS와 같은 HTTP 메서드 이름의 함수를 export하여 해당 메서드 요청을 처리합니다.
GET Route Handler 생성
GET 요청은 주로 데이터를 조회할 때 사용됩니다.
실습: 사용자 목록을 반환하는 GET Route Handlersrc/lib/users-repository.ts 파일 생성목록 경로와 상세 경로가 같은 학습용 데이터를 보도록 저장소 모듈을 한 곳에 둡니다.
export interface User {
id: number;
name: string;
email: string;
}
export type UserInput = Omit<User, 'id'>;
let users: User[] = [
{ id: 1, name: '김철수', email: 'chulsoo@example.com' },
{ id: 2, name: '이영희', email: 'younghee@example.com' },
{ id: 3, name: '박민수', email: 'minsu@example.com' },
];
let nextId = 4;
export function parseUserId(value: string): number | null {
if (!/^\d+$/.test(value)) return null;
const id = Number(value);
return Number.isSafeInteger(id) && id > 0 ? id : null;
}
export function parseUserInput(value: unknown): UserInput | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
const { name, email } = value as Record<string, unknown>;
if (typeof name !== 'string' || typeof email !== 'string') return null;
const normalizedName = name.trim();
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedName || normalizedName.length > 50) return null;
if (normalizedEmail.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) return null;
return { name: normalizedName, email: normalizedEmail };
}
export function listUsers(search: string | null): User[] {
return search ? users.filter((user) => user.name.includes(search)) : users;
}
export function findUser(id: number): User | undefined {
return users.find((user) => user.id === id);
}
export function createUser(input: UserInput): User {
const user = { id: nextId++, ...input };
users = [...users, user];
return user;
}
export function updateUser(id: number, input: UserInput): User | null {
const index = users.findIndex((user) => user.id === id);
if (index === -1) return null;
const user = { id, ...input };
users = users.map((current) => current.id === id ? user : current);
return user;
}
export function deleteUser(id: number): boolean {
const nextUsers = users.filter((user) => user.id !== id);
if (nextUsers.length === users.length) return false;
users = nextUsers;
return true;
}이 배열은 한 Node.js 프로세스 안에서 Route Handler의 데이터 흐름을 연습하기 위한 대역입니다.
프로세스 재시작이나 서버리스 인스턴스 분리 뒤에는 값이 유지되지 않으므로 운영 코드에서는 데이터베이스를 사용합니다.
src/app/api/users/route.ts 파일 생성import { NextRequest, NextResponse } from 'next/server';
import { createUser, listUsers, parseUserInput } from '@/lib/users-repository';
/**
* GET 요청을 처리하는 핸들러 함수
* @param request NextRequest 객체 (선택 사항, 쿼리 파라미터 등 접근 시 사용)
*/
export async function GET(request: NextRequest) {
// 쿼리 파라미터 예시: /api/users?search=김
const searchParam = request.nextUrl.searchParams.get('search');
return NextResponse.json(listUsers(searchParam), { status: 200 });
}API 테스트:
개발 서버(npm run dev)를 실행한 후, 웹 브라우저나 Postman/Insomnia 같은 API 클라이언트에서 http://localhost:3000/api/users 경로로 접속합니다.
http://localhost:3000/api/users로 접속하면 전체 사용자 목록이 JSON 형태로 반환됩니다.http://localhost:3000/api/users?search=김으로 접속하면 '김'이 포함된 사용자만 필터링되어 반환됩니다.
POST Route Handler 생성
POST 요청은 주로 새로운 데이터를 생성하거나 서버로 데이터를 전송할 때 사용됩니다.
실습: 새로운 사용자 정보를 추가하는 POST Route Handlersrc/app/api/users/route.ts 파일에 POST 핸들러 추가:
GET 핸들러와 같은 파일에 추가합니다.
하나의 route.ts 파일은 여러 HTTP 메서드 핸들러를 포함할 수 있습니다.
export async function POST(request: NextRequest) {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ message: '올바른 JSON 본문이 필요합니다.' }, { status: 400 });
}
const input = parseUserInput(body);
if (!input) {
return NextResponse.json(
{ message: '이름과 이메일 형식을 확인해 주세요.' },
{ status: 400 },
);
}
return NextResponse.json(createUser(input), { status: 201 });
}API 테스트:
Postman, Insomnia 또는 curl 명령어를 사용하여 POST 요청을 보냅니다.
curl 예시curl -X POST \
http://localhost:3000/api/users \
-H 'Content-Type: application/json' \
-d '{
"name": "새로운 사용자",
"email": "newuser@example.com"
}'요청이 성공하면, 새로 생성된 사용자 정보와 함께 HTTP 상태 코드 201(Created)이 반환됩니다.
이후 http://localhost:3000/api/users로 GET 요청을 다시 보내면, 새로 추가된 사용자가 목록에 포함된 것을 확인할 수 있습니다.
동적 Route Handler 생성
동적 Route Handler는 URL의 일부를 파라미터로 받아 특정 리소스의 CRUD 요청을 나눕니다.
특정 ID의 사용자 조회·수정·삭제처럼 리소스를 식별해야 할 때 동적 세그먼트를 사용합니다.
파일 또는 폴더 이름을 대괄호([])로 감싸서 동적 파라미터를 정의합니다. ([slug] 또는 [id])
src/app/api/users/[id]/route.ts 파일 생성import { NextRequest, NextResponse } from 'next/server';
import {
deleteUser,
findUser,
parseUserId,
parseUserInput,
updateUser,
} from '@/lib/users-repository';
// 라우트 파라미터 타입 정의
interface Context {
params: Promise<{ id: string }>;
}
/**
* 특정 사용자 조회 (GET /api/users/[id])
* @param request NextRequest 객체
* @param context 동적 라우트 파라미터 (params.id)
*/
export async function GET(_request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
const user = findUser(id);
if (!user) {
return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
}
return NextResponse.json(user, { status: 200 });
}
/**
* 특정 사용자 업데이트 (PUT /api/users/[id])
* @param request NextRequest 객체
* @param context 동적 라우트 파라미터 (params.id)
*/
export async function PUT(request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ message: '올바른 JSON 본문이 필요합니다.' }, { status: 400 });
}
const input = parseUserInput(body);
if (!input) {
return NextResponse.json({ message: '이름과 이메일 형식을 확인해 주세요.' }, { status: 400 });
}
const user = updateUser(id, input);
if (!user) {
return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
}
return NextResponse.json(user, { status: 200 });
}
/**
* 특정 사용자 삭제 (DELETE /api/users/[id])
* @param request NextRequest 객체
* @param context 동적 라우트 파라미터 (params.id)
*/
export async function DELETE(_request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
if (!deleteUser(id)) {
return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
}
return NextResponse.json({ message: '사용자가 성공적으로 삭제되었습니다.' }, { status: 200 });
}- GET:
http://localhost:3000/api/users/1로 접속하여 ID가 1인 사용자 정보를 조회합니다. 존재하지 않는 ID(예:/api/users/99)로 접속하면 404 응답을 받습니다. - PUT: Postman 등으로
http://localhost:3000/api/users/1에 PUT 요청을 보내고 본문에{ "name": "김철수(수정됨)", "email": "chulsoo@example.com" }처럼 전체 사용자 필드를 포함합니다. - DELETE: Postman 등으로
http://localhost:3000/api/users/2에 DELETE 요청을 보내 ID가 2인 사용자를 삭제합니다.
GET, POST와 동적 경로를 만들었다면 Route Handler가 운영 코드로 바뀔 때 필요한 설계 기준을 점검해야 합니다.
Route Handler 사용 시 고려사항
- 데이터베이스 연동: 위 예시는 간단한 배열을 사용하여 데이터를 관리했지만, 실제 애플리케이션에서는 MongoDB, PostgreSQL, MySQL 등 데이터베이스와 연동하여 데이터를 영구적으로 저장하고 관리해야 합니다. Prisma, Drizzle ORM 등을 사용하여 데이터베이스 작업을 추상화할 수 있습니다.
- 오류 처리: Route Handler는 적절한 HTTP 상태 코드와 일관된 오류 응답을 반환해야 합니다. 예상 가능한 업무 오류와 예상하지 못한 서버 오류를 구분합니다.
- 데이터 유효성 검사: 클라이언트로부터 받은 데이터는 항상 유효성 검사를 수행해야 합니다. Zod, Joi 같은 라이브러리를 사용하여 스키마 기반 유효성 검사를 적용할 수 있습니다.
- 인증 및 권한 부여: 보호된 리소스의 라우트 핸들러는 Auth.js의
auth()로 요청을 감싸 로그인 상태와 역할을 확인해야 합니다. - 파일 크기: 서버리스 함수는 일반적으로 실행 시간과 번들 크기에 제한이 있습니다. 불필요한 의존성을 줄여 최적화하는 것이 좋습니다.
- Streamable Response: Next.js 13 이상에서는
NextResponse.json()외에도Response객체를 직접 반환하여 스트리밍 응답을 구현할 수도 있습니다. - CORS (Cross-Origin Resource Sharing): 다른 출처의 클라이언트가 Route Handler에 접근해야 한다면 허용 출처와 메서드를 좁게 설정합니다.
운영 Route Handler는 데이터 접근, 오류 처리, 검증과 인증을 하나의 요청 경계로 점검합니다.
아래 다이어그램은 route.ts의 요청 검증, 작업 실행과 응답 계약을 정리합니다.
아래 다이어그램은 route.ts, GET·POST 함수와 동적 세그먼트가 URL에 연결되는 방식을 보여줍니다.
Route Handler는 같은 프로젝트 안에서 HTTP 엔드포인트와 화면 코드를 함께 관리하게 합니다.
이 기능을 통해 백엔드 로직을 라우트 단위로 구현하고 프론트엔드와 데이터 계약을 맞출 수 있습니다.
아래 다이어그램은 Route Handler의 요청 진입점, 응답 형태와 오류 처리를 함께 확인합니다.