🪟

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つ:

  1. API キーは process.env 経由でサーバー側だけが知っている
  2. 入力を Zod でバリデーション して、変なデータを LLM に投げない
  3. 必要な情報だけを返す(生のレスポンスをそのまま返さない)

クライアント側(責務: ユーザー操作と自分のサーバーへの問い合わせだけ)

"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