アルアカ - Arcadia Academia

Arcadia Academiaは「エンジニアリングを楽しむ」を合言葉に日本のデジタル競争力を高めることをミッションとするテックコミュニティです。

Next.jsでGemini APIを利用して構造化されたデータを取得してみる

Featured image of the post

近年、生成AI(Generative AI)は、さまざまな分野で革新的な応用が進んでおり、テキスト生成、画像生成、音声生成など、多岐にわたる能力を発揮しています。一方で、Next.jsはReactを基盤としたフレームワークで、効率的なWebアプリケーション構築を可能にします。

本記事では、Next.jsと生成AIを組み合わせたアプリケーションの開発手法について、試してみた知見を共有したいと思います。

💡
利用している主な技術

Next.js 15
Gemini model gemini-1.5-flash
[目次を開く]

料金について

今回、利用するgemini-1.5-flashをはじめすべてのモデルが無料で使えるようです。以下のような無料枠が設けられています。

Image in a image block

ちょっとした検証などするには無料で十分ですね。

また無料枠の上限以上の利用が必要な場合は従量課金での利用もできるみたいです。

Image in a image block

設定しないと課金情報はセットされないので基本的にはいつの間にか料金が発生するということはないと思いますが、ご利用される方は、公式の情報をよく読んでご注意ください。

以下、料金についての公式ページです。

APIキーの取得

先ずはGeminiをAPIで利用するためにAPIキーを取得しましょう。

以下のURLからGoogle AI Studioにアクセスしてください。

(Googleアカウントでのログインが必要です)

ページを開くと新しいプロンプトの画面が表示されるので左上の「Get API Key」をクリックしてください。

Image in a image block

すると「キーAPIキーを作成」(若干、日本語があやしいですが。。。)というボタンがあるので、そちらをクリックしてください。

Image in a image block

作成したAPIキーはGoogle AI Studioでいつでも確認できます。Webやローカルで保存する場合、取扱には十分注意してください。

Next.jsのセットアップ

💡
前提条件
Nodeなどはインストールされていること

※Nodeがインストールされていない場合は以下の記事を参考にインストールからお願いします。
📄Arrow icon of a page linknvmでNode.jsをバージョン管理する

以下のコマンドでプロジェクトを作成していきましょう。

# next-app-geminiというプロジェクトを作成
npx create-next-app@latest next-app-gemini --use-npm --ts --tailwind --eslint --app --src-dir

npx create-next-app@latest のあとに続いているのは、TypeScript、tailwind 、eslint 、App Router、srcディレクトリを利用するオプションです。

必要なライブラリをインストールする

npm install @google/generative-ai

これでGemini APIを使うためのNext.jsのセットアップが完了しました。

Gemini APIを使ってみる

では、簡単なUIとAPIを実装して実際に使ってみましょう。

以下、ディレクトリ構成です。

/app
  /api
    /generate
      route.ts
  /page.tsx

※プロジェクトのルートに.env.local を作成しAPIキーを定義してください。

# .env.local
GEMINI_API_KEY={your api key}

page.tsxにUIを実装

'use client';

import { useState } from 'react';

interface RecipeResponse {
  recipeName: string;
  ingredients: string[];
  steps: string[];
}

