OpenAI APIでチャットボットを作ってみた【Next.js + Tailwind CSS】

OpenAI APIでチャットボットを作ってみた【Next.js + Tailwind CSS】
  • URLをコピーしました!

最近は、OpenAIのAPIを使って自分専用のチャットボットや業務用アシスタントを作る人が増えています。
この記事では、Next.js(App Router)とTailwind CSSを使って、シンプルかつカスタマイズしやすいチャットUIを構築する方法を紹介します。

OpenAIのchat/completionsエンドポイントにリクエストを送り、ブラウザ上でChatGPTのような会話体験を実現する流れを、できるだけ簡潔にまとめています。

もし「自分でコードを書くのはちょっと面倒…」という方や、
「すぐに動くUI付きテンプレートが欲しい」という方のために、以下のプラットフォームでテンプレートも販売しています。

本記事で紹介しているテンプレートを販売中!

このチャットボットUIテンプレートは、複数のプラットフォームで販売中です。
「まずは動くものを試したい」「環境構築なしですぐに使いたい」「コード全体を確認したい」
そんな方にぴったりの構成になっています。

ほかのテンプレートも各マイページからチェックできます:

目次

使用している技術について

このチャットボットUIは、以下の技術を使って構築されています。

Next.js(App Router構成)

Reactベースのフレームワークで、ページやAPIルートを統一的に構築できます。src/app を起点としたApp Router構成を使っています。

Tailwind CSS

ユーティリティファーストなCSSフレームワークで、クラスを使って効率よくデザインを整えられます。レスポンシブでシンプルなUIが素早く作れます

OpenAI API(Chat Completions)

/v1/chat/completionsエンドポイントを使って、GPT系モデルと対話する仕組みです。今回は環境変数で gpt-4.1-minigpt-3.5-turbo などを簡単に切り替えられるようにしています。

API Routes + fetch

Next.jsのAPIルート (/api/chat) を使って、ブラウザとOpenAI APIの間を中継するサーバー処理を書いています。クライアントからは fetch("/api/chat") で使えるようにしてあります。

OpenAI API keyを作成

今回は、OpenAI APIを利用しますので、OpenAI API keyを作成する必要があります。
OpenAI APIのダッシュボードでAPI keysに遷移します。「Create new secret key」を選択します。

Projectは、任意に選んでもらい、Permissionsを使いたいモデルに合わせて選択するか、Allを選択して生成します。

作成が終わると「sk-」から始まる文字列が作成されるので、それをこの後利用します。
くれぐれもこのkeyが漏れないように注意してください。

環境構築

まずは Next.js の開発環境を用意します。

プロジェクトの作成

プロジェクトを作成していきます。
基本的には、全てデフォルトのままでいいかなと思いますが、必要に応じて変更してもらえれば問題ないです。
(今回のプロジェクトは、Tailwind CSSを利用しますので、Yesにしておくと良いかと思います。デフォルトはYesです。)

$ npx create-next-app@latest my-chatbot --typescript
Need to install the following packages:
create-next-app@15.4.4
Ok to proceed? (y) y

✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /my-chatbot.

Using npm.

Initializing project with template: app-tw 


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- @tailwindcss/postcss
- tailwindcss
- eslint
- eslint-config-next
- @eslint/eslintrc


added 336 packages, and audited 337 packages in 14s

137 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Initialized a git repository.

Success! Created my-chatbot at /my-chatbot

npm notice
npm notice New minor version of npm available! 11.0.0 -> 11.5.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.5.1
npm notice To update run: npm install -g npm@11.5.1
npm notice

「Success! Created my-chatbot at /my-chatbot」が出ていれば、プロジェクト作成は完了です。
プロジェクトが作成できたら、ディレクトリは移動しておきましょう。

cd my-chatbot

OpenAI APIキーを設定

ルートに .env ファイルを作成します。

OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4.1-mini
SYSTEM_PROMPT=You are a helpful assistant.

