レビュー済み·難易度: 中級·更新: 2026-04-18

Next.js Route Handler で REST API(CRUD)を実装する

app/api/ 配下の Route Handler を使い、GET / POST / PUT / DELETE を備えた REST API を実装する例。インメモリデータを使ってシンプルに CRUD を示す。

nextjsapicrud

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の基本構造と HTTP メソッドの概念を理解していること

概要

Next.js App Router の Route Handler(app/api/ 配下の route.ts)を使い、GET / POST / PUT / DELETE を備えた REST API を実装する。インメモリデータを使ってシンプルに CRUD 操作を示す。nextjs-server-actions-crud との違いは、外部クライアントからも利用できる REST API として公開すること。

インストール

# 追加インストールは不要

実装

型定義

// types/todo.ts
export type Todo = {
  id: number;
  title: string;
  completed: boolean;
};

インメモリストア

// lib/todoStore.ts
import type { Todo } from "@/types/todo";

let todos: Todo[] = [
  { id: 1, title: "買い物をする", completed: false },
  { id: 2, title: "メールを返信する", completed: true },
];
let nextId = 3;

export const todoStore = {
  getAll: () => todos,
  getById: (id: number) => todos.find((t) => t.id === id),
  create: (title: string) => {
    const todo: Todo = { id: nextId++, title, completed: false };
    todos.push(todo);
    return todo;
  },
  update: (id: number, patch: Partial<Omit<Todo, "id">>) => {
    const index = todos.findIndex((t) => t.id === id);
    if (index === -1) return null;
    todos[index] = { ...todos[index], ...patch };
    return todos[index];
  },
  delete: (id: number) => {
    const index = todos.findIndex((t) => t.id === id);
    if (index === -1) return false;
    todos.splice(index, 1);
    return true;
  },
};

一覧・作成 API(GET / POST)

// app/api/todos/route.ts
import { NextResponse } from "next/server";
import { todoStore } from "@/lib/todoStore";

export function GET() {
  return NextResponse.json(todoStore.getAll());
}

export async function POST(request: Request) {
  const body = await request.json();
  const { title } = body as { title: string };

  if (!title?.trim()) {
    return NextResponse.json({ error: "title is required" }, { status: 400 });
  }

  const todo = todoStore.create(title.trim());
  return NextResponse.json(todo, { status: 201 });
}

詳細・更新・削除 API(GET / PUT / DELETE)

// app/api/todos/[id]/route.ts
import { NextResponse } from "next/server";
import { todoStore } from "@/lib/todoStore";

type Params = { params: Promise<{ id: string }> };

export async function GET(_req: Request, { params }: Params) {
  const { id } = await params;
  const todo = todoStore.getById(Number(id));
  if (!todo) return NextResponse.json({ error: "Not Found" }, { status: 404 });
  return NextResponse.json(todo);
}

export async function PUT(request: Request, { params }: Params) {
  const { id } = await params;
  const body = await request.json();
  const updated = todoStore.update(Number(id), body);
  if (!updated) return NextResponse.json({ error: "Not Found" }, { status: 404 });
  return NextResponse.json(updated);
}

export async function DELETE(_req: Request, { params }: Params) {
  const { id } = await params;
  const ok = todoStore.delete(Number(id));
  if (!ok) return NextResponse.json({ error: "Not Found" }, { status: 404 });
  return new NextResponse(null, { status: 204 });
}

クライアントからの呼び出し例

// 一覧取得
const res = await fetch("/api/todos");
const todos = await res.json();

// 作成
const res = await fetch("/api/todos", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "新しいタスク" }),
});

// 更新
await fetch("/api/todos/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ completed: true }),
});

// 削除
await fetch("/api/todos/1", { method: "DELETE" });

ポイント

  • Route Handler は app/api/<path>/route.ts に HTTP メソッド名と同名の関数を export することで定義する
  • Next.js 15 では動的ルートの paramsPromise 型になったため、await params が必要
  • NextResponse.json() でレスポンスを返す。第2引数でステータスコードを指定できる
  • 204 No Content のように本文なしで返す場合は new NextResponse(null, { status: 204 }) を使う
  • nextjs-server-actions-crud との使い分け: 外部クライアント(モバイルアプリ等)や fetch による利用が想定される場合は Route Handler が適切

注意点

nextjs-server-actions-crud との違いは REST API(fetch 呼び出し) vs Server Actions(直接関数呼び出し)。Route Handler は外部クライアントからも利用できる。

関連サンプル