Route Handler 생성
App Router의 route.ts에서 GET·POST 핸들러를 만들고 동적 세그먼트로 사용자 API 엔드포인트를 구성합니다.
웹 애플리케이션은 클라이언트와 데이터를 주고받기 위해 서버 쪽 API가 필요합니다. Next.js는 프론트엔드 렌더링뿐 아니라 App Router의 Route Handler로 HTTP 엔드포인트도 만들 수 있습니다. 덕분에 별도의 백엔드를 따로 띄우지 않고도 프로젝트 안에서 API 엔드포인트를 직접 만들고 관리할 수 있습니다.
11장에서는 Route Handler를 만들고, HTTP 메서드를 처리하고, API Proxy를 구성하고, 외부 API와 통합하는 방법을 차례로 다룹니다. 이 글에서는 Route Handler가 필요한 경우와 GET, POST 요청을 처리하는 방법을 알아봅니다.
Route Handler란 무엇인가요?
Route Handler는 App Router의 app 디렉터리 아래에 route.ts 파일로 만드는 HTTP 엔드포인트입니다. Pages Router의 pages/api는 API Routes라는 별도의 규칙이며 이 글에서는 다루지 않습니다. 파일 경로가 URL에 대응하며, 실행 형태는 배포 환경에 따라 서버리스 함수나 자체 호스팅 Node.js 프로세스가 됩니다.
Route Handler는 파일 시스템 기반으로 라우팅되어, 파일 이름과 디렉터리 구조가 API 엔드포인트의 URL 경로에 그대로 대응합니다. 예를 들어 app/api/users/route.ts는 /api/users 경로로 접근할 수 있습니다. 배포 플랫폼의 서버리스 함수나 자체 호스팅 Node.js 프로세스에서 실행되며, 기본 Node.js 런타임에서는 데이터베이스 연결과 외부 API 호출 같은 서버 작업을 할 수 있습니다. GET, POST, PUT, DELETE 등 여러 HTTP 메서드의 핸들러를 정의할 수 있고, 웹 표준 Request와 Response를 쓰며 NextRequest와 NextResponse는 이를 확장한 것입니다.
왜 Route Handler가 필요한가요?
Route Handler는 HTTP 엔드포인트 자체가 필요할 때 애플리케이션 안에 만들어 씁니다. 프론트엔드와 백엔드 로직을 하나의 Next.js 프로젝트에서 관리할 수 있어 개발 과정이 간단해지고, 별도의 백엔드 서버를 설정하지 않고도 API를 빠르게 만들어 시험할 수 있습니다. 모바일 앱, 웹훅, 다른 서비스가 호출할 안정적인 HTTP 인터페이스도 제공할 수 있습니다.
다만 같은 애플리케이션의 서버 컴포넌트는 굳이 자신의 Route Handler를 호출하지 않고 데이터 계층을 직접 씁니다. Auth.js의 auth()와 연동하면 보호된 API 엔드포인트를 만들 수 있으며, Route Handler는 Next.js 프로젝트와 함께 배포 대상에 포함됩니다.
Route Handler 생성하기
app 디렉터리 안에 API 경로를 만들고 그 끝에 route.ts를 둡니다. 기본 구조는 다음과 같습니다.
위 users/route.ts는 /api/users의 GET, POST 등을 처리하고, posts/[id]/route.ts는 /api/posts/123 같은 동적 경로를 처리합니다. 각 route.ts나 route.js 파일이 해당 경로의 API 요청을 처리하며, 같은 세그먼트의 page.tsx와 route.ts는 같은 경로를 차지하므로 함께 둘 수 없습니다. 파일 안에서는 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS 같은 HTTP 메서드 이름의 함수를 export해 해당 메서드의 요청을 처리합니다.
URL, 메서드, 입력은 따로 읽어야 합니다. GET /api/users?search=김과 POST /api/users는 같은 파일에서 서로 다른 함수를 고릅니다. ?search=김은 경로 폴더가 아니라 조회 입력이고, POST의 JSON 본문도 URL 경로와는 별개입니다. 함수가 돌려준 Response의 상태 코드와 본문이 클라이언트로 전달됩니다. 이 글의 params: Promise<...>와 await context.params는 지금의 App Router 방식이며 Next.js 15부터 바뀐 형태입니다. Pages Router의 req.query 예제를 이 코드와 섞지 않습니다.
GET Route Handler 생성
GET 요청은 주로 데이터를 조회할 때 씁니다. 사용자 목록을 돌려주는 GET Route Handler를 만들어 보겠습니다. 먼저 목록 경로와 상세 경로가 함께 가져올 학습용 저장소 코드를 src/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: '[email protected]' },
{ id: 2, name: '이영희', email: '[email protected]' },
{ id: 3, name: '박민수', email: '[email protected]' },
];
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;
}이 배열은 같은 모듈 인스턴스 안의 데이터 흐름을 연습하려는 대역입니다. 영속 저장소가 아니므로 운영 데이터는 데이터베이스에 저장합니다.
검증 함수가 무엇을 보장하는지도 짚어 두겠습니다. unknown은 요청 본문을 아직 믿을 수 없다는 타입 표현이고, 실제 안전장치는 parseUserInput의 객체, 문자열, 길이, 형식 검사입니다. 공백을 없애고 이메일을 소문자로 바꾸는 것은 이 예제의 저장 정책입니다. 간단한 정규식을 통과했다고 그 메일함의 주인이 확인되거나 이메일 중복이 금지되는 것은 아닙니다. { name, email, role: 'admin' }을 보내도 반환값에는 name과 email만 남고 role은 저장되지 않지만, 필드를 골라내는 것이 로그인이나 수정 권한을 확인하지는 않습니다.
listUsers는 받은 검색 문자열을 그대로 includes에 씁니다. 빈 search는 전체 목록을 돌려주고, 앞뒤 공백을 자동으로 없애거나 영문 대소문자를 맞추지 않습니다. 예를 들어 초기 이름에 없는 search=없는이름은 오류 대신 빈 배열과 200을 돌려줍니다.
NEXT · 11.1 · Route Handler
목록·상세 라우트는 같은 저장소 소스를 가져옵니다
users/route.ts의 GET·POST와 users/[id]/route.ts의 GET·PUT·DELETE가 users-repository.ts를 import합니다. 화살표는 소스 의존이며 HTTP 요청의 시간 순서가 아닙니다.
형식 검사와 배열 연산을 같은 모듈 소스에 둡니다. 같은 모듈 인스턴스의 예제 흐름이며, 배포된 서로 다른 인스턴스의 배열 공유·영속 저장·인증을 보장하지 않습니다.
위 그림은 사용자 Route Handler가 저장소 모듈과 어떻게 연결되는지 보여 줍니다. api/users/route.ts의 GET과 POST, api/users/[id]/route.ts의 GET, PUT, DELETE가 각각 같은 lib/users-repository.ts를 import하며, 이 저장소 모듈은 형식 검사와 메모리 데이터를 함께 가집니다. 화살표는 소스의 의존 관계이며 HTTP 요청의 시간 순서가 아닙니다. 그림의 설명처럼 이것은 같은 모듈 인스턴스 안의 예제 흐름일 뿐, 배포된 서로 다른 인스턴스 사이의 배열 공유나 영속 저장, 인증을 보장하지 않습니다.
이제 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 });
}개발 서버(npm run dev)를 실행한 뒤, 웹 브라우저나 Postman, Insomnia 같은 API 클라이언트로 http://localhost:3000/api/users에 접속하면 전체 사용자 목록이 JSON으로 돌아옵니다. http://localhost:3000/api/users?search=김으로 접속하면 이름에 "김"이 들어간 사용자만 걸러서 돌아옵니다.
더 깊이: GET 핸들러의 캐시 기본값과 Route Handler에서 빠지는 기능
GET Route Handler의 응답이 저장되는지는 버전에 따라 달랐습니다. Next.js 14까지는 요청 시점 API나 동적 설정을 쓰지 않는 GET 핸들러가 기본으로 캐시되었지만, Next.js 15 발표에서 기본값이 캐시하지 않음으로 바뀌었고 Next.js 16도 이를 따릅니다. 응답을 정적으로 만들고 싶다면 export const dynamic = 'force-static'이나 revalidate 같은 라우트 세그먼트 설정으로 직접 고릅니다. 다만 sitemap.ts, opengraph-image.tsx, icon.tsx 같은 메타데이터 파일은 특수한 Route Handler라서, 요청 시점 API나 동적 설정을 쓰지 않는 한 지금도 기본으로 정적입니다.
Route Handler는 React 컴포넌트 트리의 일부가 아니라는 점도 기억해 둡니다. 그래서 서버 컴포넌트에서는 같은 렌더링 안의 같은 GET fetch가 한 번만 실행되도록 메모이제이션되지만, Route Handler 안에서는 이 메모이제이션이 적용되지 않아 같은 요청을 두 번 쓰면 두 번 보냅니다. 한편 동적 Route Handler도 generateStaticParams를 내보내면, 돌려준 매개변수의 응답만 빌드할 때 정적으로 만들고 나머지는 요청 시점에 처리할 수 있습니다.
POST Route Handler 생성
POST 요청은 주로 새 데이터를 만들거나 서버로 데이터를 보낼 때 씁니다. 새 사용자 정보를 추가하는 POST 핸들러를 GET 핸들러와 같은 src/app/api/users/route.ts 파일에 추가합니다. 하나의 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 });
}이 핸들러는 본문을 JSON으로 읽고, parseUserInput의 형식 검사를 통과한 입력만 createUser에 넘긴 뒤 만든 사용자를 201과 함께 돌려줍니다. 두 단계 가운데 어느 하나에서 막히면 400으로 끝나며, 이 차이는 아래에서 요청 본문을 바꿔 가며 확인합니다. 브라우저 주소창으로는 GET 요청만 보낼 수 있으므로, POST 요청은 Postman, Insomnia, 또는 다음과 같은 curl 명령으로 보냅니다. -H는 본문이 JSON이라고 알리는 헤더이고, -d 뒤에는 보낼 JSON 본문을 적습니다.
curl -X POST \
http://localhost:3000/api/users \
-H 'Content-Type: application/json' \
-d '{
"name": "새로운 사용자",
"email": "[email protected]"
}'요청이 성공하면 새로 만든 사용자 정보와 함께 HTTP 상태 코드 201(Created)이 돌아옵니다. 같은 저장소 모듈 인스턴스를 쓰는 동안에는, 이후 http://localhost:3000/api/users의 GET 목록에도 새 사용자가 포함됩니다. 이것은 예제에서 기대하는 동작일 뿐, 별도 인스턴스 사이의 상태 공유를 보장하지 않습니다.
실패가 어느 단계에서 생겼는지도 확인해 봅니다. 본문이 {처럼 잘못된 JSON이면 request.json()에서 실패해 첫 번째 400 응답을 돌려줍니다. []는 유효한 JSON이므로 읽기는 성공하지만 객체 필드 검사에서 실패해 두 번째 400 응답을 돌려줍니다. 두 경우 모두 createUser까지 가지 않으므로 nextId가 늘지 않습니다. request.json()은 JSON 해석일 뿐 Content-Type 허용 검사나 업무 스키마 검사가 아니며, 이 코드에는 미디어 타입을 제한하는 조건이 없습니다. JSON 전용 API로 제한하려면 허용 형식을 검사하고, 지원하지 않는 형식의 응답 정책도 따로 정합니다.
동적 Route Handler 생성
동적 Route Handler는 URL의 일부를 파라미터로 받아 특정 리소스의 요청을 나눕니다. 특정 ID의 사용자를 조회, 수정, 삭제하는 것처럼 리소스를 식별해야 할 때 동적 세그먼트를 쓰며, 폴더 이름을 [id]나 [slug]처럼 대괄호로 감싸 정의하고 파일 이름은 route.ts로 유지합니다. 특정 사용자를 조회, 수정, 삭제하는 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 });
}이 예제는 Context 인터페이스를 직접 정의했지만, Next.js 16의 타입 생성 기능은 RouteContext<'/api/users/[id]'> 같은 전역 타입 도우미도 제공합니다.
GET은 http://localhost:3000/api/users/1에 접속해 ID가 1인 사용자 정보를 조회하며, 초기 데이터에 없는 /api/users/99 같은 ID는 404 분기로 이어집니다. PUT은 Postman 등으로 http://localhost:3000/api/users/1에 요청을 보내되, 본문에 { "name": "김철수(수정됨)", "email": "[email protected]" }처럼 전체 사용자 필드를 담습니다. DELETE는 http://localhost:3000/api/users/2에 요청을 보내 ID가 2인 사용자를 삭제합니다.
Route Handler 사용 시 고려사항
위 예시는 간단한 배열로 데이터를 관리했지만, 실제 애플리케이션에서는 MongoDB, PostgreSQL, MySQL 같은 데이터베이스와 연동해 데이터를 영구히 저장하고 관리해야 하며, Prisma나 Drizzle ORM으로 데이터베이스 작업을 추상화할 수 있습니다. Route Handler는 알맞은 HTTP 상태 코드와 일관된 오류 응답을 돌려줘야 하고, 예상할 수 있는 업무 오류와 예상하지 못한 서버 오류를 구분합니다.
클라이언트에게서 받은 데이터는 언제나 유효성을 검사해야 하며, Zod나 Joi 같은 라이브러리로 스키마 기반 검사를 적용할 수 있습니다. 보호된 리소스는 로그인 상태와 업무 권한을 확인해야 하며, Auth.js를 쓴다면 auth()로 요청을 감싸 검사할 수 있습니다. 지금의 사용자 예제에는 이 검사가 없습니다.
실행 시간과 번들 크기의 제한은 배포 플랫폼마다 확인하고, 불필요한 의존성을 줄이는 것이 좋습니다. NextResponse.json() 외에 Response 객체를 직접 돌려주어 스트리밍 응답을 구현할 수도 있습니다. 다른 출처의 클라이언트가 Route Handler에 접근해야 한다면 CORS(Cross-Origin Resource Sharing)의 허용 출처와 메서드를 좁게 설정합니다.
오류 로그에는 토큰이나 요청 본문 전체를 무조건 남기지 않고, 필요한 식별 정보만 기록합니다. 지금 예제의 입력 검증과 배열 연산이 운영 환경의 인증, 영속 저장, 로깅 정책을 대신하지는 않습니다.
본인 정보만 수정하는 정책의 작은 예
학습용 API는 인증 없이 호출됩니다. 이름과 이메일을 다루는 운영 API라면 목록 조회에도 공개 범위를 정하고, 수정과 삭제는 요청마다 서버에서 검증한 신원과 대상에 대한 권한을 확인해야 합니다. 화면에서 수정 버튼을 숨겨도 HTTP 요청은 직접 보낼 수 있습니다. 다음 함수는 관리자 예외 없이 본인만 수정할 수 있다는 정책만 표현합니다.
export function selfEditDenial(
trustedSessionUserId: number | null,
targetUserId: number,
): 401 | 403 | null {
if (trustedSessionUserId === null) return 401;
return trustedSessionUserId === targetUserId ? null : 403;
}targetUserId는 앞의 ID 검사를 통과한 값이고, trustedSessionUserId는 서버의 세션 검증을 거쳐 얻은 사용자 ID나 null이라는 전제입니다. 요청 JSON의 userId나 URL 값을 로그인한 신원으로 넣으면 이 전제가 깨집니다. 보호된 핸들러는 세션 검증과 이 정책을 수행해, 거절하면 저장을 호출하지 않고 응답하며 허용된 요청에서만 대상 조회와 수정을 진행합니다. 이 함수를 추가하는 것만으로 기존의 공개 핸들러가 보호되지는 않습니다.
이 정책에서 로그인하지 않은 요청은 401이고, 검증된 사용자 1이 대상 2를 수정하려는 경우는 403입니다. 입력이 올바른 양의 정수라도 실제로 없는 사용자 99는 기존 상세 조회에서 404이며, 형식이 잘못된 1.0은 조회하기 전에 400입니다.
정리
- Route Handler는
app디렉터리의route.ts로 만드는 HTTP 엔드포인트이며, 파일 경로가 URL이 되고 메서드 이름의 함수를export해 요청을 처리합니다. - 같은 세그먼트에는
page.tsx와route.ts를 함께 둘 수 없고, 같은 앱의 서버 컴포넌트는 자신의 Route Handler 대신 데이터 계층을 직접 씁니다. - 동적 세그먼트의 값은
await context.params로 읽으며, 문자열 ID는 형식과 범위를 검사한 뒤 씁니다. - POST와 PUT은 JSON 해석 실패와 필드 검사 실패를 각각 400으로 처리하고, 검증을 통과한 값만 저장소에 넘깁니다.
- 입력 검증은 인증과 권한 검사를 대신하지 않으므로, 보호된 API는 서버에서 검증한 세션으로 신원과 대상의 권한을 확인합니다.
연습 문제
1. 문자열 ID와 모듈 상태
저장소의 초기 상태에서 정상 사용자 한 명을 만들고, 같은 모듈 인스턴스의 상세 GET에 ID 문자열 01을 넘깁니다. 새 사용자의 ID와 상세 GET이 조회하는 ID는 각각 얼마일까요? 1.0이나 9007199254740993을 ID로 보내면 어떤 상태로 끝날까요? 그 뒤 저장소 모듈을 새로 초기화했을 때 만든 사용자가 남는지도 선언을 따라 판단해 보세요.
해설 보기
nextId가 4로 시작하므로 새 사용자는 ID 4를 받습니다. 01은 숫자로만 이루어져 있고 Number로 바꾸면 안전한 양의 정수 1이 되므로, 기존 ID 1을 조회합니다. 1.0은 숫자만 허용하는 정규식에서, 9007199254740993은 안전한 정수 검사에서 거절되어 각각 400입니다.
저장소를 새로 초기화하면 코드에 선언한 세 사용자와 nextId 4로 돌아갑니다. 같은 소스를 import한다는 사실을 영구 데이터나 서로 다른 인스턴스 사이의 공유 상태로 해석하지 않습니다.
2. POST 본문에 따라 달라지는 결과
각 실험은 저장소의 초기 상태에서 시작합니다. POST 본문이 {, [], { "name": " 새 사용자 ", "email": "[email protected]", "role": "admin" }일 때 상태 코드와 저장 호출 여부는 어떻게 달라질까요? 마지막 요청에서 응답할 ID, 이름, 이메일, role을 판단해 보세요.
해설 보기
{는 JSON 읽기 실패로 400, []는 읽은 뒤 객체 검사에서 실패해 400이며 둘 다 저장을 호출하지 않습니다. 마지막 요청은 필드 검사를 통과해 201이 되고, ID 4, 이름 새 사용자, 이메일 [email protected]을 돌려줍니다. role은 UserInput 반환값에 들어가지 않으므로 저장되지도 응답에 담기지도 않습니다. 앞의 실패 요청이 nextId를 쓰지 않는다는 점도 확인하세요.
3. 같은 이메일로 두 번 가입하기
같은 모듈 인스턴스에서 같은 이메일의 정상 POST를 두 번 보냅니다. 지금 코드가 이메일 중복을 거절할까요? 두 ID와 전체 목록의 길이는 무엇일까요? search가 빈 문자열일 때와 없는이름일 때의 GET 응답도 비교해 보세요.
해설 보기
중복 조회나 고유 제약이 없으므로 두 요청 모두 사용자를 만들며, ID는 4와 5, 목록 길이는 3 + 2 = 5입니다. 이메일 형식 검사와 중복 금지는 서로 다른 조건입니다. 빈 search는 전체 5명을 돌려주고, 이름에 일치하는 사용자가 없는 없는이름은 빈 배열을 돌려주며 둘 다 200입니다. 이 결과를 다른 모듈 인스턴스나 데이터베이스 정책으로 일반화하지 않습니다.
4. Content-Type과 PUT의 실패 경로
유효한 JSON 본문에 Content-Type이 text/plain으로 지정되었습니다. 지금의 POST에는 헤더를 읽는 조건이 없고, request.json()이 본문을 해석할 수 있다고 가정합니다. 미디어 타입 때문에 저절로 거절될까요? PUT의 대상 ID가 99이고 본문이 유효할 때와, 대상 ID가 1이고 본문이 []일 때의 응답도 비교해 보세요.
해설 보기
저절로 거절되지 않습니다. 주어진 조건에서는 JSON 읽기와 필드 검사가 성공하면 생성으로 진행하며, 미디어 타입을 제한하려면 명시적인 검사 조건이 필요합니다. PUT에서 유효한 ID 99와 유효한 본문은 updateUser가 대상을 찾지 못해 404입니다. ID 1과 본문 []는 입력 검사에서 400이며, 기존 사용자 1을 수정하지 않습니다.
5. 본인 수정 정책 적용하기
본인 수정 정책을 보호된 핸들러에서 저장하기 전에 호출합니다. 대상 ID는 검사를 통과한 2이고, 서버에서 검증된 세션 ID는 각각 null, 1, 2입니다. 각 경우의 거절 코드와 저장 가능 여부를 판단해 보세요. 클라이언트 JSON의 userId=2를 세션 ID 대신 넘기는 방식은 왜 잘못되었을까요?
해설 보기
null은 401, 1은 대상 2와 달라 403이며 둘 다 저장을 호출하지 않습니다. 2는 정책이 null을 돌려주므로 다음 단계인 대상 조회와 수정으로 진행할 수 있지만, 그렇다고 저장 성공까지 보장되지는 않습니다.
JSON의 userId는 요청자가 바꿀 수 있는 값입니다. 이를 신원 검증 결과로 취급하면, 다른 사용자가 대상 ID에 맞춰 값을 넣어 권한 검사를 통과할 수 있습니다. 서버가 검증한 세션의 신원을 요청마다 써야 합니다.
Route Handler의 파일 규칙과 지원하는 메서드는 Next.js 공식 문서의 route.js 레퍼런스에서 확인할 수 있습니다.
다음 글 HTTP 메서드 처리에서는 메서드별로 요청과 응답을 다루는 방법을 더 자세히 알아봅니다.