【第24回】画面とサーバーをつなぐ窓口を作る|API(嫁子が教えるTS講座)
今回のゴール
- サーバー側に「窓口」を1つ作れる
- ブラウザからその窓口を呼べる
- 失敗したときの返し方まで書ける
前回(サーバーサイド)で置き場所の話をしました。今回は、画面からサーバーを呼ぶ道を作ります。全体の流れは講座の目次へ。
嫁子: APIって言葉、いろんな意味で使われるから混乱しやすいよね。
今日の意味は狭くて、「サーバー側に用意した、外から呼べる窓口」のこと。それだけだよ。
1. ファイルを置けば窓口になる
第18回でページを作ったとき、フォルダの構造がそのままURLになりました。窓口も同じ仕組みです。
app/api/items/route.ts というファイルを作ります。
export async function GET() {
const items = [
{ id: 1, name: "牛乳" },
{ id: 2, name: "卵" },
];
return Response.json(items);
}
これで /api/items にアクセスすると、JSONが返ってきます。ブラウザのアドレス欄に http://localhost:3000/api/items と打てば、そのまま中身が見えます。
ページとの違いはファイル名だけです。
| ファイル名 | 何を返すか |
|---|---|
page.tsx |
画面(HTML) |
route.ts |
データ(JSONなど) |
関数名がそのままHTTPメソッドになります。GET という名前で書けばデータ取得の窓口、POST なら送信の窓口です。
2. 受け取る窓口を作る
データを送ってもらう側も、同じファイルに書けます。
export async function POST(request: Request) {
const body = await request.json();
const name = String(body.name ?? "").trim();
if (name === "") {
return Response.json({ error: "名前が空です" }, { status: 400 });
}
return Response.json({ id: Date.now(), name: name }, { status: 201 });
}
3つ、押さえるところがあります。
await request.json() で中身を取り出します。送られてくるのは文字列なので、待って変換する必要があります。第8回の await がここでも出てきます。
入力チェックをここでも書きます。前回の話の通りで、画面側のチェックは防御になりません。String(...) で包んでいるのは、数値やnullが送られてきても文字列として扱うためです。
失敗したときは番号で伝えます。この番号がステータスコードです。
| 番号 | 意味 | いつ返すか |
|---|---|---|
| 200 | 成功 | 取得できた |
| 201 | 作成した | 新しく登録した |
| 400 | 送られた内容がおかしい | 空欄、形式違い |
| 404 | 見つからない | そのIDのデータが無い |
| 500 | サーバー側の失敗 | 想定外のエラー |
嫁子: 番号を返すのは、呼ぶ側が「何が起きたか」を判断できるようにするためなの。
全部200で返して本文に「失敗しました」と書く作りだと、呼ぶ側が中身を読まないと判断できないでしょ。AIに作らせるときも「失敗時は適切なステータスコードで返して」と言っておくと、あとがラクだよ。
3. ブラウザから呼ぶ
画面側からは fetch で呼びます。第8回で少しだけ出てきたものです。
"use client";
async function addItem(name: string) {
const res = await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error ?? "追加に失敗しました");
}
return await res.json();
}
読み方はこうです。
method— どの窓口を使うか(POSTならPOST関数が動く)headers— 「JSONを送ります」という申告body— 送る中身。JSON.stringifyで文字列に変換してから渡すres.ok— 200番台ならtrue。失敗の判定に使う
res.ok の確認を忘れないでください。fetch は400や500が返ってきても例外を投げません。通信そのものが失敗したときだけ例外になります。
嫁子: ここ、AIのコードでもよく抜けるところだよ。
res.okを見ずにres.json()だけ書いてあると、エラーの本文をデータとして受け取って、そのまま画面に出そうとするの。「なぜか画面が空になる」の原因がこれだったりする。
4. サーバー側から呼ぶなら、窓口は要らない
ここは順番として大事なところです。Next.jsでは、サーバー側で動く画面から直接データを取れます。
// app/page.tsx(サーバー側)
export default async function Page() {
const items = await getItems(); // 直接呼べる
return <ul>{/* ... */}</ul>;
}
この場合、/api/items を経由する必要はありません。自分のサーバーの中で完結するなら、窓口を作らずに直接呼ぶほうが速いです。
では窓口が要るのはどんなときか。
| 窓口が要る | 窓口が要らない |
|---|---|
| ブラウザの操作で送信する(フォーム、削除ボタン) | 最初の表示に必要なデータ |
| 外部のサービスから呼ばれる | サーバー側で完結する処理 |
| スマホアプリなど別のものから使う |
嫁子: 「APIを作らないといけない」と思い込んでる人、けっこう多いと思う。
表示するだけなら要らないの。操作の結果をサーバーに反映させたいときに初めて必要になる、くらいで覚えておいて。
5. 動かして確かめる
窓口を作ったら、画面を作る前に単体で確認できます。
GETはブラウザで:アドレス欄に http://localhost:3000/api/items を打つだけです。
POSTは開発者ツールから:第9回で開いたConsoleで、こう打ちます。
await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "牛乳" })
}).then(r => r.json());
画面を作る前に窓口だけ確認しておくと、問題が起きたときの切り分けが速くなります。画面が悪いのか窓口が悪いのかを、先に分けておけるからです。
つまずきポイント
ファイル名は route.ts です。page.tsx と同じフォルダには置けません。同じURLで両方は返せないからです。
Response.json() は文字列ではなくオブジェクトを渡します。JSON.stringify は不要です。送る側(fetch の body)では必要なので、方向によって違う点に注意してください。
GETの窓口を作ったのにPOSTを送ると405が返ります。その名前の関数が無い、という意味です。
今回のまとめ
app/api/○○/route.tsを置けば窓口になる。関数名がHTTPメソッド- 受け取る側では
await request.json()で中身を取り出し、サーバー側でも入力チェックをする - 失敗はステータスコードで伝える(400、404、500)
- ブラウザからは
fetch。res.okの確認を忘れない - 表示するだけなら窓口は要らない。操作の結果を反映するときに作る
次にやること
次回はデータベースの基礎です。ここまでのデータはコードに直接書いていたので、まだ再起動すると消えます。消えない置き場所を用意します。
今回の宿題:GET の窓口を1つ作って、ブラウザのアドレス欄から開いてみてください。JSONがそのまま表示されるのを見ておくと、次回以降で「窓口が動いていない」のか「画面が悪い」のかを切り分けられるようになります。
嫁子: 窓口だけ先に作って動かす、って地味だけど効くよ。
画面まで作ってから「動きません」だと、原因の候補が一気に増えるからね。1つずつ確かめられる形にしておくと、AIに相談するときも話が早いよ。