【第24回】画面とサーバーをつなぐ窓口を作る|API(嫁子が教えるTS講座)

今回のゴール

前回(サーバーサイド)で置き場所の話をしました。今回は、画面からサーバーを呼ぶ道を作ります。全体の流れは講座の目次へ。


嫁子: 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();
}

読み方はこうです。

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が返ります。その名前の関数が無い、という意味です。


今回のまとめ


次にやること

次回はデータベースの基礎です。ここまでのデータはコードに直接書いていたので、まだ再起動すると消えます。消えない置き場所を用意します。

今回の宿題:GET の窓口を1つ作って、ブラウザのアドレス欄から開いてみてください。JSONがそのまま表示されるのを見ておくと、次回以降で「窓口が動いていない」のか「画面が悪い」のかを切り分けられるようになります。

嫁子: 窓口だけ先に作って動かす、って地味だけど効くよ。

画面まで作ってから「動きません」だと、原因の候補が一気に増えるからね。1つずつ確かめられる形にしておくと、AIに相談するときも話が早いよ。