「保存」ボタンを押したのに、画面が読み込み中のまま止まっている。見た目は崩れていないので、HTMLやCSSの問題ではなさそうです。
このとき、ブラウザの裏側では、画面からサーバーへデータを送る通信が失敗しているかもしれません。Webアプリでは、画面を表示するコードだけでなく、データを受け渡すAPIが動いています。
APIという言葉は広い意味で使われますが、この記事ではWebアプリでよく使うHTTP APIを扱います。自分でAPIを設計できるようになることより、エージェントが作った通信をたどり、失敗した場所を説明できることが目標です。
APIは画面と処理の受付窓口
記事を投稿するアプリを考えてみます。ブラウザへ表示されているフォームだけでは、投稿をデータベースへ保存できません。入力内容をサーバーへ送り、サーバー側で確認してから保存します。
- ブラウザの投稿フォーム
- リクエストとして
POST /api/articlesを送る - 入力内容を確認する
- データベースへ保存する
- レスポンスとして作成した記事をブラウザへ返す
この/api/articlesが、記事作成を受け付けるAPIの入口です。APIの個々の入口をendpointと呼びます。
シリーズ内のAI駆動開発で困らないためのデプロイでは、ブラウザから自分のAPIを通って外部のLLM APIへ届く流れを説明しました。ここでは、その通信の中身をもう少し細かく見ていきます。
リクエストとレスポンス
ブラウザなどがAPIへ送るものをリクエスト、APIが返すものをレスポンスと呼びます。

リクエストには、主に次の情報があります。
- どこへ送るかを表すURL
- 何をしたいかを表すHTTPメソッド
- 追加情報を持つheaders
- 送信するデータを持つbody
レスポンスには、処理結果を表すstatus code、headers、返すデータが含まれます。
Request
POST /api/articles
Content-Type: application/json
{ "title": "はじめての記事" }
Response
201 Created
{ "id": "article-123", "title": "はじめての記事" }
画面で「保存できませんでした」と表示されているだけでは、どの段階で失敗したか分かりません。ブラウザのNetworkパネルを見ると、実際のリクエストとレスポンスを確認できます。
URLを分けて読む
次のURLを例にします。
https://example.com/api/articles/123?preview=true
大まかには、次のように分けられます。
- 接続先は
https://example.com - pathは
/api/articles/123 - query parameterは
?preview=true
123の部分をpath parameterとして扱い、記事IDを表すことがあります。preview=trueはquery parameterで、表示方法や検索条件など、追加の指定に使われます。
機密情報をquery parameterへ入れるのは避けます。URLはブラウザ履歴、アクセスログ、共有されたスクリーンショットなどへ残ることがあるからです。
HTTPメソッドは操作の種類を表す
よく使うHTTPメソッドは次のとおりです。
| メソッド | よくある用途 |
|---|---|
| GET | データを取得する |
| POST | 新しいデータを作る、処理を開始する |
| PUT | データ全体を置き換える |
| PATCH | データの一部を変更する |
| DELETE | データを削除する |
これは絶対的な命名規則ではありませんが、APIを読む手掛かりになります。
GET /api/articles 記事一覧を取得
GET /api/articles/123 記事を1件取得
POST /api/articles 記事を作成
PATCH /api/articles/123 記事を変更
DELETE /api/articles/123 記事を削除
特に注意したいのは、GETを開いただけでデータが削除されるような設計です。リンクの先読みや検索エンジンのアクセスでもGETが実行されることがあるため、データを変更する操作には適切なメソッドと権限確認が必要です。
JSONはデータを渡す形式
Web APIでは、JSONという形式でデータを渡すことがよくあります。
{
"id": "article-123",
"title": "はじめての記事",
"tags": ["AI", "Next.js"],
"published": false
}
見た目はJavaScriptのオブジェクトに似ていますが、JSONには独自の決まりがあります。キーと文字列はダブルクォートで囲み、コメントや関数は書けません。
APIの返答に期待している項目がなかったり、publishedが真偽値ではなく文字列"false"になっていたりすると、画面側で不具合が起きます。TypeScriptの型を書いただけでは、実際に届いたJSONを検証したことにはなりません。
画面とAPIは、データの形について同じ約束を守る
記事を保存するAPIの修正をエージェントに頼んだあと、記事一覧のタイトルが空欄で表示されるようになりました。調べると、APIが返す項目名がtitleからnameに変わっていました。画面のほうは、変わらずtitleを読んでいます。型チェックもビルドも通っていました。
画面とAPIは、やり取りするデータの形について、同じ約束を守る必要があります。この約束をAPIの契約と呼ぶことがあります。どちらか片方だけが変わると、エラーにならないまま表示が崩れます。
// 画面側のコード
type Article = {
id: string
title: string
}
const response = await fetch("/api/articles/123")
const article: Article = await response.json()
articleにArticle型を付けていますが、これは画面側の思い込みを書いただけです。response.json()が返す値を、TypeScriptは実際には検査しません。APIがnameを返していても、型チェックは通ります。TypeScriptの型がいつ働くかは、シリーズ内のAI駆動開発で困らないためのTypeScriptで説明しています。
形を揃える方法は、大きく3つに分かれます。
| 方法 | 形を決める場所 | 食い違いに気づけるか |
|---|---|---|
| 別々に書く | 画面とAPIのそれぞれ | 気づきにくい |
| 型を1か所に置いて共有する | 共通のファイル | 型チェックで気づける |
| 仕様書や定義ファイルから作る | 仕様書・定義ファイル | 生成し直したときに気づける |

