CreaTech
← All posts
ainextjsopenai

Building an AI feature into a Next.js app

Adding a real AI-powered interaction to a Next.js site: streaming responses, API routes, and keeping it from becoming a mess.

Clients are asking for AI features now. Not "make this look smart", but actual AI: a chatbot that knows the product catalogue, a tool that generates a draft, a form that gives instant feedback. Here's how I wire these up cleanly in a Next.js app.

Keep the AI call server-side

The API key never goes to the browser. Full stop. An API route (or a Server Action) handles the call:

// app/api/chat/route.ts
import OpenAI from "openai";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
export async function POST(request: Request) {
  const { message } = await request.json();
 
  const stream = await openai.chat.completions.create({
    model: "gpt-4o",
    stream: true,
    messages: [
      { role: "system", content: "You are a helpful assistant for CreaTech." },
      { role: "user", content: message },
    ],
  });
 
  return new Response(stream.toReadableStream());
}

The client calls /api/chat. It never touches the OpenAI SDK or the key.

Stream the response

Nobody wants to wait six seconds for a block of text to appear at once. Streaming makes the AI feel fast even when it isn't:

"use client";
async function sendMessage(message: string) {
  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ message }),
  });
 
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
 
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    setOutput(prev => prev + decoder.decode(value));
  }
}

Each chunk arrives as it's generated. The UI updates token by token. It feels responsive.

Constrain what the model can do

An open-ended chat is hard to control and easy to abuse. Narrow the model's scope with a system prompt that describes exactly what it's for:

You are a product assistant for AstraLuna. Answer questions about our tea
range, brewing guides, and delivery. If asked about anything else, politely
redirect to the topic at hand. Do not mention competitors.

A tight system prompt dramatically reduces unexpected outputs and keeps the feature focused.

Rate-limit the endpoint

Without rate limiting, a publicly accessible AI endpoint is expensive. Even basic limiting at the middleware level (five requests per IP per minute) prevents the obvious abuse cases:

// Vercel KV or a simple in-memory map for development
const requests = new Map<string, number>();
 
export function rateLimit(ip: string, limit = 5): boolean {
  const count = (requests.get(ip) ?? 0) + 1;
  requests.set(ip, count);
  setTimeout(() => requests.delete(ip), 60_000);
  return count <= limit;
}

For production, use a proper store like Vercel KV or Upstash Redis.

The pattern is the same across most AI features: keep the model call server-side, stream the output, constrain the prompt, and protect the endpoint. Everything else is product work.

Got a project in mind?

I build fast, production-grade sites for freelance clients. Let’s talk.