Node.jsから生成AIのAPIを叩く仕事が、ここ1〜2年で急に増えました。社内ツールや業務システムのちょっとした一機能に組み込む、というレベルの依頼まで含めれば、Webエンジニアやマーケター寄りの人にとって「一度は書く」コードになりつつあります。
本記事では、Claude APIとChatGPT APIの2つを、Node.jsから呼び出すための最小構成を並べて紹介します。そのあとで、実装したあとに必ずぶつかる「プロンプトをコード内にそのまま書くと辛くなる」問題への、実務的な対処パターンをまとめます。対象読者は「Node.jsを触ったことがあり、これから生成AIのAPIを組み込みたい人」を想定しています。
コード例のモデル名やSDKの使い方は、執筆時点(2026年8月)の情報に基づいています。生成AI周辺は変化が速いので、実装時には各社の公式ドキュメントで最新の推奨モデル名・パッケージバージョンを確認してください。
前提と用意するもの
必要なものは3つです。
- Node.js 18以降(fetch APIが標準で使えるバージョン)
- Anthropic ConsoleのAPIキー(
ANTHROPIC_API_KEY) - OpenAI PlatformのAPIキー(
OPENAI_API_KEY)
プロジェクトディレクトリを1つ作って、以下のようなpackage.jsonを用意します。
{
"name": "genai-minimal",
"version": "1.0.0",
"type": "module",
"scripts": {
"claude": "node claude.js",
"chatgpt": "node chatgpt.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.30.0",
"openai": "^4.60.0",
"dotenv": "^16.4.0"
}
}
"type": "module" を指定して、以降のコード例はESM記法で書きます。
APIキーは.envファイルに置きます。
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
.envは.gitignoreに必ず入れてください。うっかりコミットしてGitHubにpushすると、両社ともキー漏洩を検知して自動失効させてきますが、それ以前の問題として、公開されている間に第三者に叩かれると請求が跳ねます。
Claude APIをNode.jsから呼び出す最小構成
Anthropicの公式SDK @anthropic-ai/sdk を使います。
npm install @anthropic-ai/sdk dotenv
最小コードはこれだけです。
// claude.js
import "dotenv/config";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{ role: "user", content: "Node.jsとは何か、3行で説明してください。" },
],
});
console.log(message.content[0].text);
node claude.js で実行できます。
レスポンスのcontentは配列で返ります。テキスト応答の場合は content[0].text に本文が入っています。ツール呼び出しや画像入力を扱い始めるとcontentの中に複数種類のブロックが入るので、実装が進んだ段階で構造を整理する必要が出てきますが、最初はこの形で十分です。
max_tokens は必須パラメータで、生成される応答の最大長を制御します。指定を忘れるとエラーになります。日本語で普通の応答を返すなら、1024〜4096あたりで様子を見るのが実務的です。
ChatGPT APIをNode.jsから呼び出す最小構成
OpenAIの公式SDK openai を使います。
npm install openai dotenv
最小コードはこちらです。
// chatgpt.js
import "dotenv/config";
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const response = await client.chat.completions.create({
model: "gpt-5",
messages: [
{ role: "user", content: "Node.jsとは何か、3行で説明してください。" },
],
});
console.log(response.choices[0].message.content);
node chatgpt.js で実行できます。
こちらはmax_tokensが任意で、指定しなくても走ります。デフォルトではモデル側の最大出力長まで生成される可能性があるので、料金コントロールしたいなら明示するのが安全です。
両者の呼び出しパターンを見比べる
コードを並べてみると、生成AI APIの呼び出しはどのプロバイダも基本構造がよく似ていることが分かります。
- クライアントインスタンスをAPIキーで初期化する
-
messages配列にroleとcontentを持つオブジェクトを積む - 応答オブジェクトの中から生成されたテキストを取り出す
この共通構造を意識しておくと、あとでプロバイダを差し替えたい・両方を並列で呼びたいといった要件が出てきたときに、共通のインターフェースでラップしやすくなります。
一方で細かな差もあります。Anthropic側はsystemプロンプトをmessagesとは別のトップレベル引数として渡す仕様で、OpenAI側はmessages配列内にrole: "system"のオブジェクトとして混ぜます。エラーレスポンスの構造も違いますし、レート制限のヘッダー名も違います。実装が育ってきたら、この差を吸収する薄いアダプタ層を1枚挟むことになります。
プロンプトをコード内にそのまま書くと辛くなる
ここまでのサンプルコードでは、プロンプト(content の文字列)をJavaScriptのコード内に直書きしていました。動かして遊ぶ分にはこれで十分ですが、本番実装に近づくにつれて、この書き方は3つの意味で辛くなります。
- 可読性:プロンプトが長くなると、コードの中で「ここからここまでが生成AIへの指示」が視覚的に埋もれる
- 再利用性:似たプロンプトを別の場所でも使いたくなったとき、コピペ管理になり、片方だけ更新される事故が起きる
- 変更追跡性:プロンプトの微調整と、コードのロジック変更が同じコミットに混ざり、あとで「あの改善はいつ入れたっけ」が追えなくなる
これらは、生成AIを組み込んだシステムを1ヶ月も運用すれば、ほぼ確実に体感する類のものです。以下では、実務で使えるプロンプトテンプレート化のパターンを、段階的に紹介します。
変数注入パターン
まず最小の一歩として、プロンプト内に埋め込みたい可変値を、テンプレート文字列の中でプレースホルダに置き換えます。
// prompt-utils.js
export function fillTemplate(template, variables) {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
if (!(key in variables)) {
throw new Error(`テンプレート変数が渡されていません: ${key}`);
}
return variables[key];
});
}
使い方はこうなります。
import { fillTemplate } from "./prompt-utils.js";
const template = `
次の商品説明文を、{{targetLength}}文字以内に要約してください。
商品説明文:
{{productDescription}}
`;
const prompt = fillTemplate(template, {
targetLength: "80",
productDescription: "軽量アルミフレームを採用した折りたたみ式チェアで...",
});
JavaScriptのテンプレートリテラル(バッククォート+${})でも同じことはできますが、プレースホルダを{{variable}}のような明示的な記法にしておくと、あとでプロンプトを外部ファイルに切り出したときに、そのままの記法で管理できるという利点があります。
未定義変数のときにエラーを投げるようにしているのは、変数名のtypoに気づかず「なんか出力が変」というデバッグ困難な状況に陥るのを防ぐためです。
4ブロック分離テンプレート
もう一段階進めます。プロンプトを「役割定義」「タスク説明」「制約条件」「出力フォーマット」の4つのブロックに分けて書くパターンです。
// templates/summarize.js
export const summarizeTemplate = `
# 役割
あなたは、EC商品説明文の編集者です。読者が短時間で商品の要点を把握できるよう、要約を作成します。
# タスク
以下の商品説明文を要約してください。
商品説明文:
{{productDescription}}
# 制約条件
- 出力は{{targetLength}}文字以内
- 商品の主要な用途を必ず含める
- 誇張表現・主観的な形容詞は使わない
- 数値情報(サイズ・重量・価格など)が原文にある場合は保持する
# 出力フォーマット
プレーンテキストで、要約本文のみを出力してください。見出し・箇条書き・引用符などは付けないでください。
`;
このように分けて書くと、以下のメリットがあります。
- 何を変更したいかによって、どのブロックを触れば良いかが自明になる
- 生成AI側も、指示の構造を認識しやすくなる(特に長いプロンプトで効果が出やすい)
- チームで共同編集するときに、レビューの粒度が細かくなる
呼び出し側は、これまでと同じようにfillTemplateを通して変数を差し込むだけです。
import Anthropic from "@anthropic-ai/sdk";
import { fillTemplate } from "./prompt-utils.js";
import { summarizeTemplate } from "./templates/summarize.js";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const prompt = fillTemplate(summarizeTemplate, {
productDescription: "軽量アルミフレームを採用した折りたたみ式チェアで...",
targetLength: "80",
});
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 512,
messages: [{ role: "user", content: prompt }],
});
console.log(message.content[0].text);
4ブロックそれぞれの中身をどう設計するかは、この記事の範囲を超えるので、別の記事で改めて掘り下げます。
プロンプトを外部ファイルに切り出す
プロンプトが長くなってきたら、JavaScriptのソースからも切り離して、独立したテキストファイルにしてしまうのが実務的にはやりやすいです。
project/
├── prompts/
│ ├── summarize.md
│ └── translate.md
├── src/
│ └── index.js
└── package.json
prompts/summarize.md の中身は、先ほどのテンプレート文字列をそのまま置くだけです。
読み込み側はNode.jsのfsモジュールで読み込みます。
// src/prompt-loader.js
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const promptsDir = join(__dirname, "..", "prompts");
export function loadPrompt(name) {
return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
}
使う側は次のようになります。
import { loadPrompt } from "./prompt-loader.js";
import { fillTemplate } from "./prompt-utils.js";
const template = loadPrompt("summarize");
const prompt = fillTemplate(template, {
productDescription: "軽量アルミフレームを採用した...",
targetLength: "80",
});
外部ファイルに切り出すことで、次の恩恵があります。
- プロンプトの編集がJavaScriptの構文エラーを起こさない(バッククォートの中のバッククォートに悩まされない)
- Gitで見たときに、プロンプトの変更差分がクリーンに見える
- 非エンジニアメンバーがプロンプトを編集する運用もやりやすくなる
- 拡張子を
.mdにしておくと、エディタでシンタックスハイライトが効き、可読性が上がる
エラーハンドリングの最小構成
生成AI APIは、外部サービスへのHTTP通信なので、失敗するときは失敗します。実務で最初に用意しておきたいのは、以下の3種類の失敗への対処です。
- レート制限(短時間に多くのリクエストを送りすぎた)
- タイムアウト(応答が返ってこない・遅すぎる)
- APIキー無効・請求関連エラー
最小のtry/catchで囲むと、こうなります。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
timeout: 60_000, // 60秒でタイムアウト
});
try {
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "こんにちは" }],
});
console.log(message.content[0].text);
} catch (error) {
if (error instanceof Anthropic.APIError) {
// APIから返ったエラー(4xx, 5xx)
console.error(`APIエラー: status=${error.status} type=${error.name}`);
console.error(error.message);
} else if (error.name === "AbortError") {
// タイムアウト
console.error("タイムアウトしました");
} else {
// それ以外(ネットワーク断など)
console.error("予期しないエラー:", error);
}
process.exit(1);
}
Anthropic.APIError はSDKが定義しているエラークラスで、HTTPステータスコード(error.status)が取れます。429ならレート制限、401ならAPIキー無効、500系ならサーバ側の一時障害、といった判別ができます。
OpenAI SDKも似た構造で、OpenAI.APIErrorが用意されています。
リトライまで含めた本格的な作り込みは要件次第ですが、まずは「どの種類の失敗が起きたのかがログに残る」状態にしておくことが、運用初期のデバッグを大きく楽にします。
まとめ
Node.jsからClaude APIとChatGPT APIを呼び出す最小構成と、プロンプトを実務で扱うための3段階のテンプレート化パターンを紹介しました。整理すると次のようになります。
- 最小コードは10行そこそこで動く
- プロンプトを直書きすると、可読性・再利用性・変更追跡性の3点で辛くなる
- 変数注入 → 4ブロック分離 → 外部ファイル切り出し、の順で段階的にリファクタするとスムーズ
- エラーハンドリングは最初から最小限入れておくと、運用初期のデバッグが楽になる
4ブロック分離テンプレートで登場した「役割」「タスク」「制約条件」「出力フォーマット」のうち、プロンプトの品質に直接効いてくるのは「役割定義」「制約条件」「出力形式」の3つです。この3要素を意識するだけで、応答の再現性がかなり安定します。3要素それぞれをどう書き分けるかについては、別記事で改めて整理する予定です。