Next.jsで画面とAPIを同じリポジトリに書いている場合は、型を1か所に置いて両方からimportできます。
// src/types/article.ts
export type Article = {
id: string
title: string
published: boolean
}
// src/app/api/articles/[id]/route.ts
import type { Article } from "@/types/article"
// src/app/articles/[id]/page.tsx
import type { Article } from "@/types/article"
titleをnameに変えるときは、この型を変えます。すると、古いtitleを使っている画面とAPIの両方で型エラーが出るので、直し漏れに気づけます。
型は実行時に消えるので、受け口で検証する
型を共有しても、実際に届いたJSONを確かめたことにはなりません。型はコードを書くときとビルドするときに使われ、動いている最中には残らないからです。
実際に届いた値を確かめるには、実行中に検査する仕組みが必要です。よく使われるライブラリにZodがあります。
// src/types/article.ts
import { z } from "zod"
export const ArticleSchema = z.object({
id: z.string(),
title: z.string().min(1),
published: z.boolean(),
})
export type Article = z.infer<typeof ArticleSchema>
ArticleSchemaは、データの形を実行中に確かめるための定義です。z.inferを使うと、そこからTypeScriptの型も作れます。形を書く場所が1か所になるので、型と検査の内容がずれません。
APIで入力を受け取るときに使うと、次のようになります。
const ArticleInputSchema = ArticleSchema.omit({ id: true })
export async function POST(request: Request) {
const result = ArticleInputSchema.safeParse(await request.json())
if (!result.success) {
return Response.json({ error: "入力が正しくありません" }, { status: 400 })
}
// result.data は形が確かめられた値
}
omit({ id: true })は、idを除いた形を作ります。IDはサーバー側で決めるので、入力には含めませんsafeParseは、形が合わなくても例外を投げず、成功か失敗かを結果として返します- 形が合わなければ、処理を進めずに400を返します
外部のAPIから届くデータを画面で使う場合も、同じように検査できます。相手のサービスが返す形を変えたとき、表示が崩れる前に失敗として気づけます。

仕様書と、仕組みごと型を共有する方法
APIを別のチームやモバイルアプリ、外部の利用者が使う場合は、コードとは別に仕様書を用意することがあります。HTTP APIの仕様書でよく使われる形式がOpenAPIです。
openapi: 3.1.0
info:
title: Articles API
version: 1.0.0
paths:
/api/articles/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: 記事を1件返す
content:
application/json:
schema:
$ref: "#/components/schemas/Article"
components:
schemas:
Article:
type: object
required: [id, title, published]
properties:
id:
type: string
title:
type: string
published:
type: boolean
大まかには、次のように読みます。
pathsに、endpointとHTTPメソッドが並ぶparametersに、URLなどから受け取る値が書かれるresponsesに、status codeごとの返答が書かれるcomponents.schemasに、データの形がまとめて定義される
OpenAPIの仕様書からTypeScriptの型を生成するツールや、逆にコードから仕様書を生成するツールもあります。気を付けたいのは、どちらを正とするかです。仕様書を手で書き、コードも手で書いていると、片方だけ更新されてずれていきます。エージェントが古い仕様書を読むと、存在しないendpointを呼ぶコードを書くことがあります。
APIの形を共有する仕組みには、ほかにもいくつかあります。名前を見かけたときに、何のための仕組みか分かれば十分です。
| 仕組み | 特徴 | 向いている状況 |
|---|---|---|
| OpenAPI | HTTP APIの仕様をYAMLやJSONで書く。多くの言語向けに型を生成できる | 外部や別チームにAPIを公開する |
| tRPC | サーバーと画面の両方がTypeScriptのとき、関数を呼ぶ形で型を共有する。生成の手順が要らない | 画面とサーバーを同じリポジトリで作っている |
| GraphQL | スキーマを書き、画面が必要な項目を指定して取得する | 画面ごとに必要なデータが大きく違う |
| gRPC(Protocol Buffers) | 定義ファイルから複数の言語のコードを生成する | 言語の違うサーバーやアプリが通信する |
エージェントに「型を安全にしたい」と頼むと、これらの仕組みを新しく入れようとすることがあります。途中から切り替えるのは大きな変更になるので、まず既存のやり方で足りないかを確認します。
仕様書や定義ファイルから型を生成している場合、生成されたファイルを手で直してはいけません。次に生成したときに上書きされます。generated/のようなフォルダにある、ファイル名に.gen.tsが付いている、先頭に編集しない旨のコメントがある、といった特徴で見分けられます。形を変えたいときは、元の仕様書や定義ファイルを直してから生成し直します。
互換性を壊す変更を知っておく
APIの形を変えると、古い画面やアプリが動かなくなることがあります。これを互換性を壊す変更、英語でbreaking changeと呼びます。
| 変更 | 古い画面やアプリへの影響 |
|---|---|
| 項目を追加する | 基本的には影響しない |
| 任意だった項目を必須にする | 古い画面がその項目を送らず、エラーになる |
| 項目名を変える | 古い画面が値を読めない |
| 項目を削除する | 古い画面が値を読めない |
| 値の型を変える(数値から文字列など) | 古い画面の計算や表示が崩れる |
| 選択肢の値を追加する | 古い画面が知らない値を受け取り、表示が崩れることがある |
同じリポジトリで画面とAPIを一緒に更新していても、影響が出ることがあります。
- デプロイの途中で、新しいAPIと古い画面が同時に動く時間がある
- 利用者が開きっぱなしにしているタブは、古い画面のコードのまま新しいAPIを呼ぶ
- スマートフォンアプリは、利用者が更新するまで古いまま使われる
形を変える必要があるときは、次のような進め方があります。
- 新しい項目を追加し、古い項目もしばらく残す
- 画面が新しい項目を使うようになってから、古い項目を削除する
- 大きく変える場合は、
/api/v2/articlesのように別のバージョンとして用意する
status codeで結果を知る
HTTPのstatus codeは、リクエストがどうなったかを三桁の数字で表します。
| 範囲 | 大まかな意味 | 例 |
|---|---|---|
| 200番台 | 成功 | 200 OK、201 Created、204 No Content |
| 300番台 | 別の場所への案内など | 301 Moved Permanently、302 Found |
| 400番台 | リクエスト側の問題 | 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found |
| 500番台 | サーバー側の問題 | 500 Internal Server Error、503 Service Unavailable |
初心者が混乱しやすいのは401と403です。
401:ログイン情報がない、期限切れなど、本人確認ができていない403:本人は分かっているが、その操作をする権限がない
APIが失敗したのに200を返し、JSONの中だけに{"success": false}と書く設計もあります。しかし、ブラウザや監視ツールが成功と失敗を判断しにくくなります。エージェントには、処理結果に合うstatus codeを使っているか確認します。
Next.jsのRoute Handlerを読む
App Routerを使うNext.jsでは、route.tsにAPIの処理が書かれることがあります。
src/app/api/articles/route.ts
import { NextResponse } from "next/server"
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json(
{ id: "article-123", title: body.title },
{ status: 201 },
)
}
このPOST関数が、POST /api/articlesを処理します。GET関数を書けば、同じpathへのGETを処理できます。
読むときは、次の順に追います。
- どのメソッドを受け付けるか
- URLやbodyから何を受け取るか
- 入力を検証しているか
- 誰が実行できるかを確認しているか
- データベースや外部APIのどの処理を呼ぶか
- どのstatus codeとデータを返すか
ファイルの場所や細かな書き方は、Next.jsのバージョンやプロジェクト構成によって変わることがあります。
ブラウザからfetchする
画面側では、fetchを使ってAPIへ通信することがあります。
const response = await fetch("/api/articles", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "はじめての記事" }),
})
if (!response.ok) {
throw new Error(`保存に失敗しました: ${response.status}`)
}
const article = await response.json()
fetchは、サーバーが404や500を返しても、それだけで必ず例外になるわけではありません。そのため、response.okやresponse.statusを確認します。
また、ボタンを連打して同じPOSTが複数回送られることがあります。二重登録を画面側で防ぐだけでなく、API側でも同じ処理が重複しない設計が必要になる場合があります。
headersには通信の追加情報が入る
headersには、データ形式や認証情報などが入ります。
Content-Type: application/json
Authorization: Bearer ...
Content-Typeはbodyの形式を伝えます。Authorizationは認証情報を送るために使われることがあります。
ブラウザで動くコードへAPIキーを直接書いてはいけません。シリーズ内のデプロイの記事で説明したとおり、ブラウザへ届いたJavaScriptや通信内容は利用者が確認できます。外部サービスの秘密鍵を使う通信は、サーバー側で行います。
CORSは権限管理ではない
ブラウザは、開いているWebサイトとは異なるoriginへ通信するとき、CORSのルールを確認します。originは、基本的にscheme、host、portの組み合わせです。
https://app.example.com
https://api.example.com
この二つはhostが違うため、別originです。API側が許可するoriginやメソッドをレスポンスヘッダーで示していないと、ブラウザが返答を画面のJavaScriptへ渡さないことがあります。
CORSエラーが出たからといって、何でも*で許可すればよいわけではありません。また、CORSはAPIの認証や認可の代わりではありません。ブラウザ以外のプログラムはCORSに従わず直接通信できるため、API側で本人確認と権限確認を行います。
timeout、retry、同じ処理の重複
外部APIは、必ずすぐ返るとは限りません。通信が遅い、接続が切れる、相手側が一時的に止まることがあります。
そのため、次を決めます。
- 何秒待ったらtimeoutにするか
- どの失敗ならretryするか
- 何回までretryするか
- 同じ処理が二重に実行されても安全か
すべての失敗を自動で再試行すると、注文、課金、メール送信などが重複する危険があります。同じリクエストを繰り返しても結果が重複しない性質をidempotencyと呼びます。重要な作成処理では、idempotency keyや処理済み記録を使うことがあります。
相手が他社のサービスである場合は、失敗の扱いに加えて、呼ぶたびにかかる費用と、短時間に呼べる回数の上限も決めることになります。これらは外部APIを使うときの料金と上限(現在は公開されていません)で扱っています。
Webhookは相手から届くAPI通信
通常のAPI通信では、自分のアプリから外部サービスへ問い合わせます。Webhookでは、外部サービス側から自分のAPIへ通知が届きます。
- 決済サービスで支払いが完了する
- Webhookとして自分の
/api/webhooks/paymentへ通知が届く - 注文を支払い済みに更新する
WebhookのURLは外部からアクセスできるため、送信元を署名で確認します。画面から同じURLを開けないようにするだけでは不十分です。通知が重複した場合や、到着順が前後した場合も考えます。
Networkパネルで通信を見る
Chromeなどの開発者ツールでNetworkパネルを開き、問題の操作をもう一度行います。
確認する項目は次のとおりです。
- リクエストが実際に送られているか
- URLとHTTPメソッドは正しいか
- status codeは何か
- Request Payloadに想定したデータがあるか
- Responseにどんなエラーが返っているか
- 同じリクエストが複数回送られていないか
- 処理に何秒かかっているか
Cookie、Authorization header、個人情報、APIの返答には秘密情報が含まれる場合があります。スクリーンショットやログをエージェントへ渡す前に確認してください。
エージェントが作ったAPIを確認する
- endpointとHTTPメソッドは何か
- ブラウザ、Next.js、外部APIのどこで処理しているか
- path、query、headers、bodyのどこから値を受け取るか
- 入力値をサーバー側で検証しているか
- データの形を決めている場所はどこか(別々、共有の型、仕様書)
- 項目名や型を変えたとき、画面とAPIの両方を直したか
response.json()の結果に型を付けただけで、検証したことにしていないか- 仕様書がある場合、コードの変更と一緒に更新したか
- 生成されたファイルを手で直していないか
- 項目の削除や名前の変更など、互換性を壊す変更が含まれていないか
- 認証と権限確認をどこで行うか
- 成功と失敗に適切なstatus codeを返すか
- エラーの詳細を利用者へ出しすぎていないか
- timeoutとretryの方針があるか
- 二重送信されても安全か
- 外部から届くWebhookの署名を確認しているか
- Networkパネルとサーバーログの両方で動作を確認したか
エージェントには、次のように頼めます。
この機能で発生するAPI通信を、ブラウザから順番に図にしてください。 各通信のURL、HTTPメソッド、入力、status code、返答を示してください。
このAPIで、入力検証、認証、権限確認を行っている場所を分けて説明してください。 CORSの設定を権限確認として扱っていないことも確認してください。
この変更で、APIがやり取りするデータの項目を変更前と変更後で表にしてください。 項目の追加、削除、名前の変更、型の変更を分けて示してください。
外部から受け取るデータを、実行時に検証している場所を示してください。 検証していない入口があれば、Zodなどで検証を追加する案を出してください。
失敗時のtimeout、retry、二重実行の扱いを確認してください。 注文やメール送信など、再試行で重複する処理があれば実行前に止めてください。
APIを読むときは、まずリクエストとレスポンスを一組として見ます。どのURLへ何を送り、何が返ったのかが分かれば、画面、サーバー、データベース、外部サービスのどこで止まったのかを切り分けられます。そのうえで、やり取りするデータの形をどこで決め、どこで確かめているかまで押さえておくと、エラーにならずに表示だけが崩れる不具合も追えるようになります。
