Next.js
How to Integrate OpenAI & Gemini API in Next.js (Step-by-Step Guide)
Learn how to build full-stack AI applications by integrating OpenAI and Google Gemini APIs into Next.js App Router using Server Actions and Vercel AI SDK.
- Next.js
- OpenAI
- Gemini
On this page
A Next.js App Router app can call OpenAI and Google Gemini without exposing either API key to the browser. The route handler holds the key, streams tokens back, and a small client component renders the conversation with useChat from the Vercel AI SDK.
Prerequisites and package install
Start from an App Router project on Node.js 20 or newer. You need an OpenAI API key and a Gemini key from Google AI Studio. The AI SDK speaks to both providers through the same streaming interface, so the UI does not care which model answered.
- A Next.js App Router app.
- Accounts that can create
OPENAI_API_KEYandGOOGLE_GENERATIVE_AI_API_KEY. - The
aipackage plus the OpenAI and Google provider packages.
npm install ai @ai-sdk/openai @ai-sdk/googleEnvironment variables
Put both keys in .env.local. The OpenAI provider reads OPENAI_API_KEY. The Google provider reads GOOGLE_GENERATIVE_AI_API_KEY. Leave the NEXT_PUBLIC_ prefix off. A public variable is shipped to the browser.
OPENAI_API_KEY=your_openai_key
GOOGLE_GENERATIVE_AI_API_KEY=your_gemini_keyRestart next dev after saving the file. On Vercel, add the same names in the project environment settings for Production and Preview. Do not commit .env.local.
Stream from a route handler
Create app/api/chat/route.ts. The client sends the message history and which provider to use. The handler picks a model, streams the completion, and returns the data stream useChat expects.
import { google } from "@ai-sdk/google";
import { openai } from "@ai-sdk/openai";
import { streamText, type CoreMessage } from "ai";
export const maxDuration = 30;
type ChatRequest = {
messages: CoreMessage[];
provider?: "openai" | "gemini";
};
export async function POST(request: Request) {
const body = (await request.json()) as ChatRequest;
const messages = Array.isArray(body.messages) ? body.messages : [];
const model =
body.provider === "gemini"
? google("gemini-2.5-flash")
: openai("gpt-4o-mini");
const result = streamText({
model,
messages,
});
return result.toDataStreamResponse();
}Keep model names in this file, not in client code that you do not trust. Swapping gpt-4o-mini or gemini-2.5-flash for another model from the same provider does not change the rest of the route.
Render the chat with useChat
useChat posts to /api/chat by default, stores the messages, and appends streamed text to the latest assistant message. Pass body.provider when the reader switches between OpenAI and Gemini.
"use client";
import { useChat } from "ai/react";
import { useState } from "react";
export function ChatPanel() {
const [provider, setProvider] = useState<"openai" | "gemini">("openai");
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: "/api/chat",
body: { provider },
});
return (
<section>
<label>
Provider
<select
value={provider}
onChange={(event) => setProvider(event.target.value as "openai" | "gemini")}
>
<option value="openai">OpenAI</option>
<option value="gemini">Gemini</option>
</select>
</label>
<ol>
{messages.map((message) => (
<li key={message.id}>
<strong>{message.role}</strong>
<p>{message.content}</p>
</li>
))}
</ol>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} placeholder="Ask a question" />
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</section>
);
}Rate limits and server-side requests
The route is public as soon as you deploy it. Anyone who can reach /api/chat can spend your provider quota. Check the body, cap how often a caller can post, and keep the provider request on the server.
- 01
Reject a bad body
Require
messagesto be an array, cap its length, and ignore roles or fields you did not ask for. - 02
Limit bursts
Count calls per user or per IP inside a short window. A process-local map works on one server. A shared store is required when several serverless instances run at once.
- 03
Bound the work
Set
maxDurationon the route andmaxTokensonstreamTextso one prompt cannot run until the platform kills it.
const hits = new Map<string, { count: number; reset: number }>();
export function allowRequest(key: string, limit = 10, windowMs = 60_000) {
const now = Date.now();
const current = hits.get(key);
if (!current || current.reset < now) {
hits.set(key, { count: 1, reset: now + windowMs });
return true;
}
if (current.count >= limit) return false;
current.count += 1;
return true;
}Call allowRequest at the top of POST and return a 429 response when it returns false. Prefer a signed-in user id over the raw x-forwarded-for header, which a client can spoof. Log failures and status codes. Do not log the API keys or the full prompt if the conversation may contain private text.

