Setting up Langchain and Langraph in NextJS 15 on Vercel with Google AI
This guide shows how to configure Langchain and Langraph in a Next.js 15 project for Vercel deployment using Google's Gemini models for building AI agents with TypeScript.
Mental Model: Building Blocks Architecture
Think of this setup like assembling a modern web API with specialized middleware. Next.js 15 provides the server framework, Langchain acts as your AI orchestration layer (like Express middleware for LLMs), and Langraph adds stateful workflow capabilities (similar to Redux Saga for AI workflows). Google's Gemini models serve as the intelligence layer, while Vercel's platform handles serverless deployment, automatically scaling your AI endpoints like Google Cloud Functions but with better DX.
Basic Setup: Project Initialization
1. Create Next.js 15 Project
npx create-next-app@latest my-ai-agent --typescript --tailwind --app --no-src-dir
cd my-ai-agent
Creates a new Next.js 15 project with TypeScript, Tailwind CSS v4, App Router, and files in the root directory.
2. Install Core Dependencies
npm install langchain @langchain/core @langchain/langgraph @langchain/google-genai
npm install @langchain/community ai @ai-sdk/google
npm install daisyui@latest
Installs Langchain core libraries, Langraph for workflows, Google Gemini integration, community tools, Vercel AI SDK, and DaisyUI v5 for UI components.
3. TypeScript Configuration
Next.js 15 automatically configures TypeScript when you create a project with the --typescript
flag. The default tsconfig.json
is already optimized for Langchain usage, no changes needed.
4. Environment Variables
# .env.local
GOOGLE_API_KEY=your-google-ai-api-key
LANGCHAIN_TRACING_V2=true # Optional: Enables LangSmith tracing
LANGCHAIN_API_KEY=your-langsmith-key # Optional: For LangSmith debugging
LANGCHAIN_CALLBACKS_BACKGROUND=false # Required: For Vercel serverless
Important: LANGCHAIN_CALLBACKS_BACKGROUND Explanation
When set to true
(default), Langchain runs callbacks asynchronously in the background after sending the response. This works fine on traditional servers but causes issues in serverless environments like Vercel because:
- Serverless functions terminate immediately after returning a response
- Background callbacks get killed before completing, causing hanging requests or timeouts
- Vercel may report errors like "Function execution took too long" even for simple requests
Setting it to false
ensures all callbacks complete synchronously before the response is sent, preventing serverless function termination issues. This is mandatory for Vercel, AWS Lambda, and similar platforms.
5. Update globals.css for Tailwind v4 and DaisyUI v5
/* app/globals.css */
@import "tailwindcss";
@plugin "daisyui";
Imports Tailwind CSS v4 and loads DaisyUI v5 as a plugin using the new CSS-based configuration.
6. Basic Langchain API Route with Google Gemini
// app/api/chat/route.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const maxDuration = 300; // 5 minutes for Pro plan
export async function POST(req: Request) {
try {
const { message } = await req.json();
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-pro',
temperature: 0.7,
streaming: false,
maxOutputTokens: 2048,
});
const response = await model.invoke([
new HumanMessage(message)
]);
return NextResponse.json({
content: response.content
});
} catch (error) {
console.error('Chat API error:', error);
return NextResponse.json(
{ error: 'Failed to process chat' },
{ status: 500 }
);
}
}
Creates a POST endpoint that accepts messages, processes them through Gemini Pro, and returns AI responses with Node.js runtime for full Langchain support.
Advanced Example: Langraph Agent with Streaming
1. Install Additional Dependencies
npm install @langchain/google-genai @langchain/langgraph zod
npm install @vercel/kv uuid @google/generative-ai
npm install @tanstack/react-query
Adds dependencies for Google AI embeddings, Langraph agents, schema validation with Zod, Vercel KV for caching, and TanStack Query for data fetching.
2. Create Agent with Tools
// lib/agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { Calculator } from '@langchain/community/tools/calculator';
import { WebBrowser } from '@langchain/community/tools/webbrowser';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { HumanMessage } from '@langchain/core/messages';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
export function createAgent() {
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0,
streaming: true,
maxOutputTokens: 8192,
});
const embeddings = new GoogleGenerativeAIEmbeddings({
modelName: "embedding-001",
});
const tools = [
new Calculator(),
new WebBrowser({ model, embeddings }),
];
return createReactAgent({
llm: model,
tools,
});
}
Sets up a ReAct agent with Gemini Flash for fast responses, equipped with Calculator and WebBrowser tools for enhanced capabilities.
3. Streaming API Route with Langraph
// app/api/agent/route.ts
import { createAgent } from '@/lib/agent';
import { HumanMessage } from '@langchain/core/messages';
export const runtime = 'nodejs';
export const maxDuration = 300;
export async function POST(req: Request) {
const { message } = await req.json();
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const agent = createAgent();
// Start streaming in background
(async () => {
try {
const eventStream = await agent.stream({
messages: [new HumanMessage(message)],
});
for await (const event of eventStream) {
if (event.agent?.messages?.[0]?.content) {
await writer.write(
encoder.encode(`data: ${JSON.stringify({
content: event.agent.messages[0].content
})}\n\n`)
);
}
}
} catch (error) {
console.error('Streaming error:', error);
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Implements server-sent events (SSE) streaming to send agent responses in real-time as they're generated, improving perceived performance.
4. Client Component with Streaming
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
Sets up TanStack Query provider with optimized defaults for AI applications.
// app/layout.tsx
import './globals.css';
import Providers from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Wraps the application with the TanStack Query provider for global access.
// components/ChatInterface.tsx
'use client';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
export default function ChatInterface() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const streamChat = useMutation({
mutationFn: async (userMessage: string) => {
setResponse('');
const res = await fetch('/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage }),
});
if (!res.ok) throw new Error('Failed to send message');
const reader = res.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
setResponse(prev => prev + data.content);
} catch {}
}
}
}
return response;
},
onError: (error) => {
console.error('Chat error:', error);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (message.trim()) {
streamChat.mutate(message);
}
};
return (
<div className="card w-full bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">Google Gemini AI Agent</h2>
<form onSubmit={handleSubmit}>
<div className="form-control">
<textarea
className="textarea textarea-bordered"
placeholder="Ask your question..."
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={3}
disabled={streamChat.isPending}
/>
</div>
<div className="card-actions justify-end mt-4">
<button
type="submit"
className="btn btn-primary"
disabled={streamChat.isPending || !message.trim()}
>
{streamChat.isPending ? (
<>
<span className="loading loading-spinner"></span>
Thinking...
</>
) : 'Send'}
</button>
</div>
</form>
{streamChat.isError && (
<div className="alert alert-error mt-4">
<span>Failed to send message. Please try again.</span>
</div>
)}
{response && (
<div className="alert mt-4">
<span>{response}</span>
</div>
)}
</div>
</div>
);
}
React component using TanStack Query's useMutation hook for managing streaming API calls with built-in loading and error states.
5. Prompt Chaining Implementation with Gemini
// lib/chains/prompt-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { PromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { NextResponse } from 'next/server';
export function createPromptChain() {
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0
});
// Step 1: Extract specifications
const extractPrompt = PromptTemplate.fromTemplate(
"Extract technical specifications from: {input}"
);
// Step 2: Transform to JSON
const transformPrompt = PromptTemplate.fromTemplate(
"Convert to JSON with 'cpu', 'memory', 'storage' keys: {specs}"
);
// Create the chain
const chain = RunnableSequence.from([
extractPrompt,
model,
new StringOutputParser(),
{ specs: (prev: string) => prev },
transformPrompt,
model,
new StringOutputParser(),
]);
return chain;
}
// Usage in API route
export async function POST(req: Request) {
const { text } = await req.json();
const chain = createPromptChain();
const result = await chain.invoke({ input: text });
return NextResponse.json({ result });
}
Implements the Prompt Chaining pattern by breaking complex tasks into sequential steps - first extracting data, then transforming it to JSON format.
6. Context Engineering with Google Gemini
// lib/context-engineer.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
import { Document } from '@langchain/core/documents';
interface ContextConfig {
systemPrompt: string;
retrievedDocs?: Document[];
userHistory?: string[];
environmentContext?: Record<string, any>;
}
export class ContextEngineer {
private model: ChatGoogleGenerativeAI;
constructor() {
this.model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-pro',
temperature: 0.3,
maxOutputTokens: 8192,
});
}
async processWithContext(
userQuery: string,
config: ContextConfig
) {
const messages = [];
// Add system context
messages.push(new SystemMessage(config.systemPrompt));
// Add retrieved documents as context
if (config.retrievedDocs?.length) {
const docContext = config.retrievedDocs
.map(doc => doc.pageContent)
.join('\n\n');
messages.push(new SystemMessage(
`Relevant context:\n${docContext}`
));
}
// Add user history
if (config.userHistory?.length) {
messages.push(new SystemMessage(
`Previous conversation:\n${config.userHistory.join('\n')}`
));
}
// Add environment context
if (config.environmentContext) {
messages.push(new SystemMessage(
`Environment: ${JSON.stringify(config.environmentContext)}`
));
}
// Add the actual user query
messages.push(new HumanMessage(userQuery));
return await this.model.invoke(messages);
}
}
Builds a comprehensive context layer by combining system prompts, retrieved documents, conversation history, and environment data before processing queries.
7. Vercel Deployment Configuration
// vercel.json
{
"functions": {
"app/api/agent/route.ts": {
"maxDuration": 300
},
"app/api/chat/route.ts": {
"maxDuration": 60
}
}
}
Configures Vercel to allow longer execution times for AI operations. Environment variables are automatically handled by Next.js and Vercel.
8. Main Page Integration
// app/page.tsx
import ChatInterface from '@/components/ChatInterface';
export default function Home() {
return (
<main className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<div className="text-center mb-8">
<h1 className="text-5xl font-bold">AI Agent Platform</h1>
<p className="py-6">Powered by Google Gemini & Langraph</p>
</div>
<div className="flex justify-center">
<div className="w-full max-w-2xl">
<ChatInterface />
</div>
</div>
</div>
</main>
);
}
Main landing page that centers the chat interface component with responsive layout and DaisyUI styling.
9. Advanced Langraph Workflow
// lib/workflows/stateful-workflow.ts
import { StateGraph, END } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage } from '@langchain/core/messages';
interface WorkflowState {
messages: BaseMessage[];
currentStep: string;
metadata: Record<string, any>;
}
export function createStatefulWorkflow() {
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
});
const workflow = new StateGraph<WorkflowState>({
channels: {
messages: {
value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
default: () => [],
},
currentStep: {
value: (x: string, y: string) => y || x,
default: () => 'start',
},
metadata: {
value: (x: Record<string, any>, y: Record<string, any>) => ({...x, ...y}),
default: () => ({}),
},
},
});
// Define nodes
workflow.addNode('analyze', async (state) => {
const response = await model.invoke(state.messages);
return {
messages: [response],
currentStep: 'process',
};
});
workflow.addNode('process', async (state) => {
// Process the analysis
return {
currentStep: 'complete',
metadata: { processed: true },
};
});
// Define edges
workflow.addEdge('analyze', 'process');
workflow.addEdge('process', END);
workflow.setEntryPoint('analyze');
return workflow.compile();
}
Creates a stateful workflow using Langraph's StateGraph that manages message history, tracks execution steps, and processes data through defined nodes and edges.
10. Integrating Stateful Workflow with Frontend
// app/api/workflow/route.ts
import { createStatefulWorkflow } from '@/lib/workflows/stateful-workflow';
import { HumanMessage } from '@langchain/core/messages';
export const runtime = 'nodejs';
export const maxDuration = 300;
export async function POST(req: Request) {
const { message, sessionId } = await req.json();
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const workflow = createStatefulWorkflow();
(async () => {
try {
// Stream workflow events
const events = await workflow.stream({
messages: [new HumanMessage(message)],
currentStep: 'start',
metadata: { sessionId },
});
for await (const event of events) {
// Send step updates to frontend
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'step',
step: event.currentStep || 'processing',
content: event.messages?.[event.messages.length - 1]?.content,
metadata: event.metadata
})}\n\n`)
);
}
await writer.write(
encoder.encode(`data: ${JSON.stringify({ type: 'complete' })}\n\n`)
);
} catch (error) {
await writer.write(
encoder.encode(`data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`)
);
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
API route that streams workflow execution events including step transitions and intermediate results to the frontend.
// hooks/useWorkflowStream.ts
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
interface WorkflowStep {
name: string;
status: 'pending' | 'active' | 'complete';
output?: string;
}
interface WorkflowData {
message: string;
sessionId: string;
}
export function useWorkflowStream() {
const [steps, setSteps] = useState<WorkflowStep[]>([
{ name: 'analyze', status: 'pending' },
{ name: 'process', status: 'pending' },
{ name: 'complete', status: 'pending' }
]);
const mutation = useMutation({
mutationFn: async (data: WorkflowData) => {
// Reset steps
setSteps(steps.map(s => ({ ...s, status: 'pending', output: undefined })));
const res = await fetch('/api/workflow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Workflow failed');
const reader = res.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'step') {
setSteps(prev => prev.map(step => {
if (step.name === data.step) {
return { ...step, status: 'active', output: data.content };
} else if (prev.findIndex(s => s.name === data.step) >
prev.findIndex(s => s.name === step.name)) {
return { ...step, status: 'complete' };
}
return step;
}));
} else if (data.type === 'complete') {
setSteps(prev => prev.map(s => ({ ...s, status: 'complete' })));
} else if (data.type === 'error') {
throw new Error(data.error);
}
} catch (error) {
if (error instanceof SyntaxError) continue;
throw error;
}
}
}
}
return { success: true };
},
onError: (error) => {
console.error('Workflow error:', error);
setSteps(prev => prev.map(s => ({ ...s, status: 'pending' })));
},
});
return {
steps,
runWorkflow: mutation.mutate,
isLoading: mutation.isPending,
isError: mutation.isError,
error: mutation.error,
reset: mutation.reset,
};
}
Custom hook using TanStack Query to manage workflow streaming with automatic state updates and error handling.
// components/WorkflowInterface.tsx
'use client';
import { useState } from 'react';
import { useWorkflowStream } from '@/hooks/useWorkflowStream';
export default function WorkflowInterface() {
const [message, setMessage] = useState('');
const { steps, runWorkflow, isLoading, isError, error, reset } = useWorkflowStream();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (message.trim()) {
runWorkflow({
message,
sessionId: crypto.randomUUID()
});
}
};
return (
<div className="card w-full bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">Workflow Execution</h2>
<form onSubmit={handleSubmit}>
<div className="form-control">
<input
type="text"
className="input input-bordered"
placeholder="Enter your request..."
value={message}
onChange={(e) => setMessage(e.target.value)}
disabled={isLoading}
/>
</div>
<div className="flex gap-2 mt-4">
<button
type="submit"
className="btn btn-primary"
disabled={isLoading || !message.trim()}
>
{isLoading ? (
<>
<span className="loading loading-spinner"></span>
Running Workflow...
</>
) : 'Execute Workflow'}
</button>
{isError && (
<button type="button" className="btn btn-outline" onClick={() => reset()}>
Reset
</button>
)}
</div>
</form>
{isError && (
<div className="alert alert-error mt-4">
<span>Error: {error?.message || 'Workflow failed'}</span>
</div>
)}
{/* Workflow Steps Visualization */}
<div className="mt-6">
<ul className="steps steps-vertical">
{steps.map((step, idx) => (
<li
key={idx}
className={`step ${
step.status === 'complete' ? 'step-success' :
step.status === 'active' ? 'step-primary' : ''
}`}
>
<div className="text-left ml-4">
<div className="font-semibold capitalize">{step.name}</div>
{step.output && (
<div className="text-sm opacity-70 mt-1">
{step.output.substring(0, 100)}
{step.output.length > 100 && '...'}
</div>
)}
</div>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
Frontend component using the custom workflow hook with TanStack Query for clean state management and error handling.
// app/workflow/page.tsx
import WorkflowInterface from '@/components/WorkflowInterface';
export default function WorkflowPage() {
return (
<main className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold">AI Workflow Executor</h1>
<p className="py-4">Watch your AI agent work through complex tasks step by step</p>
</div>
<div className="flex justify-center">
<div className="w-full max-w-3xl">
<WorkflowInterface />
</div>
</div>
</div>
</main>
);
}
Dedicated page for the workflow interface that provides a clean layout for monitoring multi-step AI agent operations.
Conclusion
This setup provides a production-ready foundation for building AI agents with Langchain and Langraph on Next.js 15 and Vercel using Google's Gemini models. The configuration leverages Gemini's superior performance and cost-effectiveness, while maintaining full streaming support and advanced agent capabilities. Key aspects include proper Google AI integration, Tailwind v4's simplified configuration with DaisyUI v5, and comprehensive prompt chaining implementations that follow the patterns from the Agentic Design Patterns document.