Back to blogs
AI 2025-04-08 12 min read

Integrating OpenAI Into a Next.js App: A Complete Production Guide

Beyond the hello-world examples — here's how to integrate OpenAI into a Next.js application with proper error handling, rate limiting, cost controls, and user experience considerations.

Why Most OpenAI Integrations Are Production Nightmares

The OpenAI quickstart docs will get you a working chatbot in 20 minutes. Getting it production-ready takes another 2–3 days if you know what you're doing — and 2–3 weeks if you don't.

I've built production AI features for 4 client projects. Here's everything I wish I'd known before the first one.


The Right Architecture for AI Features in Next.js

Never call OpenAI directly from the browser. Always route through your own API:

Browser → Your Next.js API Route → OpenAI API

This gives you:

- A place to implement rate limiting (critical)

- A place to log requests for debugging

- No exposed API keys in client code

- The ability to add caching


Setting Up the API Route

// app/api/ai/chat/route.ts

import OpenAI from 'openai'

import { NextRequest, NextResponse } from 'next/server'

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

export async function POST(req: NextRequest) {

try {

const { message, conversationHistory } = await req.json()

// Validate input

if (!message || message.length > 2000) {

return NextResponse.json({ error: 'Invalid message' }, { status: 400 })

}

const response = await openai.chat.completions.create({

model: 'gpt-4o-mini', // Use mini for cost efficiency

messages: [

{ role: 'system', content: YOUR_SYSTEM_PROMPT },

...conversationHistory.slice(-10), // Limit context window

{ role: 'user', content: message }

],

max_tokens: 1000,

temperature: 0.7,

})

return NextResponse.json({

reply: response.choices[0].message.content,

usage: response.usage

})

} catch (error) {

if (error instanceof OpenAI.APIError) {

return NextResponse.json(

{ error: 'AI service temporarily unavailable' },

{ status: error.status }

)

}

return NextResponse.json({ error: 'Internal error' }, { status: 500 })

}

}


Rate Limiting — Non-Negotiable

Without rate limiting, a single user (or bot) can drain your entire monthly OpenAI budget in hours. I've seen this happen.

For Next.js, I use Upstash Redis for rate limiting:

import { Ratelimit } from '@upstash/ratelimit'

import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({

redis: Redis.fromEnv(),

limiter: Ratelimit.slidingWindow(20, '1 h'), // 20 requests per hour

})

// In your API route:

const identifier = req.ip ?? 'anonymous'

const { success, remaining } = await ratelimit.limit(identifier)

if (!success) {

return NextResponse.json(

{ error: 'Rate limit exceeded. Please try again later.' },

{ status: 429 }

)

}

Upstash has a generous free tier. This is one of those investments that takes 30 minutes and prevents a catastrophe.


Cost Control: The Three Levers

**1. Model Selection**

GPT-4o costs ~10x more than GPT-4o-mini. For most tasks — summarization, classification, simple Q&A — mini is indistinguishable in quality. Default to mini, upgrade only when you have a specific reason.

**2. Context Window Management**

Each token costs money. Don't blindly pass the entire conversation history:

// Bad: sends everything

messages: [...allMessages, userMessage]

// Good: sliding window

messages: [...allMessages.slice(-8), userMessage]

**3. Response Length Limits**

Always set `max_tokens`. A runaway response can cost 10x what you expect. For chat: 1000 tokens. For summaries: 500. For classifications: 50.


Streaming Responses (The Right Way)

Streaming makes AI features feel instant instead of frozen. Here's the pattern that works in Next.js:

// API Route — Stream response

const stream = await openai.chat.completions.create({

...params,

stream: true,

})

const encoder = new TextEncoder()

const readable = new ReadableStream({

async start(controller) {

for await (const chunk of stream) {

const content = chunk.choices[0]?.delta?.content ?? ''

controller.enqueue(encoder.encode(content))

}

controller.close()

}

})

return new Response(readable, {

headers: { 'Content-Type': 'text/plain; charset=utf-8' }

})

// Client — Consume stream

const response = await fetch('/api/ai/chat', { method: 'POST', body: ... })

const reader = response.body.getReader()

const decoder = new TextDecoder()

while (true) {

const { done, value } = await reader.read()

if (done) break

setMessage(prev => prev + decoder.decode(value))

}


The UX Details That Make AI Features Feel Premium

1. **Show a typing indicator** while waiting for the first token

2. **Disable the input** while a response is streaming (prevent double submissions)

3. **Show a stop button** during streaming so users can cancel long responses

4. **Handle the error state gracefully** — "Something went wrong, please try again" beats a blank response

5. **Add a character counter** if there's an input length limit


Monitoring: You Need This From Day One

Set up basic logging for every AI request:

await db.aiLog.create({

data: {

userId: session.user.id,

model: 'gpt-4o-mini',

promptTokens: response.usage.prompt_tokens,

completionTokens: response.usage.completion_tokens,

cost: calculateCost(response.usage), // write this helper

latencyMs: Date.now() - startTime,

}

})

This gives you per-user cost visibility, latency trends, and the data to optimize intelligently.


*Building an AI feature and want to get it right the first time? [Let's talk](/contact).*

#AI#Next.js#OpenAI#Tutorial#Production