1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】10分で動く!JavaScript×生成AIの最小チャットアプリ(Docker対応)

Posted at

この記事でできること (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を起動

起動方法

  1. .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
    
  2. Docker Composeの実行
    ターミナルでプロジェクトのルートディレクトリに移動し、以下のコマンドを実行します。--buildオプションは、初回起動時やDockerfileを更新した際に必要です。

    docker compose up --build
    
  3. ブラウザでアクセス
    ビルドと起動が完了したら、ブラウザで 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.jsapp.use(cors())を記述することで、この問題を解決しています。
  • Dockerのポート競合: Port is already allocatedのようなエラーが出た場合、ホストマシンで3000番や8080番ポートが既に使用されています。docker-compose.ymlports設定(例: "3001:3000")を変更して、別のポートを利用してください。

今後の拡張案

この最小構成をベースに、様々な機能を追加できます。

  • ストリーミング応答: AIの応答をタイプライターのように一文字ずつ表示する。
  • チャット履歴の実装: 会話の文脈を保持して、より自然な対話を実現する。
  • UIの改善: Markdown形式の応答をレンダリングしたり、UIフレームワーク(React, Vueなど)を導入する。

おわりに

本記事では、Docker, Node.js, Vanilla JavaScriptを使って、10分で動かせる生成AIチャットアプリの作り方を解説しました。この最小構成が、皆さんのオリジナルAIアプリ開発の第一歩となれば幸いです。

最後までお読みいただきありがとうございました!この記事が役に立ったと思ったら、ぜひLGTMをお願いします!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?