export default function HomePage() {
  const [prompt, setPrompt] = useState<string>(''); // プロンプト
  const [result, setResult] = useState<RecipeResponse | null>(null); // 結果をオブジェクトで管理
  const [loading, setLoading] = useState<boolean>(false); // ローディング状態

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setResult(null);

    try {
      // APIリクエスト送信
      const response = await fetch('/api/generate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ input: prompt }),
      });

      const data = await response.json();
      console.log('Response:', data);

      if (response.ok && data.response) {
        // レスポンスから文字列形式のJSONを抽出してパース
        const jsonString = data.response.match(/```json\n([\s\S]*?)\n```/)?.[1];
        if (jsonString) {
          const parsedResult: RecipeResponse = JSON.parse(jsonString);
          setResult(parsedResult);
        } else {
          console.error('Failed to parse JSON from response.');
          setResult(null);
        }
      } else {
        console.error(`Error: ${data.error}`);
        setResult(null);
      }
    } catch (error) {
      console.error('Error:', error);
      setResult(null);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>料理手順生成デモ</h1>
      <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
        <textarea
          rows={4}
          style={{ width: '100%', marginBottom: '10px', padding: '10px' }}
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="料理名を入力してください"
        />
        <button
          type="submit"
          disabled={loading}
          style={{
            padding: '10px 20px',
            background: loading ? '#ccc' : '#0070f3',
            color: 'white',
            border: 'none',
            cursor: loading ? 'not-allowed' : 'pointer',
          }}
        >
          {loading ? '生成中...' : '生成'}
        </button>
      </form>

      {/* 結果を表示 */}
      {result ? (
        <div>
          <h2>料理名: {result.recipeName}</h2>
          <h3>材料:</h3>
          <ul style={{ paddingLeft: '20px' }}>
            {result.ingredients.map((ingredient: any, index: number) => (
              <li key={index} style={{ marginBottom: '5px' }}>
                {ingredient}
              </li>
            ))}
          </ul>
          <h3>手順:</h3>
          <ol style={{ paddingLeft: '20px' }}>
            {result.steps.map((step, index) => (
              <li key={index} style={{ marginBottom: '10px' }}>
                {index + 1}: {step}
              </li>
            ))}
          </ol>
        </div>
      ) : (
        !loading && <p></p>
      )}
    </div>
  );
}

layout.tsxを編集

import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body>
        {children}
      </body>
    </html>
  );
}

route.tsを編集

import { NextResponse } from "next/server";
import { GoogleGenerativeAI } from "@google/generative-ai";

const apiKey = process.env.GEMINI_API_KEY;

if (!apiKey) {
  throw new Error("APIキーを設定してください");
}

const genAI = new GoogleGenerativeAI(apiKey);

export async function POST(request: Request) {
  console.log('Request:', request);
  try {
    const { input } = await request.json();
    const prompt = `
  以下のJSONスキーマを使用して、料理名 "${input}" の材料と手順を記述してください。応答は日本語で記述し、正確に表現してください:
  {
    "recipeName": string,
    "ingredients": Array<string>,
    "steps": Array<string>
  }
  例:
  {
    "recipeName": "カレー",
    "ingredients": [
      "玉ねぎ 2個",
      "肉 200g",
      "カレー粉 大さじ2",
      "水 500ml",
      "野菜(にんじん、じゃがいもなど) 適量"
    ],
    "steps": [
      "玉ねぎをみじん切りにして炒める。",
      "肉を加えて火が通るまで炒める。",
      "カレー粉を加え、水を注いで煮込む。",
      "野菜を加え、さらに煮込む。",
      "ご飯にかけて完成。"
    ]
  }
`;



    // モデルを取得
    const model = genAI.getGenerativeModel({
      model: "gemini-1.5-flash",
    });

    // プロンプトを生成AIに送信
    const result = await model.generateContent(prompt);
    console.log('Result:', result);
    // 結果をレスポンスとして返却
    return NextResponse.json({
      response: result.response.text(),
    });
  } catch (error: any) {
    console.error("Error generating content:", error.message);

    return NextResponse.json(
      {
        error: error.message || "An unexpected error occurred.",
      },
      { status: 500 }
    );
  }
}

※公式のリファレンスにあったコードをもとにしています。

料理名を入力しレシピをステップバイステップで表示してくれるデモを作成してみました。

Image in a image block

まとめ

こういう形で構造化データを定義して展開できると考えると、すごい可能性を感じますね。工夫次第で実用性のあるアプリができそうです。みなさんも、ぜひ、オリジナルのWebアプリを作成してみてください!

あなたを爆速で成長させるメンタリングプログラムはこちらから↓↓

RUNTEQ(ランテック) - 実践型Webエンジニア養成プログラミングスクールの入会