Blog / Coding tips

How to Add a Gemini API Key to Next.js and Vercel

Cover image for How to Add a Gemini API Key to Next.js and Vercel

To add a Gemini API key to a Next.js app and Vercel: create the key in Google AI Studio, put it in .env.local as GEMINI_API_KEY (with no NEXT_PUBLIC_ prefix), read it only in server code such as a route handler, then add the same variable in your Vercel project under Settings → Environment Variables and redeploy. The browser calls your route, your route calls Gemini, and the key never leaves the server. This is the setup I use in Quizwright, my Gemini quiz generator, and below I show each step plus a test proving where the key does and doesn't end up.

I tested this on Next.js 16 (App Router) with the official @google/genai SDK: I built the app with fake keys, searched the browser bundle for them, and called the route with a valid and an invalid key. If you want the deeper security story, like redacting errors and validating AI output, read how I kept my AI API key off the browser. This post is the practical setup.

Step 1: get a key from Google AI Studio

  1. Open aistudio.google.com/apikey and sign in with your Google account.
  2. Click Create API key. AI Studio may ask you to choose or create a Google Cloud project for it.
  3. Copy the key. Treat it like a password: anyone who has it can make requests that count against your quota or your bill.

Free-tier limits and prices change, so check Google's pricing page instead of trusting numbers in an old tutorial.

Step 2: put it in .env.local

Create a file called .env.local in the project root, next to package.json:

# .env.local (never commit this file)
GEMINI_API_KEY=paste-your-key-here

Next.js already ignores .env* files in the .gitignore it generates, but check yours. Then restart npm run dev. Next.js reads env files once at startup, so a running dev server won't see a new key.

Step 3: a server-only helper

I keep everything that touches the key in one file, lib/gemini.ts. Install the SDK and the guard package first with npm install @google/genai server-only:

import "server-only"; // build fails if a client component imports this file

import { GoogleGenAI } from "@google/genai";

const MODEL = process.env.GEMINI_MODEL?.trim() || "gemini-3.5-flash-lite";

export async function askGemini(prompt: string): Promise<string> {
  const apiKey = process.env.GEMINI_API_KEY?.trim();
  if (!apiKey) {
    throw new Error("GEMINI_API_KEY is not set");
  }

  const ai = new GoogleGenAI({ apiKey });
  const response = await ai.models.generateContent({
    model: MODEL,
    contents: prompt,
  });
  return response.text ?? "";
}

The first line is the important one. import "server-only" makes the build fail if any client component imports this file, even by accident through another file. I tested it by importing askGemini into a "use client" page, and next build stopped with "You're importing a module that depends on "server-only"" and the import trace that led there. That's a much better time to find the mistake than after deploying.

The key is also read inside the function, not at the top of the file. That way the project still builds on Vercel before you've added the variable, and a missing key gives a clear error at request time.

Step 4: the route handler

Create app/api/ask/route.ts. This is the only door between the browser and Gemini:

import { askGemini } from "@/lib/gemini";

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
  if (prompt === "" || prompt.length > 2000) {
    return Response.json({ error: "Send a prompt of 1 to 2000 characters." }, { status: 400 });
  }

  try {
    const text = await askGemini(prompt);
    return Response.json({ text });
  } catch (error) {
    // Details go to the server log only. The browser gets a fixed message.
    console.error("Gemini request failed:", error instanceof Error ? error.message : error);
    return Response.json({ error: "The AI request failed. Please try again." }, { status: 502 });
  }
}

Two habits here matter as much as hiding the key. The route checks its input on the server, because anyone can send any request to /api/ask, not just your page. It's the same "never trust the client" rule from my PHP form validation guide, just in TypeScript. And the error details go only to the server log, while the browser gets a fixed message.

When I called this route with a fake key, the server log showed Google's "API key not valid" error and the browser only got {"error":"The AI request failed. Please try again."} with status 502. With no key at all, the log said GEMINI_API_KEY is not set and the browser saw the same fixed message. Bad input, like an empty prompt or a body that isn't JSON, returned a 400 without calling Gemini at all.

Step 5: call it from the page

"use client";

import { useState } from "react";

