Skip to main content
Learn how to integrate LlamaIndex.TS with Next.js for building production-ready AI applications.

Overview

LlamaIndex.TS works seamlessly with Next.js in multiple runtime environments:
  • Node.js Runtime - Full feature support with server actions and API routes
  • Edge Runtime - Optimized for Vercel Edge Functions
  • Server Actions - Direct LlamaIndex operations in React Server Components
  • API Routes - RESTful endpoints for LlamaIndex functionality

Next.js Configuration

Use the withLlamaIndex wrapper for proper bundling:
next.config.mjs
import withLlamaIndex from "llamaindex/next";

const nextConfig = {
  // Your Next.js config
};

export default withLlamaIndex(nextConfig);
This ensures:
  • Proper webpack configuration
  • Correct polyfills for edge environments
  • Optimized bundling for LlamaIndex packages

Server Actions Example

Use LlamaIndex directly in server actions:
src/actions/openai.ts
"use server";

import { HuggingFaceEmbedding } from "@llamaindex/huggingface";
import { OpenAI, OpenAIAgent } from "@llamaindex/openai";
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import { Settings, VectorStoreIndex } from "llamaindex";

// Configure settings
Settings.llm = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  model: "gpt-4o",
});

Settings.embedModel = new HuggingFaceEmbedding({
  modelType: "BAAI/bge-small-en-v1.5",
});

// Add callback listeners
Settings.callbackManager.on("llm-tool-call", (event) => {
  console.log(event.detail);
});

Settings.callbackManager.on("llm-tool-result", (event) => {
  console.log(event.detail);
});

export async function queryDocuments(query: string) {
  try {
    // Load documents
    const reader = new SimpleDirectoryReader();
    const documents = await reader.loadData("./data");
    
    // Create index
    const index = await VectorStoreIndex.fromDocuments(documents);

    // Create query engine tool
    const tools = [
      index.queryTool({
        options: {
          similarityTopK: 10,
        },
        metadata: {
          name: "document_search",
          description: "Search through uploaded documents",
        },
      }),
    ];

    // Create agent
    const agent = new OpenAIAgent({ tools });

    const { response } = await agent.chat({
      message: query,
    });
    
    return {
      message: response,
    };
  } catch (err) {
    console.error(err);
    return {
      error: "Error processing query",
    };
  }
}

Using Server Actions in Components

src/app/page.tsx
import { queryDocuments } from "@/actions/openai";

export const runtime = "nodejs";

export default async function Home() {
  const result = await queryDocuments(
    "What are the key features of this product?"
  );

  return (
    <main>
      <h1>Document Query</h1>
      <p>{result.message}</p>
    </main>
  );
}

API Routes Example

Create RESTful endpoints for LlamaIndex operations:
src/app/api/chat/route.ts
import { openai } from "@llamaindex/openai";
import { VectorStoreIndex } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    const { query } = await request.json();
    
    // Initialize your index (use caching in production)
    const index = await VectorStoreIndex.fromDocuments(documents);
    const queryEngine = index.asQueryEngine();
    
    const response = await queryEngine.query({ query });
    
    return NextResponse.json({
      response: response.toString(),
    });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to process query" },
      { status: 500 }
    );
  }
}

Streaming API Route

src/app/api/chat-stream/route.ts
import { openai } from "@llamaindex/openai";
import { VectorStoreIndex } from "llamaindex";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const { query } = await request.json();
  
  const index = await VectorStoreIndex.fromDocuments(documents);
  const queryEngine = index.asQueryEngine();
  
  const stream = await queryEngine.query({
    query,
    stream: true,
  });
  
  const encoder = new TextEncoder();
  
  const readableStream = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        controller.enqueue(encoder.encode(chunk.response));
      }
      controller.close();
    },
  });
  
  return new Response(readableStream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  });
}

Edge Runtime Example

Optimize for Vercel Edge Functions:
src/app/api/edge-chat/route.ts
import { openai } from "@llamaindex/openai";
import { SimpleChatEngine } from "llamaindex";

export const runtime = "edge";

export async function POST(request: Request) {
  const { message } = await request.json();
  
  const chatEngine = new SimpleChatEngine({
    llm: openai({ model: "gpt-4o-mini" }),
  });
  
  const response = await chatEngine.chat({ message });
  
  return Response.json({
    response: response.message.content,
  });
}
Edge runtime has limitations. Not all LlamaIndex features work in edge environments. Stick to basic LLM operations and avoid file system operations.

