Next.jsのAPI Routes入門:GET・POST・入力検証とエラー応答
Next.jsのAPI RoutesをTypeScriptで実装。GET・POSTの処理、HTTPメソッドとJSON入力の検証、エラー応答、静的出力との違いを学べます。
Pages RouterのAPI Routesは、src/pages/api/へ配置したファイルでHTTPリクエストを処理する機能です。画面を返すページとは異なり、JSONなどのレスポンスを返します。Expressなどの追加フレームワークは必須ではありません。
この記事はTypeScriptとPages Routerを前提にします。API Routesを本番で動かすにはNext.jsを実行できるサーバー環境が必要で、output: 'export'による静的出力では利用できません。App Routerのroute.tsで定義するRoute Handlersとは、配置場所とAPIが異なります。
APIを作成して動作を確認する
GET専用のAPIを作る
src/pages/api/hello.tsへ次のコードを保存します。このファイルは/api/helloに対応します。
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData =
| { message: string }
| { error: string };
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET']);
res.status(405).json({ error: 'GETメソッドを使用してください。' });
return;
}
res.status(200).json({ message: 'API Routesが動作しています。' });
}
API Routeは、ファイルを置くだけではGET専用になりません。req.methodを確認して、対応しないメソッドへ405を返しています。Allowヘッダーは利用できるメソッドを伝えます。
NextApiResponse<ResponseData>は、開発時にレスポンスの形を確認するための型です。送信されたリクエストの内容を自動で検証するものではありません。
POSTの入力を実行時に検証する
src/pages/api/greeting.tsを作成します。名前を受け取り、挨拶文を返すAPIです。データベースへの保存は行いません。
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = { message: string } | { error: string };
function readName(value: unknown): string | null {
if (typeof value !== 'object' || value === null || !('name' in value)) {
return null;
}
if (typeof value.name !== 'string') return null;
const name = value.name.trim();
return name.length >= 1 && name.length <= 50 ? name : null;
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
res.status(405).json({ error: 'POSTメソッドを使用してください。' });
return;
}
const mediaType = req.headers['content-type']?.split(';')[0].trim().toLowerCase();
if (mediaType !== 'application/json') {
res.status(415).json({ error: 'JSON形式で送信してください。' });
return;
}
const body: unknown = req.body;
const name = readName(body);
if (name === null) {
res.status(400).json({ error: 'nameは1〜50文字の文字列にしてください。' });
return;
}
res.setHeader('Cache-Control', 'no-store');
res.status(200).json({ message: `${name}さん、こんにちは。` });
}
export const config = {
api: { bodyParser: { sizeLimit: '10kb' } },
};
req.bodyの型はanyなので、そのままbody.nameが文字列だと思って使わず、いったんunknownとして確認しています。型アサーションのasを付けても検証の代わりにはなりません。
この例の文字数はJavaScriptのlengthで数えます。絵文字などを「見た目の1文字」として数える要件がある場合は、別の数え方が必要です。JSONの構文が壊れている場合や、サイズ上限を超えた場合は、ハンドラーの前にNext.jsのbody parserが拒否することもあります。
curlで成功と失敗を確認する
npm run devで開発サーバーを起動してから、別のターミナルで実行します。ポートが異なる場合はURLを変更してください。
curl -i http://localhost:3000/api/hello
curl -i -X POST http://localhost:3000/api/hello
curl -i http://localhost:3000/api/greeting \
-H 'Content-Type: application/json' \
-d '{"name":"TypeScript"}'
curl -i http://localhost:3000/api/greeting \
-H 'Content-Type: application/json' \
-d '{"name":42}'
上から順に200、405、200、400になることを確認します。正常なケースだけでなく、メソッド違い、型違い、空文字も試すと、入力条件を確かめられます。
ブラウザから呼び出す
src/pages/greeting.tsxに、フォームからAPIを呼ぶページを作ります。実行中の二重操作を抑え、HTTPエラーと想定外のレスポンスを確認します。
import { useState } from 'react';
import type { FormEvent } from 'react';
export default function GreetingPage() {
const [name, setName] = useState('');
const [result, setResult] = useState('');
const [pending, setPending] = useState(false);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (pending) return;
setPending(true);
setResult('');
try {
const response = await fetch('/api/greeting', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (!response.ok) throw new Error(`送信に失敗しました(${response.status})。`);
const data: unknown = await response.json();
if (typeof data !== 'object' || data === null ||
!('message' in data) || typeof data.message !== 'string') {
throw new Error('レスポンスの形式が正しくありません。');
}
setResult(data.message);
} catch (error: unknown) {
setResult(error instanceof Error ? error.message : '通信に失敗しました。');
} finally {
setPending(false);
}
}
return (
<main>
<h1>挨拶を作る</h1>
<form onSubmit={handleSubmit}>
<label>名前<input value={name} onChange={(event) => setName(event.target.value)} required maxLength={50} /></label>
<button type="submit" disabled={pending}>{pending ? '送信中' : '送信'}</button>
</form>
<p role="status">{result}</p>
</main>
);
}
公開時に考えること
APIのURLは外部から呼び出せます。画面にボタンがないことやTypeScriptの型は、アクセス制限にはなりません。保存・更新を行うAPIなら、処理内容に応じて認証、権限確認、CSRF対策、レート制限などを設計します。
API Routeのコードはサーバーで実行されますが、レスポンスへ含めた情報は利用者に届きます。秘密のキーや内部のエラー詳細を返さないようにします。また、メモリー上の配列へ保存するだけでは、プロセスの再起動や複数サーバーへの分散に対応できません。永続化が必要ならデータベースを使います。
API Routesがサーバーレス関数になるか、常駐するNode.jsサーバーで動くかは配信環境によります。静的サイトのout/へAPIコードを置いても実行されない点に注意してください。
関連記事と公式資料
入力の扱いはTypeScriptの実践的な型設計、サーバーと静的出力の違いはSSGの設定も参考になります。
APIを公開する前の確認
型定義と入力検証を分ける
TypeScriptでリクエストの型を用意しても、外部からその形のデータだけが届くわけではありません。メソッド、本文の形式、必要な値を実行時に検証し、失敗時のステータスも確かめます。
実行できる配信環境を選ぶ
Pages RouterのAPI Routesはリクエスト時の実行環境が必要です。静的なoutディレクトリを配置するだけでは動かないため、Node.jsなどの対応環境か外部APIを用意します。