Next.js、OpenAI、PostgreSQLで食事プラン生成アプリを作成
パーソナライズされた食事プランを生成するフルスタックAIアプリを構築するチュートリアルです。OpenAI APIでのプロンプトエンジニアリング、Next.jsによるフルスタック開発、PrismaとPostgreSQLでのデータ保存を説明します。
このページの内容
冷蔵庫の前に立ち尽くして、「今日の夕飯は何にしようか?」と迷う経験はありませんか?意思決定の疲れは реально なものであり、特に健康目標を達成しようとしたり、食事制限を管理したり、レцепトル・ルーティンから脱却しようとしている場合には 더욱 그렇습니다。スマートな食事計画の需要は急増しており、アメリカ市場はその価値が2億7,100万美元を超えています。AIの力を借りてこの問題を解決するツールを作成できたらどうでしょうか?
このチュートリアルでは、パーソナライズされた食事プラン生成アプリを構築します。ユーザーは食事のニーズ(「ヴィーガン」、「低糖質」など)、制限(「グルテンフリー」)、健康目標(「体重減少」)を入力すると、構造化された7日分の食事プランが生成されます。
сучасний で powerful なテクノロジース택を使用します:
- Next.js:サーバーサイド機能を持つフルスタックのReactアプリケーション用。
- OpenAI API(GPT-4o mini):知的で人間らしい創造性を持って食事プランを生成。
- PostgreSQL:データを保存するための堅牢なオープンソースのリレーショナルデータベース。
- Prisma:データベースアクセスを容易にする次世代ORM。
このプロジェクトは、単なるAIチャットボットではありません。AIを構造化され 신뢰できるデータを生成させる方法を教える実用的な реальные アプリケーションであり、Large Language Models(LLM)を扱う開発者にとって重要なスキルです。
問題の理解
このようなアプリケーションでLLMを使用する場合の中核的な課題は、テキストの生成だけでなく、構造化データの生成です。食事プランを説明する自由形式の段落は、アプリケーションが解析して表示するのが困難です。私たちは予測可能なJSONオブジェクトが必要です。
基本的なプロンプトの制限:AIに「JSON形式で食事プランを作成してください」と単に依頼するだけでは信頼性に欠けます。モデルは以下を行う可能性があります:
- フィールドを幻觉(hallucinate)したり、命名が一貫しない。
- 特定の)や食事を含めるのを忘れる。
- JSON構文を破坏し、解析エラーを引起ごす。
私たちのアプローチ:高度なプロンプトエンジニアリングでこの問題を解決します。プロンプト内に明確なスキーマと具体的な指示を提供することで、AI出力の信頼性を大幅に向上させることができます。JSON構造がどのように見えるかを正確に指示し、PostgreSQLデータベースに保存するためのクリーンなデータを確保します。
前提条件
始める前に、以下を確認してください:
- Node.js(v18以降)がインストールされている。
- OpenAI APIキー:OpenAI Platformから取得可能。
- PostgreSQLデータベース:Dockerを使用してローカルで実行するか、SupabaseやNeonなどの無料ホスティングサービスを利用可能。
- React、Next.js、JavaScript/TypeScriptの基本知識。
ステップ1:Next.jsプロジェクトとPrismaのセットアップ
まず、Next.jsアプリケーションを作成し、データベース接続の基盤を設定しましょう。
実施内容
新しいNext.jsプロジェクトを初期化し、Prismaをインストールして、PostgreSQLデータベースに接続するように設定します。
実装
-
Next.jsアプリを作成: ターミナルを開き、以下を実行:
bash npx create-next-app@latest ai-meal-planner cd ai-meal-plannerTypeScriptとApp Routerを含むデフォルトオプションを選択。
-
PrismaとPostgreSQLドライバーをインストール:
bash npm install @prisma/client pg npm install --save-dev prismapgパッケージはPostgreSQL用のNode.jsドライバーです。 -
Prismaを初期化: このコマンドは、必要なPrisma設定ファイルを設定します。
bash npx prisma initこれにより、
schema.prismaファイルとデータベースURL用の.envファイルを含むprismaディレクトリが作成されます。 -
データベース接続を設定:
.envファイルを開き、DATABASE_URL変数をPostgreSQL接続文字列に設定。DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE"プレースホルダーを実際のデータベース認証情報に置き換えます。
動作仕組み
create-next-appは完全なNext.jsプロジェクトをスキャフォールドします。prisma initは、データベース構造の单一の情報源として機能するスキーマファイルを作成することで、プロジェクトがPrismaを使用するように設定します。
ステップ2:データベーススキーマの定義
次に、アプリケーションのデータモデルを定義します。ユーザーに生成された食事プランを保存する方法が必要です。
実施内容
schema.prismaファイルを編集して、MealPlanモデルを定義します。このモデルは、ユーザーの入力と生成されたJSON食事プランを保存します。
実装
prisma/schema.prismaを開き、その内容を以下のように置き換えます:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model MealPlan {
id String @id @default(uuid())
// User's preferences that generated the plan
preferences String
// The generated meal plan, stored as a JSON object
plan Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
動作仕組み
generator client:Prisma Clientを生成したいことを指定します。これはタイプセーフなクエリビルダーです。datasource db:データベース接続を設定し、PrismaにPostgreSQLを使用し、接続URLの場所を伝えます。model MealPlan:データベース内のテーブルを定義。id:各食事プランの一意の識別子。preferences:ユーザーの生入力を保存するための単純なString。plan:最も重要なフィールド。PostgreSQLがサポートするネイティブJson型を使用します。Prismaは、効率性とインデックス機能のためにJSONBを使用することを推奨しており、Json型をPostgreSQLのjsonbにマッピングします。
マイグレーションの実行
このスキーマをデータベースに適用するために、最初のマイグレーションを実行します。
npx prisma migrate dev --name init
このコマンドは2つのことを行います:
- スキーマの変更を反映したSQLマイグレーションファイルを作成。
- データベースにマイグレーションを適用し、
MealPlanテーブルを作成。
ステップ3:OpenAI APIルートの構築
これが魔法が起こる場所です。OpenAI APIへのリクエストを処理するための安全なバックエンドAPIルートをNext.jsで作成します。
実施内容
ユーザー設定を受け取り、詳細なプロンプトを構築し、OpenAIに送信し、構造化されたJSONレスポンスをデータベースに保存するAPIルート(/api/generate)を作成します。
実装
-
OpenAI SDKをインストール:
bash npm install openai -
OpenAI APIキーを追加: APIキーをクライアント側に露出させないでください。
.env.localファイルに安全に保存します。Next.jsは自動的にこれらの変数をロードします。# .env.local OPENAI_API_KEY="your-secret-key-here" -
Prisma Clientインスタンスを作成: 効率性のために、単一のPrismaClientをインスタンス化して再利用するのがベストプラクティスです。
lib/prisma.tsにファイルを作成:typescript // lib/prisma.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export default prisma; -
APIルートを作成:
app/api/generate/route.tsに新しいファイルを作成:typescript // app/api/generate/route.ts import { NextResponse } from 'next/server'; import OpenAI from 'openai'; import prisma from '@/lib/prisma'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); export async function POST(req: Request) { try { const body = await req.json(); const { preferences } = body; if (!preferences) { return new NextResponse('User preferences are required', { status: 400 }); } const prompt = ` You are a professional nutritionist and meal planner. Create a personalized 7-day meal plan based on the following user preferences: ${preferences}. Your response MUST be a valid JSON object. Do not include any text, notes, or explanations outside of the JSON structure. The JSON object should have a single root key: "mealPlan". "mealPlan" should be an array of 7 objects, one for each day (e.g., "Monday", "Tuesday", ...). Each day object must contain the following keys: "day", "meals", and "dailyTotals". - "day": The name of the day (string). - "meals": An object with three keys: "breakfast", "lunch", and "dinner". - Each meal ("breakfast", "lunch", "dinner") must be an object with "name" (string), "ingredients" (array of strings), and "calories" (number). - "dailyTotals": An object with the total "calories" (number) for that day. Here is the required structure: { "mealPlan": [ { "day": "Monday", "meals": { "breakfast": { "name": "Scrambled Eggs", "ingredients": ["2 Eggs", "1/4 cup Milk", "Salt", "Pepper"], "calories": 250 }, "lunch": { "name": "Grilled Chicken Salad", "ingredients": ["100g Chicken Breast", "Mixed Greens", "Cherry Tomatoes", "Cucumber", "Vinaigrette"], "calories": 400 }, "dinner": { "name": "Salmon with Quinoa", "ingredients": ["150g Salmon Fillet", "1 cup Quinoa", "Steamed Broccoli"], "calories": 550 } }, "dailyTotals": { "calories": 1200 } } ] } `; const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, // This forces the model to output valid JSON }); const content = response.choices.message.content; if (!content) { throw new Error('No content returned from OpenAI.'); } const mealPlanJson = JSON.parse(content); // Save to database const savedPlan = await prisma.mealPlan.create({ data: { preferences: preferences, plan: mealPlanJson, // Prisma handles the JSON serialization }, }); return NextResponse.json(savedPlan); } catch (error) { console.error('Error generating meal plan:', error); return new NextResponse('Failed to generate meal plan', { status: 500 }); } }
動作仕組み
- プロンプトエンジニアリング:プロンプトは非常に詳細です。AIのペルソナ(「プロフェッショナルな栄養士」)を定義し、 목표를 설명하며、最も重要的是JSON出力の厳密なスキーマを提供します。この詳細な指示が、信頼性の高いデータを取得するためのポイントです。
response_format: { type: 'json_object' }:これはOpenAI APIの強力な機能です。モデルに構文的に正しいJSONオブジェクトを出力させ、解析エラーの可能性を大幅に減少させます。- セキュリティ:ルートは
POSTハンドラーであり、すべてのOpenAIのやり取りはサーバー側で发生し、APIキーを安全に保ちます。 - データベースのやり取り:JSONを受け取って解析した後、
prisma.mealPlan.create()を使用してレコードを保存します。Prismaの型安全性により、スキーマに一致するデータを挿入していることが保証されます。
ステップ4:フロントエンドインターフェースの構築
次に、ユーザーが設定を入力し、生成された食事プランを表示するためのシンプルなUIを作成しましょう。
実施内容
メインページ(app/page.tsx)を修正して、フォームを含み、ロード中とエラーの状態を處理し、食事プランを読みやすい形式で表示します。
実装
app/page.tsxのコードを以下のように置き換えます:
// app/page.tsx
'use client';
import { useState } from 'react';
// Define a type for our meal plan structure for type safety
type Meal = {
name: string;
ingredients: string[];
calories: number;
};
type DayPlan = {
day: string;
meals: {
breakfast: Meal;
lunch: Meal;
dinner: Meal;
};
dailyTotals: {
calories: number;
};
};
type MealPlanResponse = {
id: string;
preferences: string;
plan: {
mealPlan: DayPlan[];
};
};
export default function HomePage() {
const [preferences, setPreferences] = useState('');
const [mealPlan, setMealPlan] = useState<MealPlanResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setMealPlan(null);
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ preferences }),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data: MealPlanResponse = await response.json();
setMealPlan(data);
} catch (err: any) {
setError(err.message || 'Failed to generate meal plan. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center p-8">
<h1 className="text-4xl font-bold text-gray-800 mb-4">AI Meal Planner 🥗</h1>
<p className="text-lg text-gray-600 mb-8">
Enter your dietary needs and goals, and let AI create a plan for you.
</p>
<form onSubmit={handleSubmit} className="w-full max-w-lg mb-8">
<textarea
value={preferences}
onChange={(e) => setPreferences(e.target.value)}
placeholder="e.g., 2000 calories, high-protein, gluten-free, for weight loss"
className="w-full p-3 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 transition"
rows={4}
/>
<button
type="submit"
disabled={loading || !preferences}
className="w-full mt-4 bg-green-600 text-white py-3 px-4 rounded-md font-semibold hover:bg-green-700 disabled:bg-gray-400 transition"
>
{loading ? 'Generating...' : 'Generate Meal Plan'}
</button>
</form>
{error && <p className="text-red-500">{error}</p>}
{mealPlan && (
<div className="w-full max-w-4xl bg-white p-6 rounded-lg shadow-md">
<h2 className="text-2xl font-bold text-gray-800 mb-4">Your 7-Day Meal Plan</h2>
<p className="mb-4 text-gray-600">Based on preferences: <span className="font-semibold">{mealPlan.preferences}</span></p>
<div className="space-y-6">
{mealPlan.plan.mealPlan.map((day) => (
<div key={day.day} className="border border-gray-200 p-4 rounded-md">
<h3 className="text-xl font-semibold text-green-700">{day.day}</h3>
<p className="text-sm text-gray-500 mb-2">Total Calories: {day.dailyTotals.calories}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Object.entries(day.meals).map(([mealType, meal]) => (
<div key={mealType} className="bg-gray-50 p-3 rounded">
<h4 className="font-bold capitalize">{mealType}</h4>
<p>{meal.name} ({meal.calories} kcal)</p>
<ul className="list-disc list-inside text-sm text-gray-600">
{meal.ingredients.map((ing, i) => <li key={i}>{ing}</li>)}
</ul>
</div>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
動作仕組み
'use client':この directive はコンポーネントをクライアントコンポーネントとしてマークし、状態とイベントハンドラーの使用を可能にします。- 状態管理:フォーム入力、ロードステータス、エラー、最終的な食事plandataを管理するために
useStateを使用します。 - API呼び出し:
handleSubmit関数は/api/generateエンドポイントにPOSTリクエストを送信し、ユーザーの設定をリクエストボディに含めます。 - レンダリング:コンポーネントは現在の状態に基づいて、条件付きでロードメッセージ、エラー、またはフォーマットされた食事プランをレンダリングします。
mealPlan配列をマップして、各日の詳細を表示します。 - TypeScript:データ(
Meal、DayPlan、MealPlanResponse)の型を定義し、コンポーネント全体で型安全性を確保します。
すべてをまとめる
アプリケーションをテストするには、開発サーバーを実行します:
npm run dev
ブラウザでhttp://localhost:3000を開きます。入力フォームが表示されるはずです。「2200カロリーのベジタリアンプランで、ナトリウムが少なく、30分で終わる夕飯」といった設定を入力し、「食事プランを生成」をクリックします。数秒後、パーソナライズされた構造化された食事プランが表示されます。
代替アプローチ
私たちのアプローチは堅牢ですが、検討すべき他の方法があります:
- Function Calling:AIが外部ツール(例:レシピAPI)を呼び出す必要があるより複雑なやり取りの場合、OpenAIのFunction Calling機能が優れています。これは関数引数に直接マッピングされる構造化されたJSONを返すように設計されています。
- Zod/Pydantic統合:Zod(TypeScript用)などのライブラリを使用して、コードでスキーマを定義できます。OpenAI SDKにはこれらの組み込みサポートさえあり、スキーマ定義をよりプログラム的で再利用可能なものにできます。
結論
フルスタックのAIパワードWebアプリケーションをゼロから構築しました。Next.jsをPrismaを使用してPostgreSQLデータベースに接続し、OpenAI APIとやり取りするための安全なAPIルートを作成し、そして最も重要なこととして、信頼性が高く構造化されたJSONを提供するプロンプトをエンジニアリングする方法を学びました。
このプロジェクトは素晴らしい出発点です。以下のような機能を追加して構築できます:
- ユーザー認証でプランをユーザーごとに保存。
- 食事プランに基づく「買い物リスト」生成機能。
- 個々の食事を再生成または入れ替える機能。
- 詳細な栄養情報(タンパク質、炭水化物、脂肪)。
人間の言語と構造化データの間のギャップを埋める能力は、モダンなWeb開発における超大国です。これで素晴らしいものを構築去吧!
リソース
- Next.js Documentation
- OpenAI API Documentation
- Prisma Documentation
- PostgreSQL JSON Types
- Vercel AI SDK
クイックリファレンス
| 側面 | 主要情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と awareness |
| アクション | 医療提供者にご相談ください |
| 頻度 | ケアチームのおすすめに応じて |
| エビデンス | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当する可能性のある人は誰ですか? A: 個人的な病史と家族歴について医療提供者と相談し、個人のリスク要因を評価し、適切な次のステップを決定してください。
Q: 医師に何を確認すればよいですか? A: 良い質問例:「結果は何を意味するのか?」「オプションは何があるのか?」「どのような生活習慣の改善が 도움이になるのか?」「いつフォローアップすべきか?」
Q: 도움이 될可能性のある生活習慣の改善はありますか? A: ほとんどの 健康状態は、基本的な対策から Benefits を得られます:バランスの取れた栄養、定期的な運動(週150分以上)、质の良い睡眠(7〜9時間)、ストレス管理、タバコの回避。
医学的免責事項
この記事は教育目的のみを目的としており、医学的なアドバイスを构成するものではありません。健康、治療、またはケアに関する决定する前に、常に资格のある医療提供者にご相談ください。個人の状況は異なり、一人に適したものが別の人に适していない場合があります。
医学的緊急事態が発生した場合は、直ちに119番(またはお住まいの地域の緊急番号)に電話してください。