Web 요청 API로 RESTful 경로 처리기 설계
네이티브 Request 및 Response 객체와 동적 세그먼트를 사용하여 GET/POST/PATCH/DELETE 처리기를 구현하는 방법을 배웁니다.
Web 요청 API로 RESTful 경로 처리기 설계은(는) CoddyKit의 무료 Next.js 15 Fullstack (App Router + Server Actions) 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Next.js 15 Fullstack (App Router + Server Actions) 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
What Route Handlers Are
In the Next.js 15 App Router, a Route Handler is a file named route.ts inside the app directory. It lets you build a REST-style API endpoint without a separate Express server.
- You export an async function named after the HTTP method:
GET,POST,PATCH,DELETE,PUT,HEAD,OPTIONS. - Each function receives a standard Web
Requestand returns a standard WebResponse. - The URL is derived from the folder path:
app/api/users/route.tsserves/api/users.
Because these are built on the platform's native Fetch API, the same mental model works on Node and Edge runtimes.
// app/api/users/route.ts
export async function GET(request: Request): Promise<Response> {
return Response.json({ users: ["Ada", "Linus"] });
}
export async function POST(request: Request): Promise<Response> {
const body = await request.json();
return Response.json({ created: body }, { status: 201 });
}Returning Responses
A handler must return a Response. Next.js gives you the native object plus a convenience helper.
Response.json(data, init)serializesdataand setsContent-Type: application/jsonautomatically.- Use the second
initargument to setstatusand customheaders. - For plain text or other payloads, construct
new Response(body, init)directly.
Picking the right status code is part of RESTful design: 200 for reads, 201 for creates, 204 for deletes with no body.
// Three idiomatic ways to respond
Response.json({ ok: true }); // 200 + JSON
Response.json({ id: 1 }, { status: 201 }); // 201 Created
new Response(null, { status: 204 }); // 204 No Content
new Response("pong", {
status: 200,
headers: { "Content-Type": "text/plain" },
});Reading the Request Body
The incoming Request is the same object you know from fetch on the client. Its body is a stream you consume once.
await request.json()parses a JSON payload.await request.text()reads raw text.await request.formData()reads multipart or URL-encoded form submissions.
You can only read the body once. If JSON parsing can fail (malformed input), wrap it in try/catch and return 400 Bad Request.
// app/api/posts/route.ts
export async function POST(request: Request) {
let body: { title?: string };
try {
body = await request.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!body.title) {
return Response.json({ error: "title is required" }, { status: 422 });
}
return Response.json({ id: 1, title: body.title }, { status: 201 });
}Reading Query Parameters
For GET requests, filters and pagination usually arrive as query-string parameters. Parse them from the request URL.
new URL(request.url)gives you aURLobject.- Its
searchParamsis aURLSearchParamsinstance withget,getAll, andhas. - Convert numeric params explicitly — every value comes in as a string.
Next.js also exposes nextUrl via NextRequest, but the native URL approach keeps your handler runtime-agnostic.
// app/api/products/route.ts -> /api/products?page=2&q=phone
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page") ?? "1");
const q = searchParams.get("q") ?? "";
return Response.json({ page, q });
}Dynamic Segments and Async Params
To handle a single resource by id, create a dynamic folder like app/api/users/[id]/route.ts. The segment is passed as the second argument.
Important Next.js 15 change: the params object is now a Promise. You must await it before reading values.
- Type the context as
{ params: Promise<{ id: string }> }. const { id } = await params;unwraps the segment.- Segment values are always strings, so parse numbers yourself.
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = { id, name: "Ada" };
return Response.json(user);
}A Full GET-by-id with 404
RESTful reads should return the resource on success and a proper 404 Not Found when it does not exist. Never return 200 with an empty body for a missing record.
- Look up the resource using the awaited
id. - If nothing is found, return
Response.json({ error }, { status: 404 }). - Otherwise return the resource with the default
200.
This handler is the canonical shape for /api/<resource>/[id].
// app/api/users/[id]/route.ts
const DB = new Map([["1", { id: "1", name: "Ada" }]]);
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = DB.get(id);
if (!user) {
return Response.json({ error: "User not found" }, { status: 404 });
}
return Response.json(user);
}PATCH for Partial Updates
PATCH updates part of a resource, while PUT replaces it entirely. For most CRUD APIs you want PATCH: the client sends only the fields that change.
- Read the dynamic
idfrom the awaited params. - Parse the JSON body for the changed fields.
- Merge the changes onto the existing record and return the updated resource with
200.
Return 404 if the target does not exist, and validate before merging.
// app/api/users/[id]/route.ts
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const existing = DB.get(id);
if (!existing) {
return Response.json({ error: "Not found" }, { status: 404 });
}
const changes = await request.json();
const updated = { ...existing, ...changes, id };
DB.set(id, updated);
return Response.json(updated);
}DELETE and 204 No Content
A successful DELETE typically returns 204 No Content with an empty body, signalling the resource is gone and there is nothing to send back.
- Confirm the resource exists; if not, return
404. - Remove it from your store.
- Return
new Response(null, { status: 204 })— do not callResponse.json, since a 204 must have no body.
Some teams prefer 200 with the deleted object; both are valid, but be consistent across your API.
// app/api/users/[id]/route.ts
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
if (!DB.has(id)) {
return Response.json({ error: "Not found" }, { status: 404 });
}
DB.delete(id);
return new Response(null, { status: 204 });
}Reading and Setting Headers
Headers carry auth tokens, content negotiation, and caching hints. The native Request.headers and Response init both use the standard Headers API.
request.headers.get("authorization")reads an incoming header (case-insensitive).- Set response headers via the
init.headersobject or aHeadersinstance. - Common ones:
Cache-Control,Location(for created resources), andWWW-Authenticate.
Returning 401 Unauthorized early keeps protected handlers clean.
// app/api/secret/route.ts
export async function GET(request: Request) {
const auth = request.headers.get("authorization");
if (auth !== "Bearer secret-token") {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json(
{ data: "top secret" },
{ headers: { "Cache-Control": "no-store" } }
);
}Caching and the Runtime
In Next.js 15, GET Route Handlers are not cached by default (this changed from Next.js 14). You opt into static caching explicitly.
- Force caching with
export const dynamic = 'force-static'. - Set a revalidation window with
export const revalidate = 60(seconds). - Reading the request body, headers, or cookies makes a handler dynamic automatically.
Choose the runtime with export const runtime = 'edge' for low-latency global execution, or the default 'nodejs' when you need Node APIs.
// app/api/quote/route.ts
export const runtime = "edge";
export const revalidate = 60; // re-generate at most once per minute
export async function GET() {
return Response.json({ quote: "Stay curious", at: Date.now() });
}A Pure Request Router You Can Run
Route Handlers are thin wrappers over Web Request/Response. To prove the model is just standard JavaScript, here is a tiny self-contained router that dispatches by method and parses an id from the path — no framework required.
- It builds a real
Request, inspectsmethodandurl, and returns aResponse. - The same logic you would put inside
GET/POSTlives here. - This runs in any modern runtime with the Fetch API available.
async function handle(req: Request): Promise<Response> {
const { pathname } = new URL(req.url);
const id = pathname.split("/").pop();
if (req.method === "GET") {
return Response.json({ id, name: "Ada" });
}
if (req.method === "DELETE") {
return new Response(null, { status: 204 });
}
return Response.json({ error: "Method Not Allowed" }, { status: 405 });
}
async function main() {
const get = await handle(new Request("http://x/api/users/1"));
console.log(get.status, await get.json());
const del = await handle(
new Request("http://x/api/users/1", { method: "DELETE" })
);
console.log(del.status); // 204
}
main();Quick Check
You are writing app/api/users/[id]/route.ts in Next.js 15. How do you correctly read the id segment inside the GET handler?
Recap
You now know how to design RESTful Route Handlers on the native Web Request/Response API in Next.js 15.
- Export method-named async functions (
GET,POST,PATCH,DELETE) fromroute.ts. - Read input with
request.json(),request.formData(), andnew URL(request.url).searchParams. - Access dynamic segments via
const { id } = await params— params is a Promise in v15. - Respond with
Response.json(data, { status }); use201for creates,404for missing resources, and204(empty body) for deletes. - Remember
GETis uncached by default; opt in withdynamic/revalidate, and pickruntimeas needed.
These patterns give you clean, predictable, framework-agnostic API endpoints.
AI 튜터와 함께 TypeScript을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 22
- 레슨
- 88
자주 묻는 질문
“Web 요청 API로 RESTful 경로 처리기 설계” 강의는 무료인가요?
네 — “Web 요청 API로 RESTful 경로 처리기 설계” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Next.js 15 Fullstack (App Router + Server Actions) 강의 전체를 잠금 해제할 수 있습니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.
“Web 요청 API로 RESTful 경로 처리기 설계”에서 뭘 배우나요?
네이티브 Request 및 Response 객체와 동적 세그먼트를 사용하여 GET/POST/PATCH/DELETE 처리기를 구현하는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 Next.js 15 Fullstack (App Router + Server Actions)을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Next.js 15 Fullstack (App Router + Server Actions)을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Next.js 15 Fullstack (App Router + Server Actions)은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“Web 요청 API로 RESTful 경로 처리기 설계” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Next.js 15 Fullstack (App Router + Server Actions) 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Next.js 15 Fullstack (App Router + Server Actions) 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Web 요청 API로 RESTful 경로 처리기 설계
- Node 런타임과 Edge 런타임의 절충점
- 처리기의 스트리밍 응답과 ReadableStream
- Zod를 활용한 요청 검증과 타입 지정 JSON 응답