簡単な説明:

  • OPENAI_API_KEY:OpenAIのAPIアクセスに必要な秘密鍵(個人アカウントで発行)
  • OPENAI_MODEL:利用するモデル名。用途に応じて gpt-3.5-turbogpt-4.1-mini など
  • SYSTEM_PROMPT:AIの性格や役割を指定する初期メッセージ(チャット開始時に効く)

開発サーバーを起動

下記のコマンドを実行することで、ひな形が起動します。

npm run dev

UIの実装(チャット画面)

チャットのユーザーインターフェースは、src/app/page.tsx に集約する形にしています。
ソースコードの全量は下記になります。

"use client";

import { useState, useRef, useEffect } from "react";

// Reusable chat interface using OpenAI API
// OpenAI API を使った再利用可能なチャットインターフェース
export default function Home() {
  const [input, setInput] = useState("");
  const [messages, setMessages] = useState<
    { role: "user" | "assistant"; content: string }[]
  >([]);
  const [loading, setLoading] = useState(false);
  const chatEndRef = useRef<HTMLDivElement>(null);

  // Handle message submission to the API
  // メッセージをAPIに送信する処理
  const sendMessage = async () => {
    if (!input.trim()) return;

    // Add user message to chat history
    // ユーザーのメッセージを履歴に追加
    const newMessages = [
      ...messages,
      { role: "user", content: input } as const,
    ];
    setMessages(newMessages);
    setInput("");
    setLoading(true);

    // Send message to backend API route
    // バックエンドのAPIエンドポイントにメッセージを送信
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: input }),
    });

    const data = await res.json();

    // Add assistant response to chat history
    // アシスタントの応答を履歴に追加
    setMessages((prev) => [
      ...prev,
      { role: "assistant", content: data.reply },
    ]);
    setLoading(false);
  };

  // Scroll to bottom of chat view on update
  // メッセージ更新時にチャットビューの最下部にスクロール
  useEffect(() => {
    chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, loading]);

  return (
    <main className="h-screen flex items-center justify-center bg-gray-100 text-gray-800">
      <div className="flex flex-col w-full max-w-3xl h-[90vh] border-x border-gray-300 bg-white">
        {/* Header */}
        {/* ヘッダー */}
        <header className="p-4 border-b text-lg font-semibold">
          ChatBot-ChatGPT
        </header>

        {/* Chat message display */}
        {/* チャットメッセージ表示領域 */}
        <div className="flex-1 overflow-y-auto p-6 space-y-4 scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100">
          {messages.map((msg, idx) => (
            <div
              key={idx}
              className={`flex ${
                msg.role === "user" ? "justify-end" : "justify-start"
              }`}
            >
              <div
                className={`max-w-xl px-4 py-2 rounded-lg text-sm whitespace-pre-wrap ${
                  msg.role === "user"
                    ? "bg-blue-600 text-white"
                    : "bg-gray-100 text-gray-900"
                }`}
              >
                {msg.content}
              </div>
            </div>
          ))}

          {/* Typing indicator while loading */}
          {/* ローディング中のインジケーター */}
          {loading && (
            <div className="flex justify-start">
              <div className="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm animate-pulse">
                Thinking...
              </div>
            </div>
          )}
          <div ref={chatEndRef} />
        </div>

        {/* Input area */}
        {/* 入力欄 */}
        <footer className="border-t p-4">
          <div className="flex gap-2">
            <input
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && sendMessage()}
              className="flex-grow p-2 border rounded focus:outline-none focus:ring focus:border-blue-300"
              placeholder="Type your message..."
            />
            <button
              onClick={sendMessage}
              className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
            >
              Send
            </button>
          </div>
        </footer>
      </div>
    </main>
  );
}

メッセージの状態管理

const [input, setInput] = useState("");
const [messages, setMessages] = useState<
  { role: "user" | "assistant"; content: string }[]
