デザインについての学習メモブログ

Next.js 入門 #5:Route HandlerでミニAPIを作る — バックエンドいらずの小技

記事内に広告が含まれています。

Next.js 入門 #5:Route HandlerでミニAPIを作る — バックエンドいらずの小技

前回の記事では外部APIからデータを取得する方法を学びました。

今回は逆に、APIを作る側に回ります。

「APIを作るにはNode.jsやPythonでサーバーを別途用意しないといけない」と思っていませんか?

Next.jsなら同じプロジェクトの中にAPIエンドポイントを書けます。

これが Route Handler です。

フロントエンドのコードとAPIのコードが一つのプロジェクトに共存する——この体験は最初かなり新鮮に感じるはずです。

Route Handlerの基本

Route Handlerは app/ フォルダの中に route.ts というファイルを作ることで定義します。

page.tsx と同じ要領で、ファイルの場所がそのままURLになります。

Bash
app/
├── page.tsx               localhost:3000/         (ページ)
└── api/
    └── hello/
        └── route.ts       localhost:3000/api/hello (APIエンドポイント)

📕 ルール:
同じフォルダに page.tsxroute.ts を両方置くことはできません。
ページとAPIは別のフォルダに分けてください。

はじめてのRoute Handler

まず一番シンプルなGETエンドポイントを作ってみましょう。

TypeScript
// app/api/hello/route.ts

import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ message: 'こんにちは、World!' });
}

💡 NextResponseとは?

NextResponse は、Web標準の Response APIをベースに、Next.jsが便利な機能を追加して拡張したクラス(型)です。

開発サーバーが起動している状態でブラウザから http://localhost:3000/api/hello にアクセスすると、以下のJSONが返ってきます。

JSON
{ "message": "こんにちは、World!" }

NextResponse.json() はオブジェクトをJSONに変換してレスポンスとして返すNext.js専用のヘルパーです。

GETリクエスト — データを返す

もう少し実用的な例を作りましょう。

ハードコードされたブログ記事の一覧を返すAPIです。

TypeScript
// app/api/posts/route.ts

import { NextResponse } from 'next/server';

const posts = [
  { id: 1, title: 'Next.jsを始めてみた', body: 'App Routerが直感的で使いやすい。' },
  { id: 2, title: 'Route Handlerが便利', body: '同じプロジェクトでAPIも書けてしまう。' },
  { id: 3, title: 'Tailwind CSSとの相性', body: 'クラス名でスタイルが完結するのが快適。' },
];

export async function GET() {
  return NextResponse.json(posts);
}

http://localhost:3000/api/posts にアクセスすると記事の配列が返ってきます。

💡 export default を使わず、export function GET() のように使う理由

Route Handlersの役割は、送られてきたリクエストのHTTPメソッド(GET, POST, PUT, DELETEなど)を明確に区別して「Next.jsのルーティングシステム」に伝えるためです。

もし export default を使ってしまうと、そのファイル全体で1つの関数しかエクスポートできなくなります。

これでは「GETリクエストが来たらこの処理、POSTリクエストが来たらこの処理」という振分けが難しくなってしまいます。

TypeScript
// ❌ もし default エクスポートだったら(実際のNext.jsでは動きません)
export default function handler(req: Request) {
  // 関数の中でわざわざメソッドを判定しなければならない(古いPages Routerの書き方)
  if (req.method === 'GET') { /* ... */ }
  if (req.method === 'POST') { /* ... */ }
}

// ⭕️ Route Handlers(App Router)の実際の書き方
// メソッド名がそのまま関数名になるので、Next.jsが自動で振り分けてくれる
export async function GET(request: Request) {
  return Response.json({ message: "これはGETリクエストです" });
}

export async function POST(request: Request) {
  return Response.json({ message: "これはPOSTリクエストです" });
}
ファイル名役割エクスポートのルール理由
page.ts画面(UI)を表示するexport default1つのURLに対して、表示する画面(コンポーネント)は常に1つだけだから。
route.tsAPIエンドポイントexport function GET() など1つのURL(エンドポイント)に対して、複数のHTTPメソッドを受け付ける必要があるから。

同じディレクトリに page.tsroute.ts を同時に置くことはできません。競合してしまうためです。

クエリパラメータを受け取る

/api/posts?limit=2 のようにURLにパラメータを付けて絞り込めるようにしてみましょう。

TypeScript
// app/api/posts/route.ts

import { NextResponse, NextRequest } from 'next/server';

const posts = [
  { id: 1, title: 'Next.jsを始めてみた', body: 'App Routerが直感的で使いやすい。' },
  { id: 2, title: 'Route Handlerが便利', body: '同じプロジェクトでAPIも書けてしまう。' },
  { id: 3, title: 'Tailwind CSSとの相性', body: 'クラス名でスタイルが完結するのが快適。' },
];

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const limit = searchParams.get('limit');

  const result = limit ? posts.slice(0, Number(limit)) : posts;

  return NextResponse.json(result);
}
  • /api/posts → 全件返す
  • /api/posts?limit=2 → 2件だけ返す

動的なRoute Handler — IDを受け取る

特定の記事1件を返すエンドポイント(/api/posts/1 など)も作れます。

動的ルートと同じ [id] フォルダの書き方です。

Bash
app/
└── api/
    └── posts/
        ├── route.ts           /api/posts
        └── [id]/
            └── route.ts       /api/posts/1、/api/posts/2 ...
TypeScript
// app/api/posts/[id]/route.ts

import { NextResponse } from 'next/server';

