Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.4

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.

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/generated

This 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 dev

Open 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 3002

Open 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.

Reference and source