Add Agentuity service clients to a React Router framework-mode app (the full-stack SSR mode, not a plain SPA). Keep your route modules, loaders, actions, and routes.ts as-is. Agentuity connects through the CLI and its service packages wherever server code needs storage, AI Gateway routing, or deploy metadata.
Create the Framework App
npx create-react-router@latest my-react-router-app
cd my-react-router-appThen add Agentuity:
npm install -D @agentuity/cli
npx agentuity project import --validate-only
npx agentuity project importUse the package manager that create-react-router configured. The CLI install is project-local: -D or --dev records @agentuity/cli in devDependencies, which keeps the CLI version in the app's lockfile. Bun's equivalent is lowercase -d.
| Package manager | Add local CLI | Run local CLI | Undo local install |
|---|---|---|---|
| npm | npm install -D @agentuity/cli | npx agentuity ... | npm uninstall @agentuity/cli |
| Bun | bun add -d @agentuity/cli | bunx agentuity ... | bun remove @agentuity/cli |
| pnpm | pnpm add -D @agentuity/cli | pnpm exec agentuity ... | pnpm remove @agentuity/cli |
| Yarn | yarn add -D @agentuity/cli | yarn exec agentuity ... | yarn remove @agentuity/cli |
A global install is optional. It only makes bare agentuity ... work outside a project; it does not import or register the app. You can switch back by adding the dev dependency again and using the package-manager exec wrapper. The global install can stay installed, or you can remove it with the package manager that created it. See Local versus global CLI for the trade-offs.
agentuity project import writes the local metadata needed for deploys. The --validate-only flag lets you review before linking.
Agentuity does not scaffold React Router from the create flow today. The build detector still recognizes existing React Router framework-mode apps from react-router.config.* or the React Router Vite plugin.
The examples below use these files:
| File | Purpose |
|---|---|
app/routes/home.tsx | UI component that calls the resource route |
app/routes/api.translate.ts | Resource route that exports loader and action only |
app/routes.ts | Route map registering both modules |
Import an Existing App
Use the same Agentuity commands in an app that already exists:
npm install -D @agentuity/cli
npx agentuity project import --validate-only # review what the CLI finds
npx agentuity project import # write local project metadataUse the package-manager table above if the app is not npm-based.
See Add Agentuity to an existing app for validation, import, the Invalid project folder deploy case, and first deploy.
What Agentuity adds
| Area | React Router keeps | Agentuity adds |
|---|---|---|
| routing | routes.ts, loaders, actions, and resource routes | service clients imported in server route code |
| local dev | react-router dev | agentuity dev supplies service credentials |
| build | React Router build output | agentuity build runs the build and packages launch metadata |
| deploy | the React Router server entry | agentuity deploy ships the packaged app |
Add a Resource Route
A resource route is any route module that exports loader or action but no default component. React Router sends GET requests to loader and POST, PUT, PATCH, DELETE to action.
The route below uses AIGatewayClient for the model call. The project SDK key authenticates the request, and the model ID is app configuration or request input.
Install the packages used in this example:
npm install @agentuity/aigateway @agentuity/keyvalue zodCreate the route file:
import { AIGatewayClient } from '@agentuity/aigateway';
import { KeyValueClient } from '@agentuity/keyvalue';
import { z } from 'zod';
import type { Route } from './+types/api.translate';
const DEFAULT_MODEL = 'openai/gpt-5.4-mini';
const HISTORY_NAMESPACE = 'translation-history';
const HISTORY_LIMIT = 5;
const SESSION_COOKIE = 'agentuity_session';
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; // 30 days
interface HistoryEntry {
readonly model: string;
readonly sessionId: string;
readonly text: string;
readonly timestamp: string;
readonly toLanguage: string;
readonly translation: string;
}
interface HistoryState {
readonly history: readonly HistoryEntry[];
readonly translationCount: number;
}
const requestSchema = z.object({
model: z.string().optional(),
text: z.string(),
toLanguage: z.string(),
});
const kv = new KeyValueClient();
const gateway = new AIGatewayClient();
function createSessionId(): string {
return `sess_${crypto.randomUUID().replaceAll('-', '').slice(0, 24)}`;
}
function getSessionId(request: Request): string {
const cookie = request.headers.get('cookie') ?? '';
const match = cookie.match(new RegExp(`(?:^|; )${SESSION_COOKIE}=([^;]+)`));
return match?.[1] ? decodeURIComponent(match[1]) : createSessionId();
}
function sessionCookie(sessionId: string): string {
const parts = [
`${SESSION_COOKIE}=${encodeURIComponent(sessionId)}`,
'HttpOnly',
'Path=/',
'SameSite=Lax',
`Max-Age=${SESSION_TTL_SECONDS}`,
];
if (process.env.NODE_ENV === 'production') parts.push('Secure');
return parts.join('; ');
}
async function readHistory(sessionId: string): Promise<HistoryState> {
const result = await kv.get<HistoryState>(HISTORY_NAMESPACE, sessionId);
return result.exists ? result.data : { history: [], translationCount: 0 };
}
// loader handles GET and returns session history
export async function loader({ request }: Route.LoaderArgs) {
const sessionId = getSessionId(request);
const state = await readHistory(sessionId);
return Response.json(
{ sessionId, history: state.history, translationCount: state.translationCount },
{ headers: { 'Set-Cookie': sessionCookie(sessionId) } }
);
}
// action handles POST / PUT / PATCH / DELETE
export async function action({ request }: Route.ActionArgs) {
const sessionId = getSessionId(request);
if (request.method === 'DELETE') {
await kv.delete(HISTORY_NAMESPACE, sessionId);
return Response.json(
{ sessionId, history: [], translationCount: 0 },
{ headers: { 'Set-Cookie': sessionCookie(sessionId) } }
);
}
const body: unknown = await request.json();
const parsed = requestSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: 'text and toLanguage are required' }, { status: 400 });
}
const { text, toLanguage } = parsed.data;
const model = parsed.data.model ?? DEFAULT_MODEL;
const result = await gateway.completeText({
model,
messages: [{ role: 'user', content: `Translate to ${toLanguage}:\n\n${text}` }],
});
if (!result.hasText) {
return Response.json({ error: 'model returned no text' }, { status: 502 });
}
const translation = result.text;
const previous = await readHistory(sessionId);
const history = [
...previous.history,
{ model, sessionId, text, timestamp: new Date().toISOString(), toLanguage, translation },
].slice(-HISTORY_LIMIT);
const next: HistoryState = { history, translationCount: previous.translationCount + 1 };
await kv.set(HISTORY_NAMESPACE, sessionId, next, { ttl: SESSION_TTL_SECONDS });
return Response.json(
{ sessionId, history, translationCount: next.translationCount, translation, model, toLanguage },
{ headers: { 'Set-Cookie': sessionCookie(sessionId) } }
);
}
// Omitting the default export makes this a resource routeRoute.LoaderArgs and Route.ActionArgs come from React Router's typegen. Run react-router typegen (or a package script that runs it) before a direct TypeScript check or agentuity build; the import type line stays unresolved until the generated .react-router/types output exists.
Use the model id exactly as it appears in the AI Gateway model catalog. Use a provider SDK directly only when the resource route depends on provider-specific APIs.
Register the route in app/routes.ts:
import { index, route } from '@react-router/dev/routes';
import type { RouteConfig } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('api/translate', 'routes/api.translate.ts'),
] satisfies RouteConfig;Call from a Component
Use fetch from client code. Do not use React Router's <Link> for resource routes because client-side navigation won't reach a module with no component to render.
async function translate(text: string, toLanguage: string, model: string) {
const response = await fetch('/api/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, toLanguage, model }),
});
if (!response.ok) {
const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
throw new Error(err.error ?? `HTTP ${response.status}`);
}
return response.json();
}
// GET history on mount
const history = await fetch('/api/translate').then((r) => r.json());
// DELETE to clear
await fetch('/api/translate', { method: 'DELETE' });Plain anchor tags and <Link reloadDocument> also work for non-JSON resource routes (downloads, feeds, etc.) because they issue a full browser request.
Run Locally
agentuity dev supplies AGENTUITY_SDK_KEY and a PORT value. Use a wrapper script that delegates to the framework dev command:
{
"scripts": {
"dev": "agentuity dev --script dev:start",
"dev:start": "react-router dev --port 3000",
"build": "react-router build",
"deploy": "agentuity deploy"
}
}Then run:
npm run devThe --port 3000 flag is required because Vite (which React Router uses under the hood) ignores the PORT env var.
Smoke-test the resource route before touching the UI:
# GET, returns session history
curl http://localhost:3000/api/translate
# POST, translate text
curl http://localhost:3000/api/translate \
-H 'Content-Type: application/json' \
-d '{"text":"Hello","toLanguage":"Spanish"}'
# DELETE, clear history
curl -X DELETE http://localhost:3000/api/translateDeployed Environment Variables
Configure the variables your deployed app needs. Direct AIGatewayClient calls use AGENTUITY_SDK_KEY; the model ID in this example is normal app configuration or request input.
| Variable | Used by |
|---|---|
AGENTUITY_SDK_KEY | Agentuity service clients and @agentuity/aigateway |
DATABASE_URL | database resource routes |
AWS_* | object storage resource routes; see Object Storage for exact variables |
Validate Before Deploy
Run React Router typegen and the framework build when you want framework-only feedback, then run the Agentuity packaging check. agentuity build runs the framework build during packaging and writes .agentuity/launch.json.
npx react-router typegen
npm run build
npx agentuity buildReact Router outputs to build/ (client assets in build/client/, server in build/server/). Agentuity detects framework-mode apps from react-router.config.* or the React Router Vite plugin. Inspect the generated metadata to confirm the server entry and static paths match what was built:
cat .agentuity/launch.jsonDeploy only after the packaging check and a local packaged validation of / and the resource route pass:
npx agentuity deploy --confirmCommon Gotchas
| Symptom | What to check |
|---|---|
| Route returns 404 in dev or deployed | app/routes.ts must include route('api/translate', 'routes/api.translate.ts'); React Router does not auto-discover routes |
Cannot find module './+types/api.translate' | Run npx react-router typegen or your package typecheck script before direct TypeScript checks |
| KV or AI Gateway calls return auth errors | Start the app through the agentuity dev --script dev:start wrapper so credentials are available |
| Model call returns an auth error | Verify AGENTUITY_SDK_KEY is available to the resource route. See AI Gateway |
| Deploy starts but only static assets are served | Inspect .agentuity/launch.json: processes[0].command must run the React Router server entry (build/server/index.js) |
Client-side <Link> to the resource route renders nothing or errors | Resource routes have no component; navigate to them with fetch, plain <a>, or <Link reloadDocument> |
Framework Docs
- React Router
routes.ts: route module registration reference - React Router Resource Routes:
loader/actionbehavior for non-UI routes - React Router Route Module:
Route.LoaderArgs,Route.ActionArgs, and other typegen exports
Next Steps
- App Configuration: configure
agentuity.json, env vars, and scripts - Build Configuration: inspect launch metadata and packaging output
- AI Gateway: route supported model calls through Agentuity
- Key-Value Storage: store compact route state by namespace and key