メインコンテンツまでスキップ

外部Web APIの利用

多くのWebサービスは、プログラムからHTTPリクエストを通じてアクセスできるWeb APIを提供しています。これらのAPIを活用することで、自前で実装することが困難な高度な機能をアプリケーションに組み込むことができます。

外部Web APIとは

これまでは、自分で作成したExpressサーバーに対してHTTPリクエストを送る方法を扱ってきました。しかし、「文章を理解して返答するAI」や「住所から地図を表示する機能」などを一から実装するのは現実的ではありません。

外部Web APIは、GoogleやOpenAIなどの企業が、自社のサービスの機能をHTTPリクエストで利用できるように公開しているものです。これらのAPIに対してHTTPリクエストを送ることで、高度な機能を自分のアプリケーションに組み込むことができます。

例えば、気象庁はHTTPリクエストで天気データを取得できるAPIを公開しています。

// 気象庁APIから東京の天気予報を取得する例
const response = await fetch(
"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json",
);
const data = await response.json();

代表的な外部APIには以下のようなものがあります。

分野サービス例機能
AI・機械学習OpenAI、Google Gemini、Anthropic Claude文章生成、翻訳、要約、画像認識
地図・位置情報Google Maps、国土地理院地図表示、経路検索、住所から座標への変換
決済Stripe、PayPalクレジットカード決済、請求書発行
SNS連携X(旧Twitter)、Slack投稿の取得・送信、通知送信

外部Web APIを利用する際、ほとんどのサービスではAPIキーと呼ばれる認証用の文字列が必要になります。APIキーをブラウザのJavaScriptに記述すると開発者ツールから閲覧可能になってしまうため、APIキーはサーバー側で管理し、サーバーから外部APIを呼び出すのが一般的です。

OpenRouter

この節では、OpenRouterというサービスを使用します。OpenRouterは、様々なAIモデルを統一されたAPIで利用できるサービスです。OpenAIのGPT-4やGoogleのGeminiなど、複数のモデルを同じ形式で呼び出すことができます。無料で使えるモデルも提供されています。

APIキーの取得

  1. OpenRouterにアクセスします
  2. 右上の「Sign In」からGoogleアカウント等でログインします
  3. Keysページで「Create Key」をクリックします
  4. 表示されたAPIキー(sk-or-v1-...で始まる文字列)をコピーします
APIキーの取り扱い

APIキーは機密情報です。GitHubなどの公開リポジトリにアップロードしてはいけません。

環境変数によるAPIキーの管理

APIキーをソースコードに直接記述すると、Gitにコミットしてしまう危険があります。データベースの節で扱ったように、.envファイルと環境変数を使用してAPIキーを管理します。

プロジェクトのルートに.envファイルを作成し、APIキーを記述します。

.env
OPENROUTER_API_KEY="sk-or-v1-..."

package.jsonstartスクリプトで--env-fileオプションを指定すると、.envファイルの内容が環境変数として読み込まれ、process.env.OPENROUTER_API_KEYでアクセスできるようになります。

package.json(抜粋)
{
"scripts": {
"start": "node --env-file=.env main.mjs"
}
}

.envファイルは.gitignoreに追加し、Gitの管理対象から除外します。

.gitignore
node_modules/
.env

外部APIを利用したアプリケーション

ここでは、AIを使って自然言語からタスクと期限を抽出するタスク管理アプリケーションを作成します。

サーバーの実装

Expressサーバーを作成し、タスクを管理するAPIを実装します。この例ではタスクをメモリ上の変数に保存しています。データベースの節で扱ったように、サーバーが終了するとデータは消えますが、ここでは外部APIの利用方法に集中するためこの方式を採用しています。

main.mjs
import express from "express";

const app = express();
app.use(express.json());
app.use(express.static("./public"));

// タスクをメモリ上で管理
let todos = [];
let nextId = 1;

const systemPrompt = `ユーザーの入力からタスクと期限を抽出してください。
出力は必ず2行で、1行目が期限の日時、2行目がタスクのタイトルです。
日時は「2024/1/21 10:00」のような形式にしてください。
期限の情報がない場合は1行目を空にしてください。

例:
入力: 明日の10時に会議
出力:
2024/1/21 10:00
会議

入力: 買い物に行く
出力:

買い物に行く`;

// タスク一覧を取得
app.get("/todos", (request, response) => {
response.json(todos);
});

// タスクを追加
app.post("/todos", (request, response) => {
const todo = {
id: nextId,
title: request.body.title,
dueAt: request.body.dueAt || null,
};
nextId += 1;
todos.push(todo);
response.json(todo);
});

// タスクを削除
app.delete("/todos/:id", (request, response) => {
const id = Number(request.params.id);
todos = todos.filter((todo) => todo.id !== id);
response.json({ success: true });
});

