Building AI features into web applications sounds simple until you try it. You need streaming responses so the user isn't staring at a spinner. You need to handle tool calls from the model, execute them server-side, and stream the results back. You need to manage conversation state. You need to support switching between OpenAI, Anthropic, and local models without rewriting your application layer.
Vercel's AI SDK handles all of that. I've used it on multiple production projects now, and it's genuinely the best DX for AI-powered web applications.
## The Core: Provider Abstraction
The SDK separates your application code from the model provider. You write your logic once. Switching from GPT-4o to Claude to a local Ollama model is changing one import and one string. For a deeper look, see [Mastra for more complex orchestration](/blog/mastra-ai-typescript-agent-framework).
```typescript
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
// Same API, different providers
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
prompt: 'Explain quantum computing',
});
// Switch provider, change nothing else
const result2 = await generateText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing',
});
```
This isn't just convenience. It's an architectural decision that lets you A/B test models, fall back to cheaper models for simple requests, and switch providers without touching your business logic. I've built systems where the model selection happens at runtime based on task complexity. The AI SDK makes that trivial.
## Streaming: Where It Actually Shines
The streaming implementation is the best part. For server-sent events (SSE) streaming with React, the SDK gives you both the server handler and the client hook.
Server side (Next.js Route Handler):
```typescript
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
messages,
tools: {
getWeather: {
description: 'Get current weather for a location',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
const weather = await fetchWeather(city);
return weather;
},
},
},
});
return result.toDataStreamResponse();
}
```
Client side (React):
```typescript
'use client';
import { useChat } from '@ai-sdk/react';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({ api: '/api/chat' });
return (
{messages.map((m) => (
);
}
```
That's a complete streaming chat with tool use. The `useChat` hook handles message state, streaming text display, loading states, and error handling. Tokens appear as they're generated. Tool calls execute server-side and the results stream back seamlessly.
Try building this from scratch with raw `EventSource` or `fetch` with `ReadableStream`. It's a week of work to handle all the edge cases the SDK handles out of the box.
## Structured Output with Zod
The SDK integrates Zod schemas for structured output, and the developer experience is excellent. You define a schema, the model output is constrained to match it, and you get a fully typed result.
```typescript
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const result = await generateObject({
model: anthropic('claude-sonnet-4-20250514'),
schema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
topics: z.array(z.string()),
summary: z.string(),
}),
prompt: 'Analyze this customer review: ...',
});
// result.object is fully typed
console.log(result.object.sentiment); // TypeScript knows this is the enum
console.log(result.object.confidence); // TypeScript knows this is a number
``` It is worth reading about [Claude's Agent SDK on the backend](/blog/claude-agent-sdk-production) alongside this.
`generateObject` returns a typed object, not a string you have to parse. `streamObject` does the same thing but streams partial objects as they're generated, which is wild for UIs that need to show structured data being built in real time.
## Tool Use That Doesn't Suck
Tools in the AI SDK are defined with Zod schemas and async execute functions. The framework handles the multi-turn loop where the model calls a tool, you execute it, and the model continues.
```typescript
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools: {
searchProducts: {
description: 'Search the product catalog',
parameters: z.object({
query: z.string(),
maxPrice: z.number().optional(),
category: z.string().optional(),
}),
execute: async ({ query, maxPrice, category }) => {
return await db.products.search({ query, maxPrice, category });
},
},
getOrderStatus: {
description: 'Check the status of an order',
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => {
return await db.orders.getStatus(orderId);
},
},
},
maxSteps: 5, // Allow up to 5 tool call rounds
messages,
});
```
The `maxSteps` parameter controls how many tool call rounds the model can do. Without it, you'd need your own loop. With it, the SDK runs the loop for you. The model calls tools, the SDK executes them, feeds results back, and continues until the model is done or the step limit is hit.
For production, you'll want to add error handling around tool execution, rate limiting on expensive tools, and logging of tool calls for observability. The SDK gives you hooks for all of that.
## Multi-Modal: Images and Files
```typescript
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'What is in this image?' },
{ type: 'image', image: new URL('https://example.com/photo.jpg') },
],
},
],
});
``` The related post on [deployment architecture](/blog/agent-deployment-patterns) goes further on this point.
Image inputs work across providers that support them. Same API shape whether you're using Claude's vision or GPT-4o. The SDK normalizes the provider differences so your multi-modal code is portable.
## The Middleware Layer
The SDK has a middleware system that lets you intercept and modify model interactions. This is where it gets powerful for production systems.
```typescript
import { wrapLanguageModel } from 'ai';
const guardedModel = wrapLanguageModel({
model: anthropic('claude-sonnet-4-20250514'),
middleware: {
transformParams: async ({ params }) => {
// Add system instructions, modify messages, inject context
return {
...params,
system: `${params.system}\n\nAlways be helpful and safe.`,
};
},
wrapGenerate: async ({ doGenerate, params }) => {
const start = Date.now();
const result = await doGenerate();
console.log(`Generation took ${Date.now() - start}ms`);
return result;
},
},
});
```
Logging, guardrails, token counting, caching, rate limiting. All of it fits cleanly into the middleware pattern. You compose concerns without polluting your business logic.
## What It Doesn't Do
The AI SDK is for web applications. It's not an agent framework. You won't find multi-agent orchestration, persistent memory systems, or autonomous planning loops here. That's not what it's for.
If you need autonomous agents, use Mastra (which builds on the AI SDK), LangGraph, or the Claude Agent SDK. Use the Vercel AI SDK for the web application layer that wraps those agents.
It also doesn't do evaluation or observability out of the box. You'll want to integrate with LangSmith, Braintrust, or your own logging for production monitoring.
## The Verdict
The Vercel AI SDK is the best tool for adding AI features to TypeScript web applications. Not the best agent framework. Not the best model evaluation tool. The best web application integration layer.
If you're building a Next.js app that needs streaming chat, structured AI outputs, tool-augmented responses, or multi-modal interactions, the AI SDK gives you the cleanest path from zero to production.
Every TypeScript project I build that touches an LLM starts with the AI SDK. Then I layer whatever agent logic I need on top. The foundation is always this.
{m.role}: {m.content}
))}