Full-Stack AI Integration¶
The bar: you can wire an AI backend to a real web frontend — stream tokens into a chat UI, keep types honest from the database to the browser, take file uploads, and ship the whole thing behind auth.
A model behind an API is a demo. Nobody but you ever sees it. The job — the thing that separates an engineer from someone who watched a tutorial — is connecting that backend to a frontend a stranger can use without reading the README. This module closes the loop: a FastAPI service that streams tokens, and a Next.js interface that renders them as they land.
Everything here is about the seam between two systems. That seam is where most self-taught engineers lose the plot: the model works, the page works, but the wire between them drops data, breaks types, or freezes the browser. Get the seam right and you have a product.
21.1 Next.js AI Patterns¶
Objective: build AI-powered Next.js applications using the framework's server-side rendering and streaming primitives.
The Next.js App Router gives you four tools for AI work, and they divide cleanly by job. Server Components run on the server and never ship their code to the browser — perfect for holding an API key or calling your model without leaking secrets. Streaming with Suspense lets you send the page shell instantly and flush the slow AI part in as it generates, so the user sees something in 200ms instead of staring at a spinner for ten seconds. Server Actions handle mutations (submit a prompt, save a message) without you hand-writing a fetch endpoint. Route handlers are your actual HTTP APIs when you need a raw endpoint, like an SSE stream.
Learn all four here — they are real, current, marketable skills. But know the ground you'll stand on at work. The Vets Who Code production app runs the Pages Router, not the App Router. The concepts translate directly: an App Router route handler becomes a handler in src/pages/api/; server-side data fetching in a Server Component becomes getServerSideProps or getStaticProps. Same idea — code that runs on the server, secrets that never reach the client — different filenames.
The classic mistake is calling your model from a client component with the API key baked in. It works on localhost, then a stranger opens dev tools and reads your key. Server-side is not a preference here; it is the only correct answer.
- Drill: build a Server Component that fetches from your FastAPI backend; on VWC's stack, port the same fetch to
getServerSideProps - Drill: wrap a slow AI section in
<Suspense>and confirm the shell paints before the content - Drill: write a Server Action for a mutation; then write the Pages Router equivalent as an
src/pages/api/handler - Drill: stand up a route handler that returns a streaming response
21.2 Streaming Chat UI¶
Objective: build real-time chat interfaces that render model output as it arrives.
Streaming is what makes AI feel alive. The backend sends Server-Sent Events (SSE) — a long-lived HTTP response that dribbles out chunks — and React consumes them, appending each token as it arrives. The user watches the answer type itself out. Do it in one shot instead and the same response feels broken, because the user waits in silence for the whole thing.
You consume SSE in React by reading the response body stream and decoding chunks as they come, updating state per token. Then you render that growing string as Markdown with syntax highlighting, because models emit Markdown and code blocks constantly. Keep a message history array so the conversation has context, and use optimistic updates so the user's own message appears the instant they hit send — before the server confirms anything.
The gotcha is React re-rendering on every single token. A naive implementation re-parses the entire Markdown tree hundreds of times and the tab heats up. Batch your updates, memoize the rendered history, and only re-parse the message that's actively streaming.
- Drill: consume an SSE stream in a React component, decoding the body reader chunk by chunk
- Drill: render tokens token-by-token into a growing message bubble
- Drill: render assistant messages as Markdown with syntax-highlighted code blocks
- Drill: keep a
messageshistory array and apply an optimistic update when the user sends
21.3 Type-Safe Integration¶
Objective: hold type safety across the whole stack, from Python backend to TypeScript frontend.
Your backend speaks Python with Pydantic models. Your frontend speaks TypeScript. Left unmanaged, the two drift: you rename a field in FastAPI, the frontend keeps reading the old name, and nothing complains until a user hits it. The fix is to generate, not hand-write. FastAPI emits an OpenAPI schema for free; run a generator against it and you get a typed client, so a backend rename becomes a frontend compile error instead of a production bug.
On the browser side, Zod validates data at runtime and mirrors your Pydantic models — same shape, same constraints, on both ends of the wire. Pydantic guards the Python boundary; Zod guards the TypeScript one. Shared type definitions and generated clients give you end-to-end type safety: change the contract in one place and every consumer that's now wrong lights up red.
The trap is trusting network data because TypeScript "typed" it. A type annotation is a compile-time promise the network can break. Parse untrusted responses with Zod at runtime — that's the difference between a type and a guarantee.
- Drill: generate a typed client from your FastAPI OpenAPI schema
- Drill: write a Zod schema that matches a Pydantic model field-for-field
- Drill:
parse()an API response through Zod at the boundary and handle the failure path - Drill: share one set of type definitions across frontend and backend consumers
21.4 File Upload¶
Objective: handle multimodal file inputs — images, documents, and audio.
Multimodal means the user hands you more than text. A solid upload surface has four parts. Drag-and-drop handling for files dropped onto the page. Image preview so the user sees the thumbnail before sending, not a filename they have to trust. Document upload with a progress indicator, because a 40MB PDF over hotel wifi with no progress bar looks frozen and the user will refresh and lose it. And an audio recording UI when you want voice input straight from the mic.
Progress is the piece beginners skip, and it's the one that matters most. Any upload past a couple megabytes needs a visible progress state or the user assumes the app died. Wire the browser's upload progress events to a real bar.
Validate on the server, not just the client. Client-side file-type checks are a nicety for the honest user; a hostile one bypasses them in seconds. The frontend gate is UX, the backend gate is security.
- Drill: implement drag-and-drop file handling with drop-zone states
- Drill: show an image preview from a selected or dropped file
- Drill: upload a document with a live progress bar driven by upload events
- Drill: build an audio recording control that captures from the mic and uploads the clip
21.5 Hands-On Lab: Complete Application¶
Objective: build and deploy a full-stack, multimodal AI application end to end.
What you ship
A working AI application that a stranger can log into and use:
- A multimodal chat interface that streams responses token-by-token
- Document upload and Q&A — upload a file, ask questions against it
- Authentication with NextAuth.js gating the app so it's not open to the world
- Deployed for real: the Next.js frontend on Vercel, the FastAPI backend on Cloud Run
This is the capstone of the build phase. It has a frontend, a backend, auth, file handling, and streaming — a full product, not a notebook. Ship it and you have something to point a hiring manager at.
On the VWC codebase
This module teaches the App Router because it's a strong, current skill worth having. The Vets Who Code production app currently runs the Pages Router — API routes live in src/pages/api/, and server-side data comes from getServerSideProps / getStaticProps rather than Server Components. When you bring these patterns to the real repo, keep the concepts and translate the filenames. The type-safety discipline from 21.3 carries over cleanly: the app is TypeScript end to end, with Prisma over Postgres holding the backend contract.