OpenAI
A complete AI layer for Next.js App Router, built directly on the official
openai SDK — no intermediate AI framework. Covers text generation,
streaming, structured (Zod-validated) outputs, image generation, embeddings
with semantic search, content moderation, and tool calling, with rate
limiting and input validation composed in by default.
Dependencies
openai: Official SDK — every provider call inservice.tsgoes through it.zod: Input validation and structured-output schemas.zod-to-json-schema: Converts tool parameter schemas (Zod) to JSON Schema for function calling.server-only: Build-time guard soservice.ts/actions.tsnever reach the client bundle.
Folder structure
ai/openai/
├── constants.ts # Models, defaults, limits, retry/rate-limit config, env var names — single source of truth
├── types.ts # Shared TypeScript types (AIResult<T>, chat messages, generation params, etc.)
├── errors.ts # Normalized AIError class + OpenAI SDK error mapping (keeps the raw provider error code)
├── schemas.ts # Zod schemas: input validation + structured-output examples (summary, translation, extraction)
├── rate-limit.ts # Sliding-window rate limiter, in-memory by default, pluggable store interface
├── service.ts # INTERNAL — raw OpenAI SDK calls (client init, generation, streaming, tools, embeddings, moderation)
├── actions.ts # PUBLIC — "use server" API: chat, summarize, translate, extractData, askWithTools, image, embed, moderate
├── client.ts # Client-safe fetch helper for streaming (the one operation that needs a Route Handler)
├── hooks.ts # React hooks for Client Components, built on actions.ts (+ client.ts for streaming)
├── prompts/
│ ├── system.ts # Base system prompts (default, summarizer, translator, extractor, tool-using assistant)
│ ├── builder.ts # Prompt interpolation, context truncation, message-array assembly
│ ├── summarize.ts # Summarization prompt + paired Zod schema
│ ├── translate.ts # Translation prompt + paired Zod schema
│ └── extract.ts # Entity-extraction prompt + paired Zod schema
├── tools/
│ ├── calculator.ts # Example tool — safe arithmetic (no eval/Function)
│ ├── search.ts # Example tool — web search via a pluggable provider (Tavily-shaped by default)
│ └── index.ts # Tool registry: defaultTools, getToolByName, selectTools
├── routes/
│ └── chat-stream.route.ts # Exact content to copy to app/api/ai/chat/stream/route.ts (streaming only)
└── env.example
How the files fit together
service.ts(internal) talks to the OpenAI API directly. It owns client initialization, retries with backoff, and every raw operation (generateText,streamText,generateStructuredObject,generateImage,generateEmbedding,moderateContent,callWithTools). Nothing here is meant to be imported from a component — it exists to keep provider-specific logic out of the public API.actions.ts(public) is the entry point for everything server-side. Every export is a Server Action: call it directly from a Server Component, a form, or another Server Action — and it can also be imported straight into a Client Component, since Next.js handles that boundary for you. Each action composesservice.tswith input validation (schemas.ts) and guardrails (rate limiting + moderation via aRequestContextargument). Every action resolves toAIResult<T>({ success, data }or{ success, error }) instead of throwing.client.tsexists only for streaming — a Server Action can't return aReadableStream, sostreamChatfetches the one bundled Route Handler (routes/chat-stream.route.ts) and exposes the response as text deltas. Everything else in this module never touchesfetch.hooks.tswrapsactions.ts(andclient.tsfor streaming) in React state — loading/data/error handling and stale-response guarding — so Client Components don't manage that by hand.
What actions.ts covers
chat— single-turn or multi-turn text generation (non-streaming).askWithTools— full tool-calling round trip (function calling), restricted to registered tool names for safety (toolNames?: string[]).summarize,translate,extractData— structured, schema-validated generation for the three built-in tasks.image— image generation.embed/semanticSearch— embeddings, plus a naive in-memory cosine-similarity ranker for prototyping semantic search.moderate— standalone content moderation check.
Streaming chat is intentionally not here — see routes/chat-stream.route.ts
and hooks.ts's useChatStream instead.
Usage examples
Chat (Server Action)
import { chat } from "@/ai/openai/actions";
const result = await chat({ prompt: "Give me a haiku about the sea." }, { userId, plan: "FREE" });
if (result.success) {
console.log(result.data.text);
} else {
console.error(result.error.message);
}
Chat — streaming
Copy routes/chat-stream.route.ts to app/api/ai/chat/stream/route.ts
once, then consume it from a Client Component:
"use client";
import { useChatStream } from "@/ai/openai/hooks";
function StreamingChat() {
const { sendMessage, text, isStreaming, stop } = useChatStream();
return (
<div>
<button onClick={() => sendMessage({ prompt: "Write a short story." })}>Ask</button>
{isStreaming && <button onClick={stop}>Stop</button>}
<p>{text}</p>
</div>
);
}
Tool calling
import { askWithTools } from "@/ai/openai/actions";
const result = await askWithTools({
prompt: "What is (245 + 15) * 3?",
toolNames: ["calculator"], // omit to expose every registered tool
});
// result.data => { text, toolCalls, toolResults, usage }
Summarize / translate / extract
import { summarize, translate, extractData } from "@/ai/openai/actions";
const summary = await summarize({ text: article, maxWords: 100 });
const translation = await translate({ text: "Hello world", targetLanguage: "Spanish" });
const extraction = await extractData({ text: invoiceText, entityTypes: ["date", "amount"] });
Images, embeddings, moderation
import { image, embed, semanticSearch, moderate } from "@/ai/openai/actions";
const picture = await image({ prompt: "a lighthouse at sunset, watercolor style" });
const vector = await embed({ input: "Next.js App Router" });
const matches = await semanticSearch("refund policy", knowledgeBaseRecords, { topK: 3 });
const check = await moderate("some user-submitted text");
Client Components — hooks
"use client";
import { useChat } from "@/ai/openai/hooks";
function ChatBox() {
const { sendMessage, data, isLoading, error } = useChat();
return (
<div>
<button onClick={() => sendMessage({ prompt: "Give me a haiku about the sea." })}>Ask</button>
{isLoading && <p>Loading…</p>}
{error && <p>{error.message}</p>}
{data && <p>{data.text}</p>}
</div>
);
}
useImageGeneration, useEmbedding, and useAskWithTools follow the same
{ data, error, isLoading, run } shape.
What you can build with this
This module is a complete foundation for any AI-powered feature in a Next.js app: a support chatbot (with or without tool calling to look things up), a "summarize this document" or "translate this page" button, an AI-assisted search box (embeddings + semantic search), automatic content moderation on user-generated text, structured data extraction from freeform text (invoices, forms, support tickets), or an image-generation feature for user avatars, thumbnails, or creative tools — all without writing any OpenAI SDK code, prompt-assembly logic, or rate-limiting/ moderation plumbing by hand.