Bài 35.2: Tích hợp OpenAI API trong Next.js

Trí tuệ Nhân tạo (AI) đang thay đổi cách chúng ta tương tác với công nghệ, và việc tích hợp khả năng của AI vào các ứng dụng web đã trở nên dễ dàng hơn bao giờ hết nhờ các API. Một trong những API mạnh mẽ và phổ biến nhất hiện nay chính là OpenAI API, cung cấp quyền truy cập vào các mô hình ngôn ngữ tiên tiến như GPT-3.5, GPT-4 và nhiều mô hình khác.

Trong bài viết này, chúng ta sẽ đi sâu vào việc tích hợp OpenAI API vào một ứng dụng Next.js. Chúng ta sẽ xây dựng một ví dụ đơn giản, nơi người dùng nhập một yêu cầu (prompt) và ứng dụng sẽ gọi đến OpenAI API thông qua API Route của Next.js để nhận về phản hồi được tạo bởi AI.

Chuẩn bị

Trước khi bắt đầu, bạn cần đảm bảo có những thứ sau:

  1. Một dự án Next.js đang hoạt động: Bạn có thể tạo mới bằng npx create-next-app@latest.
  2. Một tài khoản OpenAI và API Key: Nếu chưa có, hãy đăng ký tại OpenAI website và tạo một API Key trong phần cài đặt tài khoản. Hãy giữ bí mật API Key này!
  3. Node.js và npm/yarn đã cài đặt: Next.js yêu cầu môi trường Node.js.

Cài đặt Thư viện OpenAI

Để tương tác với OpenAI API từ ứng dụng Next.js, chúng ta cần cài đặt thư viện chính thức của OpenAI cho Node.js.

Mở terminal trong thư mục gốc của dự án Next.js và chạy lệnh sau:

npm install openai
# hoặc với yarn
yarn add openai

Thư viện này sẽ giúp chúng ta dễ dàng gửi yêu cầu đến OpenAI API mà không cần phải tự xây dựng các HTTP request phức tạp.

Bảo mật API Key với Environment Variables

Tuyệt đối không nhúng trực tiếp API Key của bạn vào code frontend hoặc bất kỳ file nào được công khai. Cách an toàn nhất để quản lý các khóa bí mật như OpenAI API Key trong Next.js là sử dụng Environment Variables (biến môi trường).

Tạo một file mới có tên .env.local ở thư mục gốc của dự án Next.js (ngang hàng với package.json). File này sẽ không bị commit vào Git nếu bạn đang sử dụng file .gitignore mặc định của Next.js.

Thêm dòng sau vào file .env.local, thay thế your_secret_api_key_here bằng API Key thực tế của bạn:

OPENAI_API_KEY=your_secret_api_key_here

Trong code backend của Next.js (API Routes), bạn có thể truy cập giá trị này thông qua process.env.OPENAI_API_KEY.

Xây dựng API Route Backend

Việc gọi OpenAI API nên được thực hiện ở phía backend (server-side) thay vì frontend. Có hai lý do chính:

  1. Bảo mật: Giữ API Key an toàn trên server, không bao giờ tiết lộ ra trình duyệt của người dùng.
  2. Linh hoạt: Có thể xử lý logic phức tạp, tiền xử lý hoặc hậu xử lý dữ liệu trước khi gửi đến/từ OpenAI.

Chúng ta sẽ sử dụng tính năng API Routes của Next.js để tạo một endpoint backend đơn giản.

Tạo một file mới tại đường dẫn pages/api/generate.js (nếu bạn đang sử dụng Pages Router) hoặc app/api/generate/route.js (nếu sử dụng App Router). Ở đây, chúng ta sẽ tập trung vào ví dụ với Pages Router cho đơn giản, logic tương tự có thể áp dụng cho App Router.

Cấu trúc API Route cơ bản

File pages/api/generate.js sẽ chứa code xử lý yêu cầu từ frontend và gọi đến OpenAI API.

// pages/api/generate.js
import { OpenAI } from 'openai';

// Khởi tạo client OpenAI sử dụng API Key từ biến môi trường
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export default async function handler(req, res) {
  // Logic xử lý request và gọi OpenAI API sẽ ở đây
}

Giải thích:

  • import { OpenAI } from 'openai';: Import lớp OpenAI từ thư viện vừa cài đặt.
  • const openai = new OpenAI({ ... });: Tạo một instance của OpenAI client. Chúng ta truyền API Key thông qua process.env.OPENAI_API_KEY, mà Next.js đã load từ file .env.local cho môi trường server-side.
  • export default async function handler(req, res) { ... }: Đây là hàm xử lý chính cho API Route này. req là đối tượng request gửi đến, res là đối tượng response để gửi trả về. async được sử dụng vì chúng ta sẽ gọi các hàm bất đồng bộ của OpenAI API.
Xử lý Request và Gọi OpenAI API

Chúng ta muốn API Route này chỉ chấp nhận phương thức POST (thường dùng để gửi dữ liệu như prompt từ frontend). Chúng ta cũng cần lấy dữ liệu prompt từ body của request.

Bên trong hàm handler, thêm logic sau:

// pages/api/generate.js (Tiếp theo)
import { OpenAI } from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export default async function handler(req, res) {
  // Kiểm tra phương thức request
  if (req.method !== 'POST') {
    // Nếu không phải POST, trả về lỗi Method Not Allowed
    return res.status(405).json({ message: 'Method Not Allowed' });
  }

  // Lấy prompt từ body của request
  const { prompt } = req.body;

  // Kiểm tra xem prompt có tồn tại không
  if (!prompt) {
    return res.status(400).json({ message: 'Prompt is required' });
  }

  try {
    // Gọi OpenAI API để tạo completion (phản hồi)
    const completion = await openai.chat.completions.create({
      model: "gpt-3.5-turbo", // Chọn mô hình AI (có thể thay đổi)
      messages: [
        { role: "system", content: "You are a helpful assistant." }, // Vai trò của AI
        { role: "user", content: prompt } // Nội dung yêu cầu từ người dùng
      ],
      max_tokens: 150, // Giới hạn số lượng token trong phản hồi (tùy chọn)
      temperature: 0.7, // Độ "sáng tạo" của phản hồi (tùy chọn, 0.0 đến 2.0)
    });

    // Trích xuất nội dung phản hồi từ AI
    const assistantMessage = completion.choices[0]?.message?.content;

    // Trả về phản hồi dưới dạng JSON
    res.status(200).json({ text: assistantMessage });

  } catch (error) {
    // Xử lý lỗi nếu có vấn đề khi gọi API
    console.error("Error calling OpenAI API:", error);
    res.status(500).json({ message: 'Something went wrong', error: error.message });
  }
}

Giải thích:

  • if (req.method !== 'POST') { ... }: Đảm bảo chỉ xử lý request POST.
  • const { prompt } = req.body;: Next.js tự động parse body của request POST có Content-Type: application/json, cho phép chúng ta dễ dàng truy cập dữ liệu gửi lên (ở đây là prompt).
  • if (!prompt) { ... }: Kiểm tra đơn giản xem prompt có rỗng không.
  • try { ... } catch (error) { ... }: Khối try...catch rất quan trọng để bắt lỗi trong quá trình gọi API hoặc bất kỳ vấn đề nào khác.
  • await openai.chat.completions.create({ ... });: Đây là phần cốt lõi, gọi hàm create của OpenAI client để tạo ra đoạn văn bản dựa trên messages.
    • model: Chỉ định mô hình AI nào sẽ sử dụng. gpt-3.5-turbo là một lựa chọn phổ biến vì tốc độ và chi phí hiệu quả. Bạn có thể thử gpt-4 cho kết quả chất lượng cao hơn.
    • messages: Đây là một mảng các đối tượng đại diện cho cuộc trò chuyện. role: "system" thiết lập ngữ cảnh cho AI, role: "user" là nội dung yêu cầu của người dùng. Đây là định dạng chuẩn cho API Chat Completions.
    • max_tokens: Giới hạn độ dài của phản hồi.
    • temperature: Kiểm soát tính ngẫu nhiên/sáng tạo. Giá trị cao hơn (gần 2.0) tạo ra phản hồi đa dạng và bất ngờ hơn; giá trị thấp hơn (gần 0.0) tạo ra phản hồi tập trung và ít ngẫu nhiên hơn.
  • const assistantMessage = completion.choices[0]?.message?.content;: Trích xuất nội dung văn bản từ phản hồi của API. Cấu trúc phản hồi có thể hơi sâu, chúng ta lấy phần content của message đầu tiên trong mảng choices. Dấu ?. (optional chaining) giúp tránh lỗi nếu cấu trúc phản hồi không đúng như mong đợi.
  • res.status(200).json({ text: assistantMessage });: Gửi phản hồi thành công về frontend với status code 200 và dữ liệu là một đối tượng JSON chứa text phản hồi từ AI.
  • Trong khối catch, chúng ta log lỗi ra console server và gửi phản hồi lỗi về frontend với status code 500 (Internal Server Error).

