最近は、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-mini
やgpt-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-turbo
やgpt-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
)
本記事で紹介しているテンプレートを販売中!
このチャットボットUIテンプレートは、複数のプラットフォームで販売中です。
「まずは動くものを試したい」「環境構築なしですぐに使いたい」「コード全体を確認したい」
そんな方にぴったりの構成になっています。
まとめ
今回は、OpenAI API と Next.js を使って
シンプルなチャットボットUI を構築する方法をご紹介しました。
ポイントを振り返ると:
- Chat API を使えば、わずかなコードで高品質なチャットボットが構築できる
- Tailwind CSS + Next.js App Router により、拡張・再利用しやすい構成
- テンプレートを活用すれば、環境構築を省略してすぐに動作確認が可能
開発や個人プロジェクトのスターターとして使えるだけでなく、独自の機能追加やUIカスタマイズを行うベースとしてもおすすめです。