Skip to content
Developer working on a laptop with abstract AI and code visuals on screen
Next.jsOpenAIAPI RoutesAI IntegrationWeb Development

How to Integrate OpenAI in Next.js

9 min readby Akshath P, R & D Head, Erratum Solutions

Adding AI features to a Next.js app starts with one rule: keep your OpenAI API key on the server. This walkthrough covers installing the official SDK, wiring an App Router API route, streaming responses to the browser, and the security checks you need before shipping.

Whether you want a simple chat widget, text summarization, or classification behind your own UI, the same server-side pattern applies. We use it on production Next.js projects at Erratum Solutions, and the steps below are the foundation we start from.

The API key never belongs in client-side code. Route Handlers in the App Router give you a clean place to call OpenAI, validate input, and stream results back to React components safely.

Discuss this article

Questions after reading? Email or WhatsApp us with your scope and timeline. We reply within one business day.

Email us

connect@erratums.com

Mon–Fri · Replies within one business day

AI summary

Integrate OpenAI in Next.js by installing the openai SDK, storing the API key in server environment variables, creating a singleton client, and calling openai.responses.create from an App Router POST handler. Stream text deltas back to a Client Component with fetch and ReadableStream. Never expose keys in the browser.

  • Prerequisites: Next.js App Router, Node 18+, OpenAI account and API key.
  • Server pattern: lib/openai-client.ts singleton + app/api/chat/route.ts POST handler.
  • Client pattern: fetch the route, read the response body stream, append chunks to state.
  • Security: env vars only on server, input validation, rate limits, graceful 503 when unconfigured.
  • Production: pick a cost-appropriate model, log errors server-side, handle stream failures.

Key takeaways

  • Install the official openai package and store OPENAI_API_KEY in .env.local, never in client bundles.
  • Create a singleton OpenAI client in a server-only module so connections are reused across requests.
  • Expose AI features through App Router Route Handlers (app/api/.../route.ts), not from Client Components directly.
  • Streaming responses with the Responses API keeps chat UIs fast and reduces perceived wait time.
  • Validate input, rate-limit callers, and return clear errors when the API key or model is missing.

Why integrate OpenAI in Next.js

Next.js gives you React for the UI and server-side Route Handlers for backend logic in one repo. That split is a good fit for OpenAI: the browser renders components, the server holds secrets and talks to the model.

Common use cases include customer support chat, summarizing long documents, generating draft copy, classifying support tickets, and powering internal tools. Each one follows the same shape: collect input on the client, send it to your API route, call OpenAI, return text or structured data.

Prerequisites

You need a Next.js project using the App Router (the app/ directory), Node.js 18 or later, and an OpenAI account with an API key from the OpenAI dashboard. Create a new project with create-next-app if you are starting fresh.

This guide uses the official openai npm package (v6+) and the Responses API, which is the current recommended path for text generation and streaming in the Node SDK.

Install the OpenAI SDK

Add the package to your project. You only need this one dependency for server-side calls; do not install a separate client SDK for the browser.

Terminal
npm install openai

Set environment variables

Create a .env.local file at the project root. Next.js loads it automatically in development and expects the same variable names in production hosting (Vercel, Docker, etc.).

Prefixing is not required for server-only variables. Do not use NEXT_PUBLIC_ for the API key, because that prefix exposes values to the client bundle.

.env.local
OPENAI_API_KEY=sk-your-key-here
OPENAI_MODEL=gpt-4o-mini

Create a server-side OpenAI client

Instantiate OpenAI once and reuse it. Creating a new client on every request adds overhead. A small helper module also centralizes the check for a missing API key.

Place this file under lib/ or app/api/_lib/. Import it only from Route Handlers, Server Actions, or other server modules, never from Client Components.

lib/openai-client.ts
import OpenAI from "openai";

let client: OpenAI | null = null;

export function getOpenAIClient(): OpenAI | null {
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) return null;
  if (!client) {
    client = new OpenAI({ apiKey });
  }
  return client;
}

export function getChatModel(): string {
  return process.env.OPENAI_MODEL ?? "gpt-4o-mini";
}

Build an API Route Handler

Add a POST handler at app/api/chat/route.ts. It reads JSON from the request body, calls OpenAI, and returns the model output. Start with a non-streaming version to confirm the wiring, then add streaming in the next section.

Always validate the body shape before calling the API. Reject empty messages and cap string length so callers cannot send huge payloads.

app/api/chat/route.ts (non-streaming)
import { getOpenAIClient, getChatModel } from "@/lib/openai-client";

export async function POST(request: Request) {
  const openai = getOpenAIClient();
  if (!openai) {
    return Response.json(
      { error: "OpenAI is not configured. Add OPENAI_API_KEY." },
      { status: 503 },
    );
  }

  const body = await request.json();
  const message = typeof body.message === "string" ? body.message.trim() : "";
  if (!message || message.length > 4000) {
    return Response.json({ error: "Invalid message." }, { status: 400 });
  }

  const response = await openai.responses.create({
    model: getChatModel(),
    input: message,
  });

  const text = response.output_text ?? "";
  return Response.json({ text });
}

