Build AI Apps with Next.js 16: Server Actions and Streaming Output
AI apps must call LLMs securely, stream tokens to the frontend in real time, and manage session state. This guide uses Next.js 16 App Router, Server Actions, and AI SDK to implement a clean, streamable AI chat page ready for production.
Key Points
- Topic Focus: Next.js、React
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Building an AI application essentially means solving three problems: how to call an LLM securely without exposing the API key to the frontend, how to stream tokens to users in real time, and how to manage state across multi-turn interactions. Next.js 16's App Router provides convenient primitives for all three: Server Components / Server Actions keep secrets on the server, streaming + AI SDK display tokens as they are generated, and cookies or a database store sessions.
The complete minimal implementation below explains how it all works.
1. Project Structure
app/
chat/page.tsx // Client: message list + input box
actions.ts // Server Action: call the LLM and stream the response
lib/
ai.ts // Model client wrapper
Secrets (ANTHROPIC_API_KEY / OPENAI_API_KEY) belong only in server-side environment variables and are never exposed to the frontend. This is an inherent advantage of a Server Action over having the frontend directly fetch an /api route: it is a server-side function, so none of its code is bundled into the client in the first place.
2. Model Wrapper (lib/ai.ts)
Use Vercel's ai SDK. It normalizes the streaming protocols of different providers behind one interface, eliminating the need to implement SSE parsing manually.
import { createAnthropic } from "@ai-sdk/anthropic";
import { streamText } from "ai";
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export function chat(system: string, messages: { role: "user" | "assistant"; content: string }[]) {
return streamText({
model: anthropic("claude-sonnet-5"),
system,
messages,
});
}
To switch models, change only this file:
createOpenAI,createGoogleGenerativeAI, andcreateMistraluse the same structure.
3. Server Action: Streaming Calls (app/actions.ts)
"use server";
import { chat } from "@/lib/ai";
export async function askAI(input: string) {
const result = chat(
"You are the assistant for the CodeNodeX AI community. Answer concisely and accurately in Chinese.",
[{ role: "user", content: input }],
);
// streamUI / streaming text: return the result to the client component as a stream
return result.toUIMessageStream();
}
A Server Action is marked with "use server". The frontend calls it like an asynchronous function, but it actually runs on the server. You do not need to write an /api/route, and you do not need to worry about leaking the key.
4. Client: Display Tokens as They Arrive (app/chat/page.tsx)
Use useChat (ai/react) to manage state and receive the stream:
"use client";
import { useChat } from "ai/react";
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: "/api/chat", // You can also call the action directly; a route is more intuitive here
});
return (
<main className="mx-auto max-w-2xl p-6">
<div className="space-y-4">
{messages.map((m) => (
<div key={m.id} className={m.role === "user" ? "text-right" : ""}>
<span className="whitespace-pre-wrap">{m.content}</span>
</div>
))}
{isLoading && <span className="animate-pulse">Thinking…</span>}
</div>
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
<input value={input} onChange={handleInputChange}
className="flex-1 rounded border px-3 py-2" placeholder="Ask something…" />
<button className="rounded bg-black px-4 py-2 text-white">Send</button>
</form>
</main>
);
}
Tokens are appended to the end of messages as they are generated, producing the "typewriter" effect users see. Behind the scenes, AI SDK handles SSE and stream parsing for you; you only need to work with messages.
5. Persist the Conversation (Optional but Recommended)
The state managed by useChat exists only in memory and disappears after a refresh. To persist it, save every turn to the database in the Server Action and associate it with a sessionId stored in a cookie:
"use server";
import { cookies } from "next/headers";
import { db } from "@/lib/db";
export async function askAI(input: string) {
const sessionId = cookies().get("sid")?.value ?? crypto.randomUUID();
cookies().set("sid", sessionId);
await db.message.create({ data: { sessionId, role: "user", content: input } });
// ...call the model and return the stream; save the assistant message before/after returning
}
The next time the user returns, load the history by sessionId to continue the multi-turn conversation.
6. Three Things to Consider in Production
- Rate limiting and abuse prevention. AI calls are billed by token, so the endpoint must be rate-limited by user/IP to prevent abuse. upstash/ratelimit or middleware will work.
- Input validation. Validate
inputwith zod, limit its length, and filter dangerous content. A Server Action is not "secure by default"; it is a publicly accessible entry point. - Edge vs Node runtime. Streaming responses have lower latency and smaller cold starts on the Edge Runtime, but not every SDK is Edge-compatible. Once traffic grows, it is worth benchmarking both runtimes.
Summary
Next.js 16 already provides an excellent experience for building AI applications:
- Secure secrets: Server Actions/Components run on the server, so keys never enter the frontend bundle.
- Simple streaming: AI SDK +
useChatcan deliver a typewriter-style conversation in just a few dozen lines. - Natural state management: Cookies and a database handle sessions, and App Router's data-flow model is sufficient.
Once these three pieces are in place, a production-ready AI chat page can be delivered in an afternoon. The remaining effort is better spent on prompt tuning, rate limiting, and product experience than on hand-crafting an SSE pipeline.
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
How Should a Technical Community Design Its Tag System and Recommendation Feed?
Centered on independent development, Next.js technology trends, engineering practices, and the indie developer perspective.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.