Client-Side Integration

React Hook for Chat

src/hooks/useChat.ts
import { useState } from "react";

export function useChat() {
  const [messages, setMessages] = useState<Array<{role: string; content: string}>>([]);
  const [loading, setLoading] = useState(false);
  
  const sendMessage = async (message: string) => {
    setLoading(true);
    setMessages(prev => [...prev, { role: "user", content: message }]);
    
    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ query: message }),
      });
      
      const data = await response.json();
      setMessages(prev => [...prev, { role: "assistant", content: data.response }]);
    } catch (error) {
      console.error("Chat error:", error);
    } finally {
      setLoading(false);
    }
  };
  
  return { messages, sendMessage, loading };
}

Chat Component

src/components/Chat.tsx
"use client";

import { useChat } from "@/hooks/useChat";
import { useState } from "react";

export default function Chat() {
  const { messages, sendMessage, loading } = useChat();
  const [input, setInput] = useState("");
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim()) {
      sendMessage(input);
      setInput("");
    }
  };
  
  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
        {loading && <div>Loading...</div>}
      </div>
      
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask a question..."
        />
        <button type="submit" disabled={loading}>
          Send
        </button>
      </form>
    </div>
  );
}

Environment Variables

Configure API keys in .env.local:
.env.local
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
PINECONE_API_KEY=...
Access in server-side code:
const apiKey = process.env.OPENAI_API_KEY;
Never expose API keys to the client. Only use NEXT_PUBLIC_ prefix for non-sensitive values.

Deployment Considerations

Vercel Deployment

  1. Install dependencies:
npm install llamaindex @llamaindex/openai @llamaindex/workflow
  1. Configure environment variables in Vercel dashboard
  2. Deploy:
vercel deploy

Caching Strategies

Cache expensive operations:
import { unstable_cache } from "next/cache";

const getCachedIndex = unstable_cache(
  async () => {
    const documents = await loadDocuments();
    return await VectorStoreIndex.fromDocuments(documents);
  },
  ["vector-index"],
  { revalidate: 3600 } // Cache for 1 hour
);

Using External Vector Stores

For production, use managed vector stores:
import { PineconeVectorStore } from "@llamaindex/pinecone";
import { VectorStoreIndex } from "llamaindex";

const vectorStore = new PineconeVectorStore({
  indexName: "my-index",
});

const index = await VectorStoreIndex.fromVectorStore(vectorStore);

Complete Example Structure

nextjs-app/
├── next.config.mjs          # LlamaIndex configuration
├── .env.local               # API keys
├── src/
│   ├── actions/
│   │   └── openai.ts        # Server actions
│   ├── app/
│   │   ├── api/
│   │   │   ├── chat/
│   │   │   │   └── route.ts # API routes
│   │   │   └── edge/
│   │   │       └── route.ts # Edge routes
│   │   ├── layout.tsx
│   │   └── page.tsx         # Main page
│   ├── components/
│   │   └── Chat.tsx         # Client components
│   └── hooks/
│       └── useChat.ts       # Custom hooks
└── package.json

Best Practices

1. Separate Server and Client Code

  • Keep LlamaIndex operations in server actions/API routes
  • Use client components only for UI
  • Never import LlamaIndex in client components

2. Error Handling

try {
  const result = await queryEngine.query({ query });
  return { success: true, data: result };
} catch (error) {
  console.error("Query failed:", error);
  return { success: false, error: "Failed to process query" };
}

3. Loading States

"use client";

import { useState } from "react";
import { queryDocuments } from "@/actions/openai";

export default function QueryButton() {
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState("");
  
  const handleQuery = async () => {
    setLoading(true);
    const response = await queryDocuments("query");
    setResult(response.message);
    setLoading(false);
  };
  
  return (
    <div>
      <button onClick={handleQuery} disabled={loading}>
        {loading ? "Loading..." : "Query"}
      </button>
      {result && <p>{result}</p>}
    </div>
  );
}

4. Type Safety

interface QueryResult {
  success: boolean;
  message?: string;
  error?: string;
}

export async function queryDocuments(query: string): Promise<QueryResult> {
  // Implementation
}

Next Steps

Server Actions

Learn about Next.js server actions

API Routes

Next.js API route documentation

Edge Runtime

Edge and Node.js runtimes

Vector Stores

Production vector store integration