Stream responses to the browser

For chat UIs, enable stream: true on responses.create and pipe text deltas into a ReadableStream. The client reads the stream with response.body.getReader() and updates the UI as chunks arrive.

Handle response.output_text.delta events in your stream loop. Close the stream when the model finishes or return a friendly error if the response fails.

app/api/chat/route.ts (streaming)
import { getOpenAIClient, getChatModel } from "@/lib/openai-client";

function textDeltaStream(
  openaiStream: AsyncIterable<{ type: string; delta?: string }>,
): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  return new ReadableStream({
    async start(controller) {
      for await (const event of openaiStream) {
        if (event.type === "response.output_text.delta" && event.delta) {
          controller.enqueue(encoder.encode(event.delta));
        }
      }
      controller.close();
    },
  });
}

export async function POST(request: Request) {
  const openai = getOpenAIClient();
  if (!openai) {
    return new Response("OpenAI is not configured.", { status: 503 });
  }

  const { message } = await request.json();
  const stream = await openai.responses.create({
    model: getChatModel(),
    input: message,
    stream: true,
  });

  return new Response(textDeltaStream(stream), {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

Call the route from a Client Component

Mark your chat UI with "use client" and POST to /api/chat. For streaming, read the response body with a reader loop and append each decoded chunk to React state.

Disable the submit button while a request is in flight and show errors from non-OK responses so users know when the server or model failed.

components/ChatBox.tsx
"use client";

import { useState } from "react";

export function ChatBox() {
  const [input, setInput] = useState("");
  const [reply, setReply] = useState("");
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setReply("");

    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: input }),
    });

    if (!res.ok || !res.body) {
      setReply("Something went wrong.");
      setLoading(false);
      return;
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let text = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
      setReply(text);
    }

    setLoading(false);
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea value={input} onChange={(e) => setInput(e.target.value)} />
      <button type="submit" disabled={loading}>Send</button>
      <p>{reply}</p>
    </form>
  );
}

Security checklist

Keep the API key in server environment variables only. Audit imports so getOpenAIClient is never pulled into a Client Component.

Validate and sanitize all user input before sending it to OpenAI. Add per-IP or per-user rate limiting on the Route Handler so one client cannot abuse your endpoint.

Return generic error messages to the browser and log detailed errors on the server. Check request origin or CSRF tokens if the route is callable from browsers on other sites.

Production tips

Pick a model that matches your quality and cost needs. Smaller models are fine for drafts and classification; use larger models only where accuracy matters.

Monitor token usage in the OpenAI dashboard and set billing alerts. Wrap OpenAI calls in try/catch and return 502 or 503 with a short message when the upstream API fails.

If AI is core to your product, plan for retries, timeouts, and fallbacks. Erratum Solutions can help harden streaming chat, lead capture, and multi-step flows for production traffic.

Close-up of code on a monitor showing a web application API integration

Continue with these guides and services from Erratum Solutions.

Frequently asked questions

Can I call OpenAI directly from a Next.js Client Component?

You should not. Client Components ship to the browser, so any key embedded there is visible to users. Always call OpenAI from a Route Handler or Server Action and pass only the model output to the client.

Does this work with the Pages Router?

The openai SDK works the same way, but you would use pages/api routes instead of app/api Route Handlers. This guide targets the App Router because that is the default in modern Next.js projects.

Should I use streaming or wait for the full response?

Streaming is better for chat and long-form generation because users see text appear immediately. For short, structured outputs like JSON classification, a single non-streaming call is often simpler.

How much does OpenAI cost in a Next.js app?

Cost depends on the model and token usage per request. Start with a smaller model such as gpt-4o-mini for development, set usage alerts in your OpenAI dashboard, and add rate limiting so one user cannot run up your bill.

What happens if OPENAI_API_KEY is missing?

Your server module should return null when the key is absent, and the Route Handler should respond with a 503 and a plain message. Never throw an unhandled error that leaks stack traces to clients.

Can Erratum Solutions help build AI features into my product?

Yes. We integrate OpenAI and other providers into web apps as part of custom software and web app development engagements. Reach out if you need architecture review, streaming chat, or production hardening beyond a starter setup.

Start a conversation

Tell us what you are building

Your details open a drafted email in your mail app. Nothing is stored on our servers. We reply within one business day.

Get in touch

Send your brief

Opens in your email app. Nothing is stored on our servers.

Service interest

Opens in your email app, addressed to connect@erratums.com

  • 01

    Send your brief

    Share goals, timeline, and any constraints through the form, email, or WhatsApp.

  • 02

    We reply within one business day

    You get a direct response with fit, clarifying questions, and suggested next steps.

  • 03

    Discovery call

    When there is a match, we schedule a call to walk through scope, phases, and delivery.

Ready to turn this into a project?

Share scope, timeline, and what success looks like. We will map architecture, delivery phases, and a sensible next step.