export default function Home() {
  const [prompt, setPrompt] = useState("");
  const [answer, setAnswer] = useState("");

  async function ask() {
    setAnswer("Thinking…");
    const res = await fetch("/api/ask", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt }),
    });
    const data = await res.json();
    setAnswer(res.ok ? data.text : data.error);
  }

  return (
    <main>
      <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} />
      <button onClick={ask}>Ask Gemini</button>
      <p>{answer}</p>
    </main>
  );
}

The client component only knows the URL /api/ask. It never sees the key or the SDK. If async, await and fetch are still new to you, my JavaScript async/await tutorial covers them step by step, including checking res.ok.

Why NEXT_PUBLIC_ would leak the key

Any variable whose name starts with NEXT_PUBLIC_ is copied into the JavaScript files sent to the browser at build time. To see it for myself, I built a test page with two fake keys:

"use client";

export default function Leak() {
  console.log(process.env.GEMINI_API_KEY);             // undefined in the browser
  console.log(process.env.NEXT_PUBLIC_GEMINI_API_KEY); // the real key, baked into the JS
  return <p>Open the console</p>;
}

After next build, I searched the browser files with grep -rl "FAKE_PUBLIC" .next/static. The NEXT_PUBLIC_ value was sitting in plain text in a JavaScript chunk that anyone can download. The plain GEMINI_API_KEY value appeared in none of the files in .next/static. That's the whole rule: a NEXT_PUBLIC_ secret is not a secret. Run the same grep with the start of your real key before you deploy if you're ever unsure.

Step 6: add the key on Vercel

  1. Open your project on Vercel and go to Settings → Environment Variables.
  2. Add the name GEMINI_API_KEY and paste the key as the value. If Vercel offers to store it as a secret or sensitive value, choose that.
  3. Select the environments: Production and Preview, and Development if you use vercel dev.
  4. Save, then redeploy. Vercel applies variable changes only to new deployments, so the one already live won't see the key.

Vercel doesn't read your .env.local, because that file isn't in your Git repository. The dashboard is the only place the deployed server gets the key from. To copy the Development variables to your computer, the Vercel CLI has vercel env pull .env.local.

Troubleshooting

  • "GEMINI_API_KEY is not set" on Vercel: the variable is missing for that environment, or you didn't redeploy after adding it.
  • "API key not valid": the key was copied with a missing character, extra quotes or spaces, or it was deleted in AI Studio. The .trim() in the helper removes stray spaces.
  • The key is undefined in a component: that's correct behaviour in client components. Move the call to a route handler or server code.
  • A different key is used than the one in .env.local: real environment variables win over env files. While testing, I had a GEMINI_API_KEY exported in my terminal, and Next.js used it instead of my file. Check with echo $GEMINI_API_KEY.

FAQ

Can I call Gemini from a Server Component instead?

Yes. Server Components run on the server too, and the same server-only helper works there. Use a route handler when the browser needs to send input, like a prompt from a form.

What should I do if my key leaks?

Delete it in AI Studio right away, create a new one, update it in Vercel and .env.local, and redeploy. Removing it from a later commit isn't enough, because it stays in your Git history.

Should I commit an example env file?

Yes, a .env.example with the variable name and an empty value helps anyone who clones the project. Just never put the real value in it.

Conclusion

Using a Gemini API key safely in Next.js is mostly about where you read it: GEMINI_API_KEY in .env.local without NEXT_PUBLIC_, a helper protected with import "server-only", one route handler the browser calls, and the same variable in Vercel's settings followed by a redeploy. The Next.js guide to environment variables explains the loading order in more detail.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

Gemini Structured Output With Zod in Next.js
Coding tips

Gemini Structured Output With Zod in Next.js

Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.

October 2, 2026 · 9 min read

Build an Offline Habit Tracker With localStorage
Coding tips

Build an Offline Habit Tracker With localStorage

A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.

October 2, 2026 · 10 min read

Simple Math Captcha in PHP With Sessions
Coding tips

Simple Math Captcha in PHP With Sessions

A simple PHP math captcha in two functions: store the answer in the session, check it once with ctype_digit and ===, then add a honeypot and time check.

October 2, 2026 · 8 min read