この記事でできること (TL;DR)
この記事では、Node.js(Express)のバックエンドとVanilla JavaScriptのフロントエンドを組み合わせ、OpenAI互換の生成AIを利用したシンプルなチャットアプリを10分で動かすことを目指します。
- Dockerで環境差異なく、コマンド一発で起動
- OpenAI互換APIなら、モデルやエンドポイントの切り替えも簡単
- フロントエンドとバックエンドがどう連携するかの基礎を学べる
「とりあえず手元で動くAIチャットを作ってみたい」という方に最適な、最小構成のチュートリアルです。
はじめに
こんにちは!Qiitaでは主にAPI連携や業務自動化に関する記事を投稿している @YushiYamamoto です。
昨今の生成AIブームで、多くのエンジニアが「自分のアプリにもAIを組み込んでみたい」と考えているのではないでしょうか。しかし、いざ始めようとすると「環境構築が面倒」「フロントとバックエンドの連携がよくわからない」といった壁にぶつかりがちです。
そこで本記事では、Dockerを使って環境構築の手間を最小限にしつつ、Node.jsと素のJavaScriptだけで作る、最もシンプルなチャットアプリの実装手順を解説します。
対象読者
- 生成AIを使ったWebアプリ開発に初めて挑戦する方
- Node.jsとフロントエンドJavaScriptの連携を手を動かしながら学びたい方
- Dockerを使った基本的な開発環境の構築方法を知りたい方
システム構成
プロジェクト全体のファイル構成は以下の通りです。フロントエンドとバックエンドを明確に分離します。
chat-app/
├─ backend/
│ ├─ index.js # Expressサーバー
│ ├─ package.json # 依存パッケージ管理
│ └─ Dockerfile # バックエンド用Dockerイメージ定義
├─ frontend/
│ ├─ index.html # チャット画面のUI
│ └─ script.js # フロントエンドのロジック
└─ docker-compose.yml # Dockerコンテナの一括管理
実装手順
1. バックエンドの構築 (Node.js + Express)
APIリクエストを受け取り、生成AIのAPIを叩いて結果を返すサーバーを構築します。
backend/package.json
まずは必要なnpmパッケージを定義します。
-
express: Webサーバーを立てるための定番フレームワーク -
axios: AIのAPIを叩くためのHTTPクライアント -
cors: フロントエンドからのAPIリクエストを許可するためのミドルウェア(※ハマりどころで後述)
{
"name": "chat-backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2",
"axios": "^1.4.0",
"cors": "^2.8.5"
}
}
backend/index.js
サーバーのメインロジックです。/chatというエンドポイントにPOSTリクエストが来たら、受け取ったメッセージを元にAIのAPIを呼び出します。
const express = require('express');
const axios = require('axios');
const cors = require('cors'); // CORSミドルウェアをインポート
const app = express();
// Middleware
app.use(cors()); // すべてのオリジンからのリクエストを許可
app.use(express.json()); // POSTリクエストのJSONボディをパース
// 環境変数から設定を読み込み
const PORT = 3000;
const API_KEY = process.env.OPENAI_API_KEY;
const API_URL = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions';
// チャット処理のエンドポイント
app.post('/chat', async (req, res) => {
if (!API_KEY) {
return res.status(500).json({ error: 'APIキーが設定されていません。' });
}
try {
const { message } = req.body;
console.log('Received message:', message);
const response = await axios.post(API_URL, {
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: message }]
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
const reply = response.data.choices[0].message.content;
res.json({ reply });
} catch (err) {
// エラー詳細をサーバーログに出力
console.error(err.response?.data || err.message);
res.status(500).json({ error: 'AI応答の取得に失敗しました。' });
}
});
app.listen(PORT, () => {
console.log(`Backend server is running on http://localhost:${PORT}`);
});
backend/Dockerfile
Node.js環境を内包したDockerイメージを作成するための定義ファイルです。
# ベースイメージとしてNode.js 18の軽量版(alpine)を使用
FROM node:18-alpine
# 作業ディレクトリを作成
WORKDIR /app
# 依存パッケージを先にインストールしてビルドを高速化
COPY package*.json ./
RUN npm install --production
# アプリケーションのソースコードをコピー
COPY index.js .
# コンテナ起動時に実行するコマンド
CMD ["npm", "start"]
2. フロントエンドの構築 (HTML + JavaScript)
ユーザーがメッセージを入力し、AIとの対話を表示するシンプルなUIを作成します。
frontend/index.html
骨格となるHTMLです。チャットログを表示する<div>と、メッセージ入力用の<form>だけの最小構成です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Minimal AI Chat</title>
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; height: 100vh; margin: 0; }
#chat-container { flex: 1; padding: 1em; overflow-y: auto; }
.message { max-width: 80%; padding: 0.5em 1em; margin: 0.5em 0; border-radius: 10px; line-break: anywhere; }
.user { align-self: flex-end; background-color: #dcf8c6; }
.bot { align-self: flex-start; background-color: #f1f0f0; }
#form { display: flex; padding: 1em; border-top: 1px solid #ddd; }
#input { flex: 1; padding: 0.5em; border: 1px solid #ccc; border-radius: 5px; }
button { padding: 0.5em 1em; margin-left: 0.5em; }
</style>
</head>
<body>
<div id="chat-container"></div>
<form id="form">
<input id="input" autocomplete="off" placeholder="メッセージを入力..." />
<button>送信</button>
</form>
<script src="script.js"></script>
</body>
</html>
frontend/script.js
fetch APIを使って、バックエンドの/chatエンドポイントにリクエストを送信します。
const chatContainer = document.getElementById('chat-container');
const formEl = document.getElementById('form');
const inputEl = document.getElementById('input');
const BACKEND_URL = 'http://localhost:3000/chat';
formEl.addEventListener('submit', async (e) => {
e.preventDefault();
const userMsg = inputEl.value.trim();
if (!userMsg) return;
appendMessage(userMsg, 'user');
inputEl.value = '';
// ローディング表示などをここに追加しても良い
try {
const res = await fetch(BACKEND_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMsg })
});
if (!res.ok) {
throw new Error(`Server error: ${res.statusText}`);
}
const data = await res.json();
appendMessage(data.reply, 'bot');
} catch (error) {
console.error('Fetch error:', error);
appendMessage('エラーが発生しました。バックエンドのログを確認してください。', 'bot');
}
});
function appendMessage(text, role) {
const div = document.createElement('div');
div.className = `message ${role}`;
div.textContent = text;
chatContainer.append(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
3. Docker Composeの設定
バックエンドとフロントエンド(今回は静的ファイル配信用のNginx)をまとめて起動するための設定ファイルです。
docker-compose.yml
version: '3.8'
services:
# バックエンドサービス (Node.js)
backend:
build: ./backend
environment:
# .envファイルから環境変数を読み込む
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OPENAI_API_URL=${OPENAI_API_URL}
ports:
- "3000:3000" # ホストの3000番をコンテナの3000番にマッピング
# フロントエンドサービス (Nginx)
frontend:
image: nginx:1.25-alpine # 公式のNginxイメージを使用
volumes:
# ホストのfrontendディレクトリをコンテナのドキュメントルートにマウント
- ./frontend:/usr/share/nginx/html:ro
ports:
- "8080:80" # ホストの8080番をコンテナの80番にマッピング
depends_on:
- backend # backendが起動してからfrontendを起動
起動方法
-
.envファイルの作成
プロジェクトのルートディレクトリ(docker-compose.ymlと同じ場所)に.envファイルを作成し、APIキーを設定します。.env# 必須: あなたのOpenAI互換APIキー OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx # オプション: OpenAI以外のエンドポイントを利用する場合 # OPENAI_API_URL=https://your-custom-ai-endpoint/v1/chat/completions -
Docker Composeの実行
ターミナルでプロジェクトのルートディレクトリに移動し、以下のコマンドを実行します。--buildオプションは、初回起動時やDockerfileを更新した際に必要です。docker compose up --build -
ブラウザでアクセス
ビルドと起動が完了したら、ブラウザでhttp://localhost:8080にアクセスします。
シンプルなチャットUIが表示されれば成功です!
ハマりどころと解決策 (Gotchas!)
-
APIキーが読み込まれない:
.envファイルはdocker composeコマンドを実行するディレクトリに置く必要があります。また、docker compose upを実行した後に.envを更新した場合、docker compose down && docker compose upでコンテナを再起動しないと変更が反映されません。 -
CORSエラー: フロントエンド(
localhost:8080)からバックエンド(localhost:3000)へのAPIリクエストは、通常ブラウザのセキュリティポリシー(CORS)によってブロックされます。今回はバックエンドのindex.jsでapp.use(cors())を記述することで、この問題を解決しています。 -
Dockerのポート競合:
Port is already allocatedのようなエラーが出た場合、ホストマシンで3000番や8080番ポートが既に使用されています。docker-compose.ymlのports設定(例:"3001:3000")を変更して、別のポートを利用してください。
今後の拡張案
この最小構成をベースに、様々な機能を追加できます。
- ストリーミング応答: AIの応答をタイプライターのように一文字ずつ表示する。
- チャット履歴の実装: 会話の文脈を保持して、より自然な対話を実現する。
- UIの改善: Markdown形式の応答をレンダリングしたり、UIフレームワーク(React, Vueなど)を導入する。
おわりに
本記事では、Docker, Node.js, Vanilla JavaScriptを使って、10分で動かせる生成AIチャットアプリの作り方を解説しました。この最小構成が、皆さんのオリジナルAIアプリ開発の第一歩となれば幸いです。
最後までお読みいただきありがとうございました!この記事が役に立ったと思ったら、ぜひLGTMをお願いします!