Call your API from React
Connect Vite or Next.js to your backend with generated types and display a validated response without importing server code.
Show Hello, RELKIT! in a React page using the same GET /hello operation you ran in
Create and run an API. Your backend owns its input and output
schemas; the frontend receives generated types and calls it through RPC.
Select your framework in the tabs below. Your selection carries through the remaining steps.
1. Install the frontend client
Install the React client and its selected peers, preserving compatible versions already used by your app:
From this guide’s application/package root, install only its selected requirements:
bun add @opentelemetry/api@1.9.1 @orpc/tanstack-query@2.0.0-beta.31 @relkit/client@0.8.2 '@tanstack/query-core@>=5.80.2' '@tanstack/react-query@>=5.80.2' 'react@>=18.0.0'Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.
Install the CLI as frontend development tooling so it can pull and check the contract:
From this guide’s application/package root, install only its selected requirements:
bun add --dev @relkit/cli@0.8.4Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.
Keep your existing React DOM and TypeScript tooling. The frontend does not need the backend’s database, storage, or model-provider packages.
2. Pull the backend contract
From the backend root, run bun run check after changing a route. With the backend running,
run this in the frontend root:
bun run relkit client pull http://127.0.0.1:3000 --out .relkit/generatedThis writes contract.ts, client.ts, client-registry.d.ts, and client-manifest.json.
Keep them together and regenerate them with the command; do not edit them or import backend
service.ts files into the browser.
Include the generated declarations in the TypeScript project that checks your React source.
In a Vite app this is usually tsconfig.app.json. Merge these include entries into your existing
configuration, keeping its compiler options:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src", ".relkit/generated/**/*.ts"]
}Merge these include entries into your existing tsconfig.json. Keep your compiler options,
Next.js plugin, and any additional entries your application already uses:
{
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
".relkit/generated/**/*.ts"
]
}Your editor should now offer GET /hello as a selector and an optional name string as its input.
If the selector is missing, check the backend URL, the pull output, and the TypeScript include list.
3. Connect the local frontend
The build helper reads the fingerprint from the frontend’s .relkit/generated/client-manifest.json.
It does not pull a contract, generate types, or configure forwarding on its own.
Add relkit() to your existing Vite plugins and merge these proxy entries into server.proxy.
Keep your framework’s existing plugins:
/** Adds the pulled fingerprint and a local same-origin proxy to an existing Vite React project. */
import { relkit } from "@relkit/client/build/vite";
export default {
plugins: [relkit()],
server: {
host: "127.0.0.1",
port: 5173,
strictPort: true,
proxy: {
"/rpc": { target: "http://127.0.0.1:3000", ws: true },
"/_relkit": { target: "http://127.0.0.1:3000" },
},
},
};The browser sends /rpc calls and the /_relkit/v1/client/identity handshake to the frontend
origin; Vite forwards them to the backend. This lets the provider use the current page’s origin.
Keep 127.0.0.1 consistent in these local URLs.
The development proxy is not part of the production build. Before publishing, configure equivalent
paths at your production gateway, or set the provider’s public baseUrl and configure
credentialed CORS and cookies for separate origins.
Wrap your existing Next.js configuration with withRelkit and merge these forwarding rules into
its rewrites. Keep the other options and any existing rules:
/** Embeds the pulled fingerprint and forwards local browser requests to the backend. */
import { withRelkit } from "@relkit/client/build/next";
export default withRelkit({
/** Keeps RPC and identity requests on the frontend origin.
* @returns Local forwarding rules; replace their destinations for production. */
async rewrites() {
return [
{ source: "/rpc/:path*", destination: "http://127.0.0.1:3000/rpc/:path*" },
{ source: "/_relkit/:path*", destination: "http://127.0.0.1:3000/_relkit/:path*" },
];
},
});withRelkit embeds the pulled contract fingerprint.
The Next.js rewrites
forward /rpc calls and the /_relkit/v1/client/identity handshake to RELKIT. The browser stays
on the frontend origin, so the provider needs no separate baseUrl.
Keep RELKIT running as a separate backend on port 3000; run Next.js on port 3002 in step 5.
These rewrites also run in production: replace their local destinations with your backend’s server URL
before deploying. Static exports cannot run rewrites; use your gateway or a public backend URL with
credentialed CORS and cookies instead.
If your app adds sign-in, forward its auth mount too.
4. Render the typed response
RelkitClientProvider owns the connection and query cache; useRoute calls the generated
operation with domain input. Both frameworks use the same component.
Replace the starter src/App.tsx with this component. Keep your existing entry point rendering App:
"use client";
/** Renders the minimal backend's greeting through the pulled registry and a same-origin client provider. */
import { RelkitClientProvider, useRelkitClient, useRoute } from "@relkit/client/react";
/** Displays the typed Hello result after the provider resolves backend identity.
* @returns Loading, connection failure, request failure, or the successful greeting. */
export function Greeting() {
const { status } = useRelkitClient();
const greeting = useRoute("GET /hello", { input: { name: "RELKIT" } });
if (status === "application-updated") {
return <p role="alert">The API changed. Pull its contract and restart the frontend.</p>;
}
if (status === "error") return <p role="alert">Cannot connect to the API.</p>;
if (greeting.isError) return <p role="alert">{greeting.error.message}</p>;
if (greeting.isPending) return <p role="status">Loading greeting…</p>;
return <h1>{greeting.data.message}</h1>;
}
/** Supplies the shared query cache and identity handshake to the frontend tree.
* @returns The greeting inside a same-origin RELKIT provider. */
export default function App() {
return (
<RelkitClientProvider>
<Greeting />
</RelkitClientProvider>
);
}Create app/relkit-app.tsx with this component. If your project uses src/app, place both files
there instead. The top-level "use client" directive
allows the provider and hooks to run inside a Client Component:
"use client";
/** Renders the minimal backend's greeting through the pulled registry and a same-origin client provider. */
import { RelkitClientProvider, useRelkitClient, useRoute } from "@relkit/client/react";
/** Displays the typed Hello result after the provider resolves backend identity.
* @returns Loading, connection failure, request failure, or the successful greeting. */
export function Greeting() {
const { status } = useRelkitClient();
const greeting = useRoute("GET /hello", { input: { name: "RELKIT" } });
if (status === "application-updated") {
return <p role="alert">The API changed. Pull its contract and restart the frontend.</p>;
}
if (status === "error") return <p role="alert">Cannot connect to the API.</p>;
if (greeting.isError) return <p role="alert">{greeting.error.message}</p>;
if (greeting.isPending) return <p role="status">Loading greeting…</p>;
return <h1>{greeting.data.message}</h1>;
}
/** Supplies the shared query cache and identity handshake to the frontend tree.
* @returns The greeting inside a same-origin RELKIT provider. */
export default function App() {
return (
<RelkitClientProvider>
<Greeting />
</RelkitClientProvider>
);
}Render it from your page. Keep your existing app/layout.tsx, including its <html> and <body>:
/** Renders the shared Hello example from a Next.js App Router server page. */
import RelkitApp from "./relkit-app";
/** Keeps provider hooks inside the imported Client Component.
* @returns The greeting within the application's existing root layout. */
export default function Page() {
return <RelkitApp />;
}Keep React hooks in the Client Component. For calls from a Node.js Server Component or Route Handler,
use createClient with an explicit backend URL and the appropriate
request identity. The /server and /server/react helpers require Bun; see
server rendering before selecting a server-side API.
greeting.data.message is inferred from the backend function’s output schema. No shared handwritten
response interface is needed. The GET /hello selector identifies that route, but the client sends
RPC over /rpc; it does not construct a REST query string. Raw handlers are not generated operations.
5. Verify the page and contract
From the frontend root:
bun run tsc --noEmit -p tsconfig.app.json
bun run relkit client check http://127.0.0.1:3000 --out .relkit/generated
bun run devOpen http://127.0.0.1:5173. Expect a loading message followed by Hello, RELKIT!. In the browser’s
Network panel, the identity request and the RPC request should succeed. Stop the backend and reload
to check the connection error message.
With your existing dev script running next dev, run:
bun run tsc --noEmit
bun run relkit client check http://127.0.0.1:3000 --out .relkit/generated
bun run dev --hostname 127.0.0.1 --port 3002Open http://127.0.0.1:3002. Expect a loading message followed by Hello, RELKIT!. In the browser’s
Network panel, the identity and RPC requests should use port 3002 and succeed through the rewrites.
Stop the backend and reload to check the connection error message.
After changing the public backend contract, run client pull again, typecheck, and restart the
frontend dev server so the plugin loads the new fingerprint. Use client check before a frontend
build. A fingerprint mismatch blocks new typed work with application-updated; coordinate frontend
and backend releases instead of bypassing the check.
Connect another capability
Each module documents its backend declaration next to the corresponding frontend usage:
| Backend capability | Frontend entry point | Continue with |
|---|---|---|
defineRoute / defineServiceRoutes |
useRoute, useRouteMutation, or createClient |
Call a defined route |
streamOf route output |
useStream |
Consume a response stream |
defineJob with a client policy |
useJobTrigger, useJobRun, or the generated client.jobs API |
Trigger and observe a job |
| Agent with a client policy | useAgent |
Run an agent |
defineGraph with a client policy |
useAgent with typed initial input and resume replies |
Run and resume a graph |
defineChannel with browser access |
useChannel, useRealtime |
Subscribe to a channel |
| Better Auth mount | Better Auth’s browser client and the RELKIT identity provider | Sign-in clients |
Buckets, caches, database models, and backend events have no direct generated browser API. Expose an authorized function through a route when the frontend needs that operation. File uploads use the route’s multipart HTTP contract, rather than treating files as ordinary RPC JSON.
Related tasks
- Add sign-in before exposing private application data.
- Upload product images when the frontend sends files.