// 自然言語でタスクを追加(AI解析)
app.post("/todos/ai", async (request, response) => {
const aiResponse = await fetch(
"https://openrouter.ai/api/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.0-flash-exp:free",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: request.body.text },
],
}),
},
);

const data = await aiResponse.json();
const content = data.choices[0].message.content;
// AIの応答を改行で分割し、1行目を期限、2行目をタイトルとして取得
const firstNewLine = content.indexOf("\n");
const dueAt = content.slice(0, firstNewLine) || null;
const title = content.slice(firstNewLine + 1) || request.body.text;

const todo = {
id: nextId,
title: title,
dueAt: dueAt,
};
nextId += 1;
todos.push(todo);
response.json(todo);
});

app.listen(3000);

いくつかの新しい要素が登場しています。

  • DELETEメソッド(45行目): Fetch APIによるデータの送信の節でGETメソッドとPOSTメソッドを扱いましたが、HTTPにはデータを削除するためのDELETEメソッドもあります。Expressではapp.deleteメソッドでDELETEリクエストを受け付けます。
  • ルートパラメータ(45行目): Expressでは、パスの一部に:を付けると、その部分が可変のパラメータになります。/todos/:idと指定すると、/todos/1/todos/2といったリクエストにマッチし、実際の値をrequest.params.idで取得できます。ただし、この値は文字列なので、数値として扱うためにNumber関数で変換しています(46行目)。
  • サーバーからのfetch(53行目): Fetch APIの節ではブラウザからサーバーへリクエストを送る際にfetch関数を使用しましたが、Node.jsでもサーバー側のコードからfetch関数を使って外部のサーバーにリクエストを送ることができます。
  • Authorizationヘッダー(58行目): 多くの外部APIでは、AuthorizationヘッダーにBearerに続けてAPIキーを設定することで認証を行います。
  • OpenRouter APIの形式(61〜67行目): OpenRouter APIでは、リクエストボディにmessagesという配列を含めます。各要素はrole"system"はAIへの事前指示、"user"はユーザーの入力)とcontent(テキスト内容)を持ちます。レスポンスのdata.choices[0].message.contentにAIが生成したテキストが格納されます(72行目)。
  • indexOfslice(74〜76行目): 文字列のindexOfメソッドは、指定した文字が最初に現れる位置を返します。sliceメソッドは、指定した範囲の部分文字列を取り出します。ここでは、AIの応答を最初の改行の位置で2つに分割しています。
プロンプト設計

AIに出力形式を明確に指示することで、プログラムから解析しやすい形式のレスポンスを得ることができます。この例では改行区切りの2行形式を指定し、1行目を期限、2行目をタスクのタイトルとして解析しています。

フロントエンドの実装

publicフォルダにHTMLとJavaScriptファイルを配置します。

public/index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>AIタスク管理</title>
</head>
<body>
<h1>タスク管理</h1>
<div>
<input type="text" id="task-input" placeholder="タスクを入力" />
<button id="ai-button" type="button">AI解析</button>
<button id="add-button" type="button">追加</button>
</div>
<ul id="todo-list"></ul>
<script src="./script.js"></script>
</body>
</html>
public/script.js
// タスク一覧を取得して表示
async function loadTodos() {
const response = await fetch("/todos");
const todos = await response.json();

const todoList = document.getElementById("todo-list");
todoList.innerHTML = "";

for (const todo of todos) {
const li = document.createElement("li");

const titleSpan = document.createElement("span");
titleSpan.textContent = todo.title;
li.appendChild(titleSpan);

if (todo.dueAt) {
const timeSpan = document.createElement("span");
timeSpan.textContent = " (" + todo.dueAt + ")";
li.appendChild(timeSpan);
}

const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
deleteButton.onclick = async () => {
await fetch("/todos/" + todo.id, { method: "DELETE" });
loadTodos();
};

li.appendChild(deleteButton);
todoList.appendChild(li);
}
}

// タスクを追加
document.getElementById("add-button").onclick = async () => {
const input = document.getElementById("task-input");
if (input.value === "") return;

await fetch("/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: input.value }),
});

input.value = "";
loadTodos();
};

// AI解析でタスクを追加
document.getElementById("ai-button").onclick = async () => {
const input = document.getElementById("task-input");
if (input.value === "") return;

await fetch("/todos/ai", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: input.value }),
});

input.value = "";
loadTodos();
};

loadTodos();
  • 25行目: 削除ボタンがクリックされると、DELETEメソッドで/todos/1のようなパスにリクエストを送信し、対象のタスクを削除します。
  • 50行目以降: 「AI解析」ボタンがクリックされると、入力されたテキストをサーバーの/todos/aiにPOSTリクエストとして送信します。サーバー側でOpenRouter APIが呼び出され、AIがテキストからタスクと期限を抽出します。