const posts = [
  { id: 1, title: 'Next.jsを始めてみた', body: 'App Routerが直感的で使いやすい。' },
  { id: 2, title: 'Route Handlerが便利', body: '同じプロジェクトでAPIも書けてしまう。' },
  { id: 3, title: 'Tailwind CSSとの相性', body: 'クラス名でスタイルが完結するのが快適。' },
];

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

export async function GET(_request: Request, { params }: Props) {
  const { id } = await params;
  const post = posts.find((p) => p.id === Number(id));

  if (!post) {
    return NextResponse.json({ error: '記事が見つかりません' }, { status: 404 });
  }

  return NextResponse.json(post);
}
  • /api/posts/1 → 記事1件のJSONを返す
  • /api/posts/999{ error: '記事が見つかりません' } を status 404 で返す

NextResponse.json() の第2引数にオプションを渡すことで、ステータスコードを指定できます。

💡 引数の_request: Request

「第二引数(params)だけを使いたい場合でも、第一引数(request)を省略することはできない」ため、使わなくても定義する必要があります。

_request のように、頭に アンダースコア(_ をつける書き方は、プログラマーの間で「引数の位置関係上、定義せざるを得ないけれど、コード内では使いません」という意思表示をする標準的なテクニックです。

TypeScriptの strict モードなどで「使っていない変数があります(noUnusedLocals)」というエラーが出るのを防ぐ役割もあります。

POSTリクエスト — データを受け取る

次はデータを受け取る側、POSTエンドポイントを作ります。

TypeScript
// app/api/posts/route.ts(GETと同じファイルにPOSTを追加)

import { NextResponse, NextRequest } from 'next/server';

const posts = [
  { id: 1, title: 'Next.jsを始めてみた', body: 'App Routerが直感的で使いやすい。' },
  { id: 2, title: 'Route Handlerが便利', body: '同じプロジェクトでAPIも書けてしまう。' },
];

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const limit = searchParams.get('limit');
  const result = limit ? posts.slice(0, Number(limit)) : posts;
  return NextResponse.json(result);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  // 簡易バリデーション
  if (!body.title || !body.body) {
    return NextResponse.json(
      { error: 'title と body は必須です' },
      { status: 400 }
    );
  }

  const newPost = {
    id: posts.length + 1,
    title: body.title,
    body: body.body,
  };

  posts.push(newPost);

  return NextResponse.json(newPost, { status: 201 });
}

同じ route.ts ファイルに GETPOST を両方 export することで、メソッドごとに処理を分けられます。

⚠️ 注意: この例では posts 配列をメモリ上で管理しているため、サーバーを再起動するとデータが消えます。実際のアプリではデータベースを使います。

フロントエンドからAPIを呼び出す

作ったAPIを実際にフロントエンドから呼び出してみましょう。

ボタンを押すと投稿を作成する例です。

クリックイベントが必要なのでClient Componentになります。

TSX
// components/CreatePostForm.tsx
"use client";

import { useState } from 'react';

export default function CreatePostForm() {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [message, setMessage] = useState('');

  const handleSubmit = async () => {
    if (!title || !body) {
      setMessage('タイトルと本文を入力してください');
      return;
    }

    const res = await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, body }),
    });

    if (res.ok) {
      const newPost = await res.json();
      setMessage(`記事「${newPost.title}」を作成しました!`);
      setTitle('');
      setBody('');
    } else {
      const error = await res.json();
      setMessage(`エラー:${error.error}`);
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '500px' }}>
      <h2>新しい記事を投稿</h2>
      <input
        type="text"
        placeholder="タイトル"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
      />
      <textarea
        placeholder="本文"
        value={body}
        onChange={(e) => setBody(e.target.value)}
        rows={4}
        style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
      />
      <button
        onClick={handleSubmit}
        style={{ padding: '0.5rem 1rem', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
      >
        投稿する
      </button>
      {message && <p style={{ color: '#333' }}>{message}</p>}
    </div>
  );
}

このコンポーネントを app/posts/new/page.tsx などで使うだけで、フォームからAPIへ投稿できるようになります。

TSX
// app/posts/new/page.tsx

import CreatePostForm from '@/components/CreatePostForm';

export default function NewPostPage() {
  return (
    <main>
      <CreatePostForm />
    </main>
  );
}

Route Handlerで使えるHTTPメソッド一覧

route.ts では以下のHTTPメソッドに対応する関数を export できます。

メソッド主な用途
GETデータの取得
POSTデータの新規作成
PUTデータの全体更新
PATCHデータの部分更新
DELETEデータの削除

必要なメソッドだけ書けばOKです。

定義していないメソッドでアクセスがあると、Next.jsが自動で405(Method Not Allowed)を返します。


ここまでのフォルダ構成

Bash
app/
├── api/
   └── posts/
       ├── route.ts           GET /api/posts、POST /api/posts
       └── [id]/
           └── route.ts       GET /api/posts/:id
└── posts/
    └── new/
        └── page.tsx           投稿フォームページ

components/
└── CreatePostForm.tsx          Client Component(フォーム)

まとめ

この記事で学んだこと:

  • route.ts を置くだけでAPIエンドポイントが作れる
  • GETPOST など、HTTPメソッドごとに関数を export して処理を分ける
  • NextResponse.json() でJSONレスポンスを返す。第2引数でステータスコードを指定できる
  • クエリパラメータnew URL(request.url).searchParams で取得する
  • 動的なID[id] フォルダで受け取れる(ページのルーティングと同じ仕組み)
  • Route Handlerで作ったAPIはフロントエンドから fetch('/api/...') で呼び出せる

次の記事ではServer Actionsを使って、APIエンドポイントすら書かずにサーバー側の処理をフォームから直接呼び出す方法を学びます。

Route Handlerとの使い分けも整理しましょう。