Với API Route này, chúng ta đã có một endpoint backend sẵn sàng nhận prompt và gọi OpenAI API.

Tích hợp Frontend

Bây giờ, chúng ta sẽ tạo một giao diện đơn giản ở phía frontend để người dùng nhập prompt và hiển thị kết quả.

Sửa đổi file pages/index.js (hoặc tạo một component mới trong thư mục src/app nếu dùng App Router) để có một form và logic gọi API Route backend.

// pages/index.js
import { useState } from 'react';

function HomePage() {
  // State để lưu trữ prompt người dùng nhập
  const [prompt, setPrompt] = useState('');
  // State để lưu trữ phản hồi từ AI
  const [response, setResponse] = useState('');
  // State để quản lý trạng thái loading khi đang gọi API
  const [loading, setLoading] = useState(false);
  // State để lưu trữ thông báo lỗi
  const [error, setError] = useState(null);

  // Hàm xử lý khi form được submit
  const handleSubmit = async (e) => {
    e.preventDefault(); // Ngăn chặn refresh trang mặc định của form

    // Thiết lập trạng thái loading và xóa kết quả/lỗi cũ
    setLoading(true);
    setError(null);
    setResponse('');

    // Kiểm tra prompt rỗng
    if (!prompt) {
        setError('Please enter a prompt.');
        setLoading(false);
        return;
    }

    try {
      // Gọi API Route backend chúng ta vừa tạo
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ prompt: prompt }), // Gửi prompt trong body dưới dạng JSON
      });

      const data = await res.json(); // Parse phản hồi JSON

      // Kiểm tra nếu phản hồi không thành công (status code khác 2xx)
      if (!res.ok) {
        // Ném lỗi với thông báo từ backend hoặc thông báo mặc định
        throw new Error(data.message || 'Something went wrong.');
      }

      // Cập nhật state với phản hồi từ AI
      setResponse(data.text);

    } catch (err) {
      // Bắt lỗi và cập nhật state lỗi
      console.error("Fetch error:", err);
      setError(err.message); // Hiển thị thông báo lỗi cho người dùng
    } finally {
      // Dù thành công hay thất bại, dừng trạng thái loading
      setLoading(false);
    }
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1> Ask OpenAI with Next.js </h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Enter your prompt here..."
          style={{ marginRight: '10px', padding: '8px', minWidth: '300px' }}
          disabled={loading} // Vô hiệu hóa input khi đang loading
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Generating...' : 'Generate Response'}
        </button>
      </form>

      {/* Hiển thị thông báo lỗi */}
      {error && <p style={{ color: 'red', marginTop: '15px' }}>Error: {error}</p>}

      {/* Hiển thị phản hồi từ AI */}
      {response && (
        <div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
          <h2>🤖 AI Response:</h2>
          <p>{response}</p>
        </div>
      )}
    </div>
  );
}

export default HomePage;

Giải thích:

  • import { useState } from 'react';: Import hook useState để quản lý trạng thái trong component.
  • const [prompt, setPrompt] = useState('');: State cho input prompt.
  • const [response, setResponse] = useState('');: State để hiển thị phản hồi từ AI.
  • const [loading, setLoading] = useState(false);: State boolean để chỉ báo liệu request đang diễn ra hay không. Giúp hiển thị trạng thái cho người dùng (ví dụ: disable button).
  • const [error, setError] = useState(null);: State để lưu trữ thông báo lỗi nếu có.
  • const handleSubmit = async (e) => { ... }: Hàm bất đồng bộ được gọi khi form được submit.
    • e.preventDefault();: Ngăn chặn hành vi submit form mặc định của trình duyệt (gây reload trang).
    • Thiết lập trạng thái loading(true) và xóa các trạng thái responseerror trước đó.
    • await fetch('/api/generate', { ... });: Sử dụng API fetch của trình duyệt để gửi một request POST đến API Route /api/generate.
      • method: 'POST': Chỉ định phương thức HTTP.
      • headers: { 'Content-Type': 'application/json' }: Cho backend biết body là JSON.
      • body: JSON.stringify({ prompt: prompt }): Chuyển đổi đối tượng { prompt: prompt } thành chuỗi JSON để gửi đi.
    • const data = await res.json();: Chờ và parse body của phản hồi thành đối tượng JavaScript.
    • if (!res.ok) { ... }: Kiểm tra thuộc tính ok của đối tượng Response. Nó là true cho các status code trong khoảng 200-299. Nếu không ok, có nghĩa là backend đã trả về lỗi (ví dụ: 400, 405, 500).
    • throw new Error(...): Ném ra một Error để khối catch có thể bắt và xử lý.
    • setResponse(data.text);: Cập nhật state response với dữ liệu nhận được từ backend (data.text).
    • catch (err) { ... }: Bắt các lỗi xảy ra trong quá trình fetch hoặc lỗi được ném ra từ khối try.
    • finally { ... }: Khối này luôn chạy sau try hoặc catch, đảm bảo rằng setLoading(false) luôn được gọi để kết thúc trạng thái loading.
  • Phần JSX hiển thị form với input và button, sử dụng các state prompt, loading. Hiển thị errorresponse một cách có điều kiện ({error && <p>...</p>}).

Chạy thử

  1. Đảm bảo bạn đã thêm OPENAI_API_KEY vào file .env.local.
  2. Chạy ứng dụng Next.js:
    npm run dev
    # hoặc
    yarn dev
    
  3. Mở trình duyệt và truy cập http://localhost:3000.
  4. Nhập một prompt vào ô input, ví dụ: "Viết một đoạn thơ ngắn về mùa thu."
  5. Nhấn nút "Generate Response".
  6. Bạn sẽ thấy trạng thái loading, và sau đó là phản hồi được tạo ra bởi OpenAI API hiển thị trên trang.

Mở rộng và Cải tiến

Đây chỉ là điểm khởi đầu. Bạn có thể mở rộng ứng dụng này theo nhiều cách:

  • Streaming: Thay vì chờ toàn bộ phản hồi, bạn có thể cập nhật text theo thời gian thực khi OpenAI trả về từng phần (streaming). Điều này mang lại trải nghiệm người dùng tốt hơn cho các phản hồi dài. OpenAI APINext.js API Routes đều hỗ trợ streaming.
  • Parameters: Thử nghiệm với các parameters khác của openai.chat.completions.create như temperature, top_p, n (số lượng phản hồi), stop (ký tự dừng).
  • Model khác: Sử dụng các model khác như gpt-4 cho các tác vụ phức tạp hơn.
  • Xử lý lỗi nâng cao: Hiển thị thông báo lỗi chi tiết hơn dựa trên mã lỗi từ OpenAI.
  • Validation: Validate prompt ở cả frontend và backend (ví dụ: giới hạn độ dài).
  • Contextual Conversations: Mở rộng mảng messages để lưu trữ lịch sử cuộc trò chuyện, cho phép AI nhớ ngữ cảnh của các tương tác trước đó.
  • UI/UX: Cải thiện giao diện người dùng, thêm hiệu ứng loading đẹp mắt.
  • Rate Limiting: Nếu triển khai lên production, hãy xem xét việc áp dụng rate limiting cho API Route của bạn để tránh bị lạm dụng.

Việc tích hợp OpenAI API vào Next.js mở ra cánh cửa cho vô số tính năng AI mạnh mẽ trong ứng dụng web của bạn, từ tạo nội dung, tóm tắt văn bản, dịch thuật, phân tích cảm xúc, đến xây dựng chatbot thông minh và hơn thế nữa.

Comments

There are no comments at the moment.