From-template and manual setup paths to a working LangGraph chat.
Two paths to a running chat against a LangGraph Cloud server. The template is fastest; the manual path is what you adapt when integrating into an existing project.
From the template
npx create-assistant-ui@latest -t langchain my-app
cd my-appSet environment variables:
# LANGCHAIN_API_KEY=your_api_key # production
# LANGGRAPH_API_URL=your_api_url # production
NEXT_PUBLIC_LANGGRAPH_API_URL=your_api_url # development (no API key required)
NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=your_graph_idnpm run dev:frontendnpm run dev also starts the template's bundled agent with langgraphjs dev, which needs ANTHROPIC_API_KEY.
The template connects to LangGraph through useStreamRuntime from @assistant-ui/react-langchain; continue on that page to add features. To use useLangGraphRuntime, follow the manual setup below.
There is not a React Native LangGraph template yet. Use the manual setup below in an Expo app.
There is not an Ink LangGraph template yet. Use the manual setup below in a Node/Ink app.
Manual setup in an existing project
Install dependencies
npm install @assistant-ui/react @assistant-ui/react-langgraph @langchain/langgraph-sdknpx expo install @assistant-ui/react-native @assistant-ui/react-langgraph @langchain/langgraph-sdknpm install @assistant-ui/react-ink @assistant-ui/react-langgraph @langchain/langgraph-sdk ink reactCreate the LangGraph client helper
import { Client } from "@langchain/langgraph-sdk";
export const createClient = () => {
const apiUrl =
process.env["NEXT_PUBLIC_LANGGRAPH_API_URL"] ||
(typeof window !== "undefined"
? new URL("/api", window.location.href).href
: "/api");
return new Client({ apiUrl });
};import { Client } from "@langchain/langgraph-sdk";
export const createClient = () => {
const apiUrl =
process.env.EXPO_PUBLIC_LANGGRAPH_API_URL ?? "http://localhost:2024";
return new Client({ apiUrl });
};import { Client } from "@langchain/langgraph-sdk";
export const createClient = () => {
const apiUrl = process.env.LANGGRAPH_API_URL ?? "http://localhost:2024";
return new Client({ apiUrl });
};Build the assistant component
"use client";
import { useMemo } from "react";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import {
unstable_createLangGraphStream,
useLangGraphRuntime,
type LangChainMessage,
} from "@assistant-ui/react-langgraph";
import { createClient } from "@/lib/chatApi";
const ASSISTANT_ID = process.env["NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID"]!;
export function MyAssistant() {
const client = useMemo(() => createClient(), []);
const stream = useMemo(
() =>
unstable_createLangGraphStream({
client,
assistantId: ASSISTANT_ID,
}),
[client],
);
const runtime = useLangGraphRuntime({
unstable_allowCancellation: true,
stream,
create: async () => {
const { thread_id } = await client.threads.create();
return { externalId: thread_id };
},
load: async (externalId) => {
const state = await client.threads.getState<{
messages: LangChainMessage[];
}>(externalId);
return {
messages: state.values.messages,
interrupts: state.tasks[0]?.interrupts,
};
},
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}import { useMemo } from "react";
import { AssistantRuntimeProvider } from "@assistant-ui/react-native";
import {
unstable_createLangGraphStream,
useLangGraphRuntime,
type LangChainMessage,
} from "@assistant-ui/react-langgraph";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { createClient } from "@/lib/chatApi";
const ASSISTANT_ID = process.env.EXPO_PUBLIC_LANGGRAPH_ASSISTANT_ID!;
export function MyAssistant() {
const client = useMemo(() => createClient(), []);
const stream = useMemo(
() =>
unstable_createLangGraphStream({
client,
assistantId: ASSISTANT_ID,
}),
[client],
);
const runtime = useLangGraphRuntime({
unstable_allowCancellation: true,
stream,
create: async () => {
const { thread_id } = await client.threads.create();
return { externalId: thread_id };
},
load: async (externalId) => {
const state = await client.threads.getState<{
messages: LangChainMessage[];
}>(externalId);
return {
messages: state.values.messages,
interrupts: state.tasks[0]?.interrupts,
};
},
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}import { useMemo } from "react";
import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
import {
unstable_createLangGraphStream,
useLangGraphRuntime,
type LangChainMessage,
} from "@assistant-ui/react-langgraph";
import { Thread } from "./thread.js";
import { createClient } from "../lib/chatApi.js";
const ASSISTANT_ID = process.env.LANGGRAPH_ASSISTANT_ID!;
export function MyAssistant() {
const client = useMemo(() => createClient(), []);
const stream = useMemo(
() =>
unstable_createLangGraphStream({
client,
assistantId: ASSISTANT_ID,
}),
[client],
);
const runtime = useLangGraphRuntime({
unstable_allowCancellation: true,
stream,
create: async () => {
const { thread_id } = await client.threads.create();
return { externalId: thread_id };
},
load: async (externalId) => {
const state = await client.threads.getState<{
messages: LangChainMessage[];
}>(externalId);
return {
messages: state.values.messages,
interrupts: state.tasks[0]?.interrupts,
};
},
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}Mount the component
import { MyAssistant } from "@/components/MyAssistant";
export default function Home() {
return (
<main className="h-dvh">
<MyAssistant />
</main>
);
}import { View } from "react-native";
import { MyAssistant } from "@/components/MyAssistant";
export default function Home() {
return (
<View style={{ flex: 1 }}>
<MyAssistant />
</View>
);
}import { Box } from "ink";
import { MyAssistant } from "./components/MyAssistant.js";
export function App() {
return (
<Box flexDirection="column">
<MyAssistant />
</Box>
);
}Set environment variables
Use the same .env.local shape as the template path above.
EXPO_PUBLIC_LANGGRAPH_API_URL=http://localhost:2024
EXPO_PUBLIC_LANGGRAPH_ASSISTANT_ID=your_graph_idLANGGRAPH_API_URL=http://localhost:2024
LANGGRAPH_ASSISTANT_ID=your_graph_idSet up UI components
Follow the UI Components guide to wire up the Thread, composer, and supporting primitives.
Follow the React Native setup to add a native Thread, composer, and supporting primitives.
Follow the Ink setup to add a terminal Thread, composer, and supporting primitives.
Production proxy backend
For development, the client above hits LangGraph Cloud directly using NEXT_PUBLIC_LANGGRAPH_API_URL. For production, proxy through your own backend so your API key never reaches the client. Limit the proxy to the endpoints you actually need.
import { type NextRequest, NextResponse } from "next/server";
const ALLOWED_METHODS = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
function isAllowedRequestContext(req: NextRequest) {
const fetchSite = req.headers.get("sec-fetch-site");
if (fetchSite !== null) {
return fetchSite === "same-origin" || fetchSite === "none";
}
const origin = req.headers.get("origin");
if (origin === null) return true;
try {
return new URL(origin).origin === req.nextUrl.origin;
} catch {
return false;
}
}
function crossOriginResponse() {
return NextResponse.json(
{ error: "Cross-origin requests are not allowed." },
{ status: 403 },
);
}
async function handleRequest(req: NextRequest, method: string) {
if (!isAllowedRequestContext(req)) return crossOriginResponse();
try {
const apiUrl = process.env.LANGGRAPH_API_URL?.trim();
if (!apiUrl) {
return NextResponse.json(
{ error: "LANGGRAPH_API_URL is not configured." },
{ status: 503 },
);
}
const path = req.nextUrl.pathname.replace(/^\/?api\//, "");
const searchParams = new URLSearchParams(req.nextUrl.search);
searchParams.delete("_path");
searchParams.delete("nxtP_path");
const queryString = searchParams.toString()
? `?${searchParams.toString()}`
: "";
const headers = new Headers();
const apiKey = process.env.LANGCHAIN_API_KEY?.trim();
if (apiKey) headers.set("x-api-key", apiKey);
const accept = req.headers.get("accept");
if (accept) headers.set("accept", accept);
const contentType = req.headers.get("content-type");
if (contentType) headers.set("content-type", contentType);
const options: RequestInit = {
method,
headers,
redirect: "manual",
signal: req.signal,
};
if (["POST", "PUT", "PATCH"].includes(method)) {
options.body = await req.text();
}
const res = await fetch(`${apiUrl}/${path}${queryString}`, options);
if (
res.status === 0 ||
(res.status >= 300 && res.status < 400 && res.status !== 304) ||
res.type === "opaqueredirect"
) {
await res.body?.cancel().catch(() => undefined);
return NextResponse.json(
{ error: "LangGraph returned an unexpected redirect." },
{ status: 502 },
);
}
const responseHeaders = new Headers(res.headers);
for (const name of [
"access-control-allow-credentials",
"access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"content-encoding",
"content-length",
"set-cookie",
"transfer-encoding",
]) {
responseHeaders.delete(name);
}
responseHeaders.set("Cache-Control", "no-store");
return new NextResponse(res.body, {
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
});
} catch (e: unknown) {
if (e instanceof Error) {
const typedError = e as Error & { status?: number };
return NextResponse.json(
{ error: typedError.message },
{ status: typedError.status ?? 500 },
);
}
return NextResponse.json({ error: "Unknown error" }, { status: 500 });
}
}
export const GET = (req: NextRequest) => handleRequest(req, "GET");
export const POST = (req: NextRequest) => handleRequest(req, "POST");
export const PUT = (req: NextRequest) => handleRequest(req, "PUT");
export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH");
export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE");
export const OPTIONS = (req: NextRequest) =>
isAllowedRequestContext(req)
? new NextResponse(null, {
status: 204,
headers: { Allow: ALLOWED_METHODS },
})
: crossOriginResponse();The route accepts a request only when Sec-Fetch-Site is same-origin or none, falls back to comparing the Origin header against the request URL for clients that omit Fetch Metadata, and does not follow upstream redirects. Behind a reverse proxy, that fallback needs the public scheme and host preserved in the request URL.
Warning
A request-context check is not authentication: requests that send neither header still pass. Before deploying with a cloud API key, require your application session in the proxy route and apply a durable rate limit.
With this route in place, drop NEXT_PUBLIC_LANGGRAPH_API_URL from production env vars; the client helper falls back to the same-origin /api path. Set LANGCHAIN_API_KEY and LANGGRAPH_API_URL server-side instead.