前回の記事では外部APIからデータを取得する方法を学びました。
今回は逆に、APIを作る側に回ります。
「APIを作るにはNode.jsやPythonでサーバーを別途用意しないといけない」と思っていませんか?
Next.jsなら同じプロジェクトの中にAPIエンドポイントを書けます。
これが Route Handler です。
フロントエンドのコードとAPIのコードが一つのプロジェクトに共存する——この体験は最初かなり新鮮に感じるはずです。
Contents
Route Handlerの基本
Route Handlerは app/ フォルダの中に route.ts というファイルを作ることで定義します。
page.tsx と同じ要領で、ファイルの場所がそのままURLになります。
app/
├── page.tsx → localhost:3000/ (ページ)
└── api/
└── hello/
└── route.ts → localhost:3000/api/hello (APIエンドポイント)📕 ルール:
同じフォルダに page.tsx と route.ts を両方置くことはできません。
ページとAPIは別のフォルダに分けてください。
はじめてのRoute Handler
まず一番シンプルなGETエンドポイントを作ってみましょう。
// 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が返ってきます。
{ "message": "こんにちは、World!" }NextResponse.json() はオブジェクトをJSONに変換してレスポンスとして返すNext.js専用のヘルパーです。
GETリクエスト — データを返す
もう少し実用的な例を作りましょう。
ハードコードされたブログ記事の一覧を返すAPIです。
// 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リクエストが来たらこの処理」という振分けが難しくなってしまいます。
// ❌ もし 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 default | 1つのURLに対して、表示する画面(コンポーネント)は常に1つだけだから。 |
route.ts | APIエンドポイント | export function GET() など | 1つのURL(エンドポイント)に対して、複数のHTTPメソッドを受け付ける必要があるから。 |
同じディレクトリに page.ts と route.ts を同時に置くことはできません。競合してしまうためです。
クエリパラメータを受け取る
/api/posts?limit=2 のようにURLにパラメータを付けて絞り込めるようにしてみましょう。
// 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] フォルダの書き方です。
app/
└── api/
└── posts/
├── route.ts → /api/posts
└── [id]/
└── route.ts → /api/posts/1、/api/posts/2 ...// 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エンドポイントを作ります。
// 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 ファイルに GET と POST を両方 export することで、メソッドごとに処理を分けられます。
⚠️ 注意: この例では posts 配列をメモリ上で管理しているため、サーバーを再起動するとデータが消えます。実際のアプリではデータベースを使います。
フロントエンドからAPIを呼び出す
作ったAPIを実際にフロントエンドから呼び出してみましょう。
ボタンを押すと投稿を作成する例です。
クリックイベントが必要なのでClient Componentになります。
// 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へ投稿できるようになります。
// 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)を返します。
ここまでのフォルダ構成
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エンドポイントが作れるGET・POSTなど、HTTPメソッドごとに関数をexportして処理を分けるNextResponse.json()でJSONレスポンスを返す。第2引数でステータスコードを指定できる- クエリパラメータは
new URL(request.url).searchParamsで取得する - 動的なIDは
[id]フォルダで受け取れる(ページのルーティングと同じ仕組み) - Route Handlerで作ったAPIはフロントエンドから
fetch('/api/...')で呼び出せる
次の記事ではServer Actionsを使って、APIエンドポイントすら書かずにサーバー側の処理をフォームから直接呼び出す方法を学びます。
Route Handlerとの使い分けも整理しましょう。


























