Skip to content

Repository files navigation

nextjs-ai-app

Streaming chat using the OpenAI API.

Live: https://nextjs-ai-app-sand.vercel.app

Stack

Next.js 16 (App Router, Turbopack) · React 19 · Tailwind CSS v4 · OpenAI SDK v6 (gpt-4o-mini) · Web Streams API · TypeScript 5 · Vercel.

Architecture

app/
├── api/chat/route.ts            POST /api/chat (streaming)
├── chat/
│   ├── _components/             Co-located UI
│   │   ├── chat-header.tsx
│   │   ├── chat-input.tsx
│   │   ├── message-bubble.tsx
│   │   └── message-list.tsx
│   ├── _types.ts
│   └── page.tsx                 Orchestrator
├── layout.tsx
├── page.tsx
└── globals.css

Flow: ChatInput fires onSubmit(content)page.tsx appends the user message and POSTs the history to /api/chat → server calls OpenAI with stream: true and pipes a ReadableStream back → client reads chunks via getReader(), decodes with TextDecoder, and appends to the last bubble.

Run locally

cp .env.example .env.local        # add OPENAI_API_KEY
npm install
npm run dev

Open http://localhost:3000/chat.

Env vars

Var Description
OPENAI_API_KEY OpenAI API key

Architecture Decisions

ADRs in Michael Nygard format.

ADR 0001: App Router

Context: Pages Router is in maintenance mode in Next.js 16.

Decision: App Router with folder-based routing. Endpoints in app/api/chat/route.ts. Components co-located under _components/.

Consequences: Aligned with current conventions. Server Components shrink the client bundle. More file conventions to remember.

ADR 0002: OpenAI SDK directly (not Vercel AI SDK)

Context: Vercel AI SDK abstracts streaming and state via useChat.

Decision: Use the openai SDK directly. Manage streams manually with ReadableStream and getReader().

Consequences: More code, but explicit handling of Web Streams, async iterators, and back-pressure. Swapping providers means changing one SDK.

ADR 0003: Plain text streaming (not SSE)

Context: SSE (text/event-stream) makes sense for multi-event channels or auto-reconnect.

Decision: text/plain with raw token chunks.

Consequences: Less framing on both ends. Errors signaled via HTTP status before the stream or .read() rejection mid-stream. Move to SSE when tool calls or multiple channels are needed.

ADR 0004: Functional setMessages

Context: Multiple setState calls inside async code; closures capture stale state.

Decision: Use setMessages((prev) => ...) whenever the new state depends on the previous one.

Consequences: No stale-closure bug. Slightly more verbose.

ADR 0005: Assistant bubble created on the first chunk

Context: Either push an empty bubble before the fetch or wait until the first chunk lands.

Decision: Wait. The typing indicator covers the gap; the bubble appears already populated on the first token.

Consequences: No empty-bubble flash. Read loop needs a firstChunk flag. Error handling distinguishes pre-stream (append) from mid-stream (replace) failures.

ADR 0006: Co-located components

Context: Component placement: global folder, route-local, or inline.

Decision: _components/ next to the route. Underscore opts out of routing.

Consequences: Components live with the route that consumes them. Promote when actually reused; no premature abstraction.

Deploy

Auto-deploy on push to main. OPENAI_API_KEY set in Vercel project settings (Production + Preview).

  • Runtime: Node (OpenAI SDK isn't Edge-compatible)
  • Cold start: ~1-3s on first request
  • Vercel Hobby tier (free)

About

🤖 Streaming chat with OpenAI gpt-4o-mini on Next.js 16 App Router, Web Streams, and Tailwind v4.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages