APIってなんだっけ?— "便利な窓口さん"に責務を守らせる話(TypeScript編)
はじめに
こんにちは、YSです。
最近個人開発で LLM API を触る機会が多く、「API ってつくづく便利だな〜」と思いつつ、同時に「気を抜くと家の鍵まで渡しそうになるな〜」とも思いました。
この記事では、
- そもそも API って何だっけ
- なぜ便利なのか
- TypeScript で使うときに 責務(どこで何をやるか) を間違えると、何が起きるのか
- 個人開発で LLM API を呼ぶときの、良くない例 と マシな例
を、いつもより少しだけゆるめに整理してみます。
1. そもそも API って何
API は Application Programming Interface の略です。
日本語にすると「アプリケーション同士をつなぐ窓口」みたいな意味になります。
…と言われてもピンと来ないので、僕はいつも 「窓口さん」 だと思ってます。
| 状況 | 窓口さんに言うこと | 窓口さんがやること |
|---|---|---|
| 天気を知りたい | 「東京の今日の天気ちょうだい」 | 裏で気象データを引いてきて返してくれる |
| 翻訳したい | 「これ英語にして」 | 翻訳エンジンに投げて結果を返してくれる |
| LLM に聞きたい | 「このコードレビューして」 | モデルに渡して応答を返してくれる |
ポイントは、こちらは "中で何をやってるか" を知らなくていい ということ。
窓口さんが「分かりました」と言ってくれれば、奥の厨房で何が起きていようと、こちらにはお皿(レスポンス)が運ばれてきます。
これが API の偉いところで、自分でゼロから作らなくていい機能を、関数を呼ぶ感覚で借りてこられる のが最大の魅力です。
2. API がもたらす"開発の幅"
ちょっと前まで「天気予報アプリを作るぞ!」と言ったら、観測網から作る話になりかねなかったわけですが、今は、
const res = await fetch("https://api.example-weather.com/today?city=osaka");
const data = await res.json();
これだけで終わります。
気象庁の中の人になる必要は、もうありません。ありがたい話です。
LLM も同じで、自分で大規模言語モデルを学習する必要はなく、
const reply = await llm.chat({ model: "claude-4-5", input: "こんにちは" });
のような感じで「賢いやつ」を借りてこられます。
個人開発の幅が一気に広がる のは、ほぼ API のおかげと言ってもいい気がします。
3. ところが、TypeScript で書いていると "責務" の問題が出てくる
ここからが本題。
API は便利なんですが、「窓口さんを呼ぶ場所」を間違えると一気に怖いものに変わります。
具体的には、こういう構図が悪さをします:
本来 サーバー側でしか触ってはいけない秘密 を、ブラウザ側のコードに書いてしまう
たとえば LLM の API キー。これを Next.js の Client Component の中に書いてしまうと、こうなります:
"use client";
// ❌ やってはいけない例
const ANTHROPIC_API_KEY = "sk-ant-xxxxxxxxxxxxxxxxx";
export function ChatBox() {
const handleSend = async (text: string) => {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": ANTHROPIC_API_KEY, // ← ブラウザに丸見え
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-4-5",
messages: [{ role: "user", content: text }],
}),
});
// ...
};
}
これ、ぱっと見動きます。動いてしまうのが恐ろしいところです。
ですが、ブラウザの DevTools を開けば API キーがリクエストヘッダにそのまま載って飛んでいきます。
誰でも見られます。優しい人なら「危ないですよ」と教えてくれますが、そうでない人は 自分の財布で他人がガンガン LLM を叩く という地獄を見せてくれます。
つまり何が起きているかというと、
- 「秘密を持つ責務」 → サーバー側にあるべきもの
- 「ユーザー操作を扱う責務」 → クライアント側にあるべきもの
この 2つが Client Component に同居してしまっている わけです。
窓口さんを呼んだつもりが、窓口の 裏の鍵束ごと 玄関先に置いていた、みたいな状態。
4. ちゃんと責務を分ける ― Next.js Route Handler の例
直し方は単純で、LLM を叩く処理はサーバー側に隔離する だけです。
Next.js なら Route Handler(app/api/.../route.ts)が担当部署になります。
サーバー側(責務: 秘密を持って外部 API を叩く)
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
import { NextResponse } from "next/server";
import { z } from "zod";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!, // サーバーの環境変数。ブラウザには出ない
});
const BodySchema = z.object({
message: z.string().min(1).max(2000),
});
export async function POST(req: Request) {
const parsed = BodySchema.safeParse(await req.json());
if (!parsed.success) {
return NextResponse.json({ error: "invalid body" }, { status: 400 });
}
const result = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: parsed.data.message }],
});
const text = result.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
return NextResponse.json({ reply: text });
}
ポイントは3つ:
- API キーは
process.env経由でサーバー側だけが知っている - 入力を Zod でバリデーション して、変なデータを LLM に投げない
- 必要な情報だけを返す(生のレスポンスをそのまま返さない)
クライアント側(責務: ユーザー操作と自分のサーバーへの問い合わせだけ)
"use client";
import { useState } from "react";
export function ChatBox() {
const [input, setInput] = useState("");
const [reply, setReply] = useState("");
const handleSend = async () => {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ message: input }),
});
const data = await res.json();
setReply(data.reply);
};
return (
<div>
<textarea value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={handleSend}>送信</button>
<p>{reply}</p>
</div>
);
}
クライアントは 「自分のサーバーの /api/chat という窓口」しか知らない 状態になります。
Anthropic が裏にいることも、API キーがどんな文字列かも、ブラウザは知らなくていい。
知らないことが安全につながる という、ちょっと面白い構図です。
5. ロジックが混ざると何が悪いのか(ざっくり)
「責務を分けろ」と言われると説教くさいですが、要するに 混ざっていると壊れたときに被害がデカい という話です。
| 混ざり方 | 起きること |
|---|---|
| API キーをクライアントに置く | キー流出 → 課金爆発 |
| 認可チェックをクライアントだけでやる | DevTools でフラグを書き換えて素通り |
| バリデーションをクライアントだけでやる | curl で直接叩かれて変なデータが入る |
| 外部 API の生レスポンスをそのまま返す | 内部スキーマ・余計な情報まで漏れる |
クライアントは "信用できないユーザーの手元で動くコード"、
サーバーは "自分の家" と思っておくとだいたい間違いません。
窓口さんは家の中にいるべきで、玄関の外で営業させないこと。
6. まとめ
- API は 「アプリ同士をつなぐ窓口さん」。自分でゼロから作らなくていいのが偉い
- 個人開発の幅は API のおかげで爆発的に広がる
- でも TypeScript で組むときは 「どこで何をやる責務か」 を意識しないと、便利さがそのまま脆弱性になる
- 最低限のルール: 秘密はサーバーに置く / 入力はサーバーで検証する / 生レスポンスをそのまま返さない
- LLM API も例外ではない。むしろ キー1本で財布が直結している ぶん、責務分離の御利益が一番ハッキリ出る領域
API は便利な窓口さんですが、家の鍵まで渡してはいけない。
これだけ覚えて帰ってもらえれば、この記事の役目は果たせた気がします。
個人開発で API を触ってみて思ったこと
ここまで書いておいてアレですが、僕も最初から責務分離を意識して書けていたわけではありません。
個人開発で LLM API を初めて触ったときは、「とりあえず動かしたい」が先行して、危ないコードを平気で書いていました。
API は、使い方を間違えなければ本当に開発の幅を広げてくれる相棒です。
窓口さんには気持ちよく働いてもらいつつ、こちらも家の鍵だけはちゃんと自分で管理していきたいですね。
学生エンジニアの実体験ベースで、これからもゆるく書いていきます。
Discussion