Next.js 15 Fullstack (App Router + Server Actions) · 강의

Web 요청 API로 RESTful 경로 처리기 설계

네이티브 Request 및 Response 객체와 동적 세그먼트를 사용하여 GET/POST/PATCH/DELETE 처리기를 구현하는 방법을 배웁니다.

레슨 1/413개 단계

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 Request and returns a standard Web Response.
  • The URL is derived from the folder path: app/api/users/route.ts serves /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) serializes data and sets Content-Type: application/json automatically.
  • Use the second init argument to set status and custom headers.
  • 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 a URL object.
  • Its searchParams is a URLSearchParams instance with get, getAll, and has.
  • 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 id from 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 call Response.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.headers object or a Headers instance.
  • Common ones: Cache-Control, Location (for created resources), and WWW-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, inspects method and url, and returns a Response.
  • The same logic you would put inside GET/POST lives 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) from route.ts.
  • Read input with request.json(), request.formData(), and new URL(request.url).searchParams.
  • Access dynamic segments via const { id } = await params — params is a Promise in v15.
  • Respond with Response.json(data, { status }); use 201 for creates, 404 for missing resources, and 204 (empty body) for deletes.
  • Remember GET is uncached by default; opt in with dynamic/revalidate, and pick runtime as 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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Web 요청 API로 RESTful 경로 처리기 설계
  2. Node 런타임과 Edge 런타임의 절충점
  3. 처리기의 스트리밍 응답과 ReadableStream
  4. Zod를 활용한 요청 검증과 타입 지정 JSON 응답
← Next.js 15 Fullstack (App Router + Server Actions)(으)로 돌아가기