Vercel AI SDK vs LangChain vs Raw API Calls
TL;DR
Vercel AI SDK if you're building a UI with streaming. Raw API calls if you know exactly what you want. LangChain only for complex multi-step agentic workflows. In 2026, most developers are abandoning LangChain for simpler solutions — the abstraction cost is high and the framework changes constantly. For most production apps: Vercel AI SDK handles 80% of use cases with a great DX, and for the other 20% (complex agent orchestration), vendor-native SDKs or the Claude/OpenAI Agents APIs are better than LangChain.
Key Takeaways
- Vercel AI SDK: best DX for streaming UIs,
useChat/useCompletionhooks, works with 20+ providers, TypeScript-first - LangChain: powerful but heavy, frequently breaking changes, better alternatives exist in 2026
- Raw API calls: maximum control, zero dependency, best for simple use cases or custom integrations
- LangChain alternatives: LangGraph (just the graph part), Mastra, or vendor Agents SDKs (Anthropic, OpenAI)
- When to use each: UI streaming → Vercel AI SDK, simple completion → raw, complex agents → vendor SDK or Mastra
The Problem With Abstraction Layers
Before comparing, understand the tradeoff:
More abstraction:
+ Less code to write
+ Handles streaming, retries, format normalization
- Harder to debug
- Dependency on framework churn
- Limited access to provider-specific features
- Version incompatibilities
Less abstraction:
+ Full control
+ Stable APIs (provider APIs change slowly)
+ Easier to debug
- More boilerplate
- Manual streaming handling
- No provider switching
The question isn't "which is best" — it's where your use case sits on this spectrum.
Vercel AI SDK: The Right Level of Abstraction
Vercel AI SDK hits the sweet spot: it handles the hard parts (streaming, provider normalization, React integration) without hiding what's happening.
Core: streamText + React Hooks
// app/api/chat/route.ts — Server-side streaming:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages,
system: 'You are a helpful assistant.',
onFinish: async ({ text, usage }) => {
// Called when stream completes — save to DB, log usage:
await db.conversation.create({
data: {
content: text,
inputTokens: usage.promptTokens,
outputTokens: usage.completionTokens,
},
});
},
});
return result.toDataStreamResponse();
}
// components/Chat.tsx — Client-side with useChat hook:
'use client';
import { useChat } from 'ai/react';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
onError: (error) => console.error('Chat error:', error),
onFinish: (message) => console.log('Done:', message),
});
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((m) => (
<div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}>
<div className={`inline-block p-3 rounded-lg max-w-[80%] ${
m.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}>
{m.content}
</div>
</div>
))}
{isLoading && <div className="text-gray-400">Thinking...</div>}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
className="flex-1 border rounded p-2"
/>
<button type="submit" disabled={isLoading} className="px-4 py-2 bg-blue-500 text-white rounded">
Send
</button>
</form>
</div>
);
}
Provider Switching — The Killer Feature
// Change one line to switch providers:
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { groq } from '@ai-sdk/groq';
import { createOpenAI } from '@ai-sdk/openai'; // For custom endpoints
// Groq (uses createOpenAI with custom baseURL):
const groqProvider = createOpenAI({
apiKey: process.env.GROQ_API_KEY,
baseURL: 'https://api.groq.com/openai/v1',
});
// A/B test models:
const model = Math.random() > 0.5
? openai('gpt-4o')
: anthropic('claude-3-5-sonnet-20241022');
const result = await streamText({ model, messages });
Structured Output with Zod
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const ProductSchema = z.object({
name: z.string(),
price: z.number().min(0),
category: z.enum(['electronics', 'clothing', 'food', 'other']),
inStock: z.boolean(),
tags: z.array(z.string()).max(5),
});
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: ProductSchema,
prompt: 'Extract product info from: "Nike Air Max 90 sneakers, $120, currently available"',
});
// object is fully typed as z.infer<typeof ProductSchema>
// No JSON.parse, no validation needed
Tool Calling
import { streamText, tool } from 'ai';
import { z } from 'zod';
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: {
getWeather: tool({
description: 'Get current weather for a location',
parameters: z.object({
location: z.string().describe('City name'),
unit: z.enum(['celsius', 'fahrenheit']).optional(),
}),
execute: async ({ location, unit = 'celsius' }) => {
// Your actual weather API call:
const data = await fetchWeather(location, unit);
return data;
},
}),
searchDatabase: tool({
description: 'Search the product database',
parameters: z.object({
query: z.string(),
limit: z.number().default(5),
}),
execute: async ({ query, limit }) => {
return await db.products.findMany({
where: { name: { contains: query } },
take: limit,
});
},
}),
},
maxSteps: 5, // Allow up to 5 tool call rounds
});
LangChain: Powerful but Complicated
LangChain (Python and JS) was the dominant LLM framework 2022-2024. In 2026, most teams have moved away from it or use only small parts.
The LangChain DX Problem
// What LangChain code often looks like:
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
const model = new ChatOpenAI({ modelName: 'gpt-4o', temperature: 0.7 });
const parser = new StringOutputParser();
const promptTemplate = ChatPromptTemplate.fromMessages([
['system', 'You are a helpful assistant.'],
['human', '{input}'],
]);
const chain = RunnableSequence.from([promptTemplate, model, parser]);
const result = await chain.invoke({ input: 'Hello' });
// vs Vercel AI SDK:
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Hello',
system: 'You are a helpful assistant.',
});
The LangChain version has 3x the imports, 3x the code, and is no more capable for this use case.
Where LangChain Still Makes Sense (LangGraph)
LangGraph (a LangChain subproject) is legitimately good for multi-agent graph workflows:
// LangGraph: complex state machines with branching, loops, parallelism
import { StateGraph, Annotation, END, START } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
const StateAnnotation = Annotation.Root({
messages: Annotation<string[]>({
reducer: (x, y) => x.concat(y),
}),
nextAction: Annotation<string>(),
});
const graph = new StateGraph(StateAnnotation)
.addNode('researcher', async (state) => {
// Research agent
const response = await researcherModel.invoke(state.messages);
return { messages: [response.content], nextAction: 'writer' };
})
.addNode('writer', async (state) => {
// Writer agent
const response = await writerModel.invoke(state.messages);
return { messages: [response.content], nextAction: 'END' };
})
.addEdge(START, 'researcher')
.addConditionalEdges('researcher', (state) =>
state.nextAction === 'writer' ? 'writer' : END
)
.addEdge('writer', END)
.compile();
Use LangGraph when you specifically need: graph-based state machines, human-in-the-loop, branching agent logic with cycles.
Raw API Calls: Maximum Control
For simple use cases, nothing beats direct API calls.
// Pure fetch — no dependencies:
async function chatCompletion(messages: { role: string; content: string }[]) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o',
messages,
temperature: 0.7,
max_tokens: 1024,
}),
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${await response.text()}`);
}
return response.json();
}
// Raw streaming with fetch:
async function* streamCompletion(prompt: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
stream: true,
}),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter((l) => l.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices[0]?.delta?.content;
if (delta) yield delta;
} catch {}
}
}
}
Raw API calls make sense when:
- You have one specific use case that won't change
- You're writing a CLI tool or script
- You want zero runtime dependencies
- You're integrating with Cloudflare Workers where bundle size matters
Comparison: When to Use What
| Scenario | Recommendation | Why |
|---|---|---|
| Chat UI in Next.js | Vercel AI SDK | useChat handles SSE, state, retries |
| Single API call in a script | Raw fetch | Zero dependencies |
| Switching between 3+ providers | Vercel AI SDK | One abstraction, all providers |
| Structured data extraction | Vercel AI SDK (generateObject) | Zod integration |
| Multi-step research agent | Mastra or Agents SDK | Better DX than LangChain |
| RAG pipeline | Vercel AI SDK + vector DB | embed() + retrieval |
| Complex graph workflows | LangGraph | That's what it's designed for |
| Cloudflare Workers edge | Raw API or Workers AI | Bundle size matters |
| Enterprise audit logging | Raw API | Full visibility into every request |
LangChain Alternatives in 2026
If you need more than Vercel AI SDK but less than LangChain's complexity:
// Mastra — TypeScript-first agent framework:
import { Mastra, createTool } from '@mastra/core';
import { openai } from '@ai-sdk/openai';
const weatherTool = createTool({
id: 'get-weather',
description: 'Get weather for a location',
inputSchema: z.object({ location: z.string() }),
execute: async ({ context }) => {
return await fetchWeather(context.location);
},
});
const researchAgent = mastra.getAgent('researcher');
const result = await researchAgent.generate('What is the weather in Tokyo?', {
tools: { weatherTool },
});
// OpenAI Agents SDK (for OpenAI-specific agent workflows):
import { Agent, Runner, handoff } from 'openai/lib/agents';
const triage = new Agent({
name: 'Triage Agent',
instructions: 'Route to the right specialist.',
tools: [
handoff({ agent: salesAgent }),
handoff({ agent: supportAgent }),
],
});
const result = await Runner.run(triage, 'I want to cancel my subscription');
Compare AI SDKs and frameworks at APIScout.