>([]);
const [loading, setLoading] = useState(false);

簡単な説明:

  • input:テキストボックスの入力値を保持
  • messages:これまでのチャット履歴(ユーザーとアシスタントの会話)
  • loading:APIからの応答待ちかどうか

送信処理とAPI呼び出し

const sendMessage = async () => {
  if (!input.trim()) return;

  const newMessages = [...messages, { role: "user", content: input }];
  setMessages(newMessages);
  setInput("");
  setLoading(true);

  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: input }),
  });

  const data = await res.json();
  setMessages((prev) => [...prev, { role: "assistant", content: data.reply }]);
  setLoading(false);
};

簡単な説明:

  • ユーザーの入力を履歴に追加しつつ、APIにメッセージを送信します
  • 応答が返ってきたら assistant の返答として履歴に追加します

チャット画面のレイアウト(Tailwindで整形)

<div className="flex-1 overflow-y-auto p-6 space-y-4">
  {messages.map((msg, idx) => (
    <div key={idx} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
      <div className={`max-w-xl px-4 py-2 rounded-lg text-sm whitespace-pre-wrap ${
        msg.role === "user"
          ? "bg-blue-600 text-white"
          : "bg-gray-100 text-gray-900"
      }`}>
        {msg.content}
      </div>
    </div>
  ))}

  {loading && (
    <div className="flex justify-start">
      <div className="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm animate-pulse">
        Thinking...
      </div>
    </div>
  )}
</div>

簡単な説明:

  • ユーザーとアシスタントのメッセージは左右に分かれて表示されます
  • 応答待ちの際は「Thinking…」と表示され、会話感を演出します

入力欄と送信ボタン

<footer className="border-t p-4">
  <div className="flex gap-2">
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      onKeyDown={(e) => e.key === "Enter" && sendMessage()}
      className="flex-grow p-2 border rounded focus:outline-none"
      placeholder="Type your message..."
    />
    <button
      onClick={sendMessage}
      className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
    >
      Send
    </button>
  </div>
</footer>

簡単な説明:

  • キーボードの Enter またはボタンで送信できる仕様
  • Tailwind CSS を使って、シンプルで見やすいフォームにしています

サーバー側の実装(OpenAI API 連携)

チャットのやりとりは、クライアントから /api/chat にリクエストを送り、
サーバー側で OpenAI の Chat API に中継・レスポンスを返すという流れです。

サーバー側のソースコードの全量は下記になります。

// src/app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";

// POST handler for chat requests
// チャットリクエストを処理するPOSTハンドラー
export async function POST(req: NextRequest) {
  const { message } = await req.json();

  // Call OpenAI API using server-side secret key and parameters
  // SYSTEM_PROMPT defines the assistant's personality, tone, or behavior.
  // サーバーサイドの秘密鍵とパラメータでOpenAI APIを呼び出す
  // SYSTEM_PROMPT はアシスタントの性格や話し方、振る舞いを指定します。
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
      messages: [
        {
          role: "system",
          content:
            process.env.SYSTEM_PROMPT || "You are a helpful AI assistant.",
        },
        { role: "user", content: message },
      ],
    }),
  });

  const data = await response.json();

  // Handle error response from OpenAI
  // OpenAIからのエラーレスポンスを処理
  if (data.error) {
    console.error(data.error);
    return NextResponse.json(
      { reply: "An error occurred. Please try again later." },
      { status: 500 }
    );
  }

  // Return assistant reply
  // アシスタントの返答を返す
  return NextResponse.json({ reply: data.choices[0].message.content });
}

route.ts の基本構成

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { message } = await req.json();

簡単な説明:

  • POST メソッドに限定したAPIルート
  • クライアント側から送られたメッセージ本文を受け取ります

OpenAI API へのリクエスト

const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
      messages: [
        { role: "system", content: process.env.SYSTEM_PROMPT || "You are a helpful assistant." },
        { role: "user", content: message },
      ],
    }),
  });

  const data = await response.json();

簡単な説明:

  • 環境変数から APIキー, モデル名, プロンプト を動的に読み込み
  • GPT系モデルに送るための messages 配列を作成

レスポンス処理とエラーハンドリング

if (data.error) {
    console.error(data.error);
    return NextResponse.json({ reply: "An error occurred." }, { status: 500 });
  }

  return NextResponse.json({ reply: data.choices[0].message.content });
}

簡単な説明:

  • OpenAI API が失敗した場合は、エラー内容をコンソールに出力
  • 正常に応答が返ってきたら、アシスタントのメッセージを reply としてクライアントに返します

【おまけ】注意点

APIキーは絶対に公開しないように .env で管理するようにしてください。
仮に、APIキーが漏れてしまった場合は、OpenAIのダッシュボードからAPIキーを削除するようにしましょう。

モデル名は環境変数化しているので、用途に応じて差し替えをしてください。
モデルによってかかる金額が変わるため、モデル選定には気を付けるようにしてください。

テンプレート販売のご案内

本記事で紹介したチャットボットUIは、商用利用も可能なテンプレートとして販売も行っています。

なぜテンプレートを販売するのか

以下のようなニーズを感じている方向けにご用意しました。

  • 「手順どおりやっても環境構築がうまくいかない…」
  • 「とにかく動くサンプルから試したい」
  • 「ブログを見て便利そうだったので、応援・寄付の意味も込めて購入したい」

開発に慣れていない方でも、最小限の手間でサクッと起動して試せる構成にしてあります。

テンプレートの活用例(カスタマイズアイデア)

このテンプレートは、個人開発や学習用途にぴったりです。
たとえば次のような改造・拡張をしてみるのもおすすめです。

  • APIの返答に履歴や文脈を追加してみる
  • 入力履歴をローカルストレージに保存するようにしてみる
  • チャットログをPDFやCSVに出力する機能をつけてみる
  • 音声読み上げ画像生成といった拡張も面白いかもしれません

プログラミング初学者向けに「課題ベース」で触ってみるのも良い練習になります。

テンプレートに含まれる内容

テンプレートには、今回紹介したプロジェクトのすべてのソースコードが含まれています。
そのため、自分でプロジェクトを一から作成・設定する必要はなく、すぐに起動可能です。

  • Next.js(App Router)によるチャットUI
  • API連携済みのサーバーサイド実装
  • コメント付きのクリーンなコード構成
  • Tailwind CSS によるシンプルで改良しやすいデザイン
  • Docker 起動に対応した構成ファイル(Dockerfile, docker-compose.yml

※実行には、Node.js や Docker などの基本的な実行環境が必要です。

本記事で紹介しているテンプレートを販売中!

このチャットボットUIテンプレートは、複数のプラットフォームで販売中です。
「まずは動くものを試したい」「環境構築なしですぐに使いたい」「コード全体を確認したい」
そんな方にぴったりの構成になっています。

ほかのテンプレートも各マイページからチェックできます:

まとめ

今回は、OpenAI API と Next.js を使って
シンプルなチャットボットUI を構築する方法をご紹介しました。

ポイントを振り返ると:

  • Chat API を使えば、わずかなコードで高品質なチャットボットが構築できる
  • Tailwind CSS + Next.js App Router により、拡張・再利用しやすい構成
  • テンプレートを活用すれば、環境構築を省略してすぐに動作確認が可能

開発や個人プロジェクトのスターターとして使えるだけでなく、独自の機能追加やUIカスタマイズを行うベースとしてもおすすめです。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

情報セキュリティを勉強するために始めたブログです。
新人のため、広い心を持って見ていただけると嬉しく思います。
楽しくプログラミングを勉強するために、「Teech Lab.」もありますので、ソフトウェア開発にも興味があればぜひ覗いて見てください!

目次