Skip to content
47% off: the complete course for $79 (reg. $149)Order now →
AI Builder
Module 3Full course11 min read

AI Features in a Next.js App

Where AI calls belong in a modern React app — route handlers, server actions, and server components — plus keys, timeouts, and caching done right.

So far in this module you've built AI features as plain functions: summarize a ticket, extract fields, classify, rewrite. Now they need to live inside a real Next.js app, which raises questions the model docs don't answer. Should "Summarize" be a server action or an API route? Can a server component call the model while it renders? Where does the API key live so it never reaches the browser? What happens when the model takes 40 seconds and the platform kills the function at 30?

This lesson answers them for the Next.js 16 App Router, using Deskmate's ticket page. You'll leave with a clear rule for where each kind of AI call belongs, a server-only AI module, a server action that generates and stores a summary, a streaming route handler, timeouts set at every layer, and caching that doesn't regenerate the same output on every page view.

Rule one: model calls run on the server

Every model call in a web app runs on your server, never in the browser. A provider API key in client code is a key anyone can copy out of the bundle and spend. In Next.js, two habits make this hard to get wrong.

First, never prefix the key with NEXT_PUBLIC_. Variables with that prefix are inlined into client bundles at build time; everything else stays on the server. ANTHROPIC_API_KEY in .env.local (and in your host's secret settings for production) is all you need, since the SDK reads it automatically.

Second, put your AI client in a module marked server-only, so that importing it into a client component fails the build instead of silently shipping server code to the browser:

src/lib/ai.ts
import "server-only";
import Anthropic from "@anthropic-ai/sdk";
 
export const anthropic = new Anthropic({
  timeout: 45_000, // milliseconds; see "Timeouts at every layer" below
  maxRetries: 1,
});
 
export const MODELS = {
  fast: "claude-haiku-5-5",
  default: "claude-sonnet-5-5",
  best: "claude-opus-5-5",
} as const;

The server-only package (install it with npm i server-only) does nothing at runtime on the server. Its only job is to make the build error out if this file ends up in a client bundle. That one line protects you from the day someone imports MODELS into a client component to show a model name in the UI.

The examples below also use a database client and a session helper. The session helper stands in for whatever auth library you use; what matters is that it runs on the server and returns the current user's identity:

src/lib/db.ts
import "server-only";
import postgres from "postgres";
 
export const sql = postgres(process.env.DATABASE_URL!);
src/lib/session.ts
import "server-only";
import { cookies } from "next/headers";
import { sql } from "@/lib/db";
 
export type SessionUser = { id: string; workspaceId: string };
 
/** Stand-in for your auth library: resolves the session cookie to a user, or throws. */
export async function requireUser(): Promise<SessionUser> {
  const token = (await cookies()).get("session")?.value;
  if (!token) throw new Error("Unauthorized");
  const [user] = await sql<SessionUser[]>`
    select u.id, u.workspace_id as "workspaceId"
    from sessions s join users u on u.id = s.user_id
    where s.token = ${token} and s.expires_at > now()`;
  if (!user) throw new Error("Unauthorized");
  return user;
}