agents/skills/routing/SKILL.md
React Router v7 routing patterns and environment variable configuration. Use whenever you touch React Router–related code (routes, links, params, loaders, actions, route config, or env in route context).
npx skillsauth add firtoz/cf-multiworker-starter-kit routingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This project uses React Router v7 with file-based routing configured in apps/web/app/routes.ts.
WHENEVER you edit apps/web/app/routes.ts, you MUST run typegen from the workspace root:
bun run typegen
Run from repo root (turbo routes to the web app). Without this, TypeScript imports will fail. The typegen command generates the +types files that route components need. Full typegen / typecheck cadence: multiworker-workflow.
Create the route file in the appropriate directory under apps/web/app/routes/:
routes/home.tsx, routes/visitors.tsx, routes/chat.tsxroutes/dashboard.tsx)// Example: Adding a new route
route("dashboard", "routes/dashboard.tsx"),
This step is REQUIRED, not optional!
bun run typegen
Run from workspace root.
Use the generated types:
// After typegen, this import will work:
import type { Route } from "./+types/dashboard";
export async function action({ request }: Route.ActionArgs) {
// ...
}
RoutePath)Every route module should export its URL so @firtoz/router-toolkit (e.g. formAction, useDynamicSubmitter) stays type-safe. The string must match the path registered in routes.ts:
import { type RoutePath } from "@firtoz/router-toolkit";
export const route: RoutePath<"/dashboard"> = "/dashboard";
Omitting this is a common mistake after cloning; forms and typed submitters depend on it. See form-submissions/SKILL.md.
Promise<MaybeError<...>>Use @firtoz/maybe-error (success / fail, type MaybeError) — import directly from that package (it is not re-exported by @firtoz/router-toolkit) — so return types are a discriminated union and TypeScript can narrow.
Loaders
Promise<MaybeError<YourData>>.success({ ...fields }) or fail("message") (or a typed error as the second generic).loaderData.success before reading loaderData.result; handle loaderData.error on failure.Deferred values (promises for <Await>) can live inside the success payload: success({ items: itemsPromise }).
Actions
formAction({ ... }) — the handler already returns Promise<MaybeError<...>> with validation errors folded in.formAction, still return success / fail the same way for consistent typing with useFetcher / toolkit helpers.For internal React UI submissions, prefer formAction + useDynamicSubmitter and route path exports. Do not reach for plain HTML forms unless you intentionally want browser-native behavior.
For external clients, terminal smoke tests, or plain HTML forms that post to an index route, remember React Router index actions require the ?index target:
useDynamicSubmitter<RouteMod>("/some-route").submitJson(...)action="/?index"POST /?index, not POST /Avoid teaching new app features to post plain forms to index routes. If an endpoint must be called externally, prefer a non-index resource route such as /sessions/new.
React Router routes render on the server. Do not access browser globals during render or in code that can run on the server.
Unsafe:
useMemo during SSR if it touches browser globalsSafe places:
useEffectClientOnly wrappersBrowser-bound APIs include window, document, WebSocket, canvas, localStorage, and DOM APIs. WebSocket helpers may use window.location, but call them only from useEffect / client-only handlers, or pass an origin explicitly from client-only code.
href from react-router)Import href from react-router and use it for all <Link to> values. Do not hardcode paths or concatenate strings.
import { Link, href } from "react-router";
href("/"), href("/collection"):param and an object of param values:<Link to={href("/")} />
<Link to={href("/collection")} />
<Link to={href("/charts/:id", { id: c.id.toString() })} />
Creating a route file without registering it in routes.ts will result in a 404. Always register new routes!
When adding new environment variables (see cf-workers-env-local):
.env.example (human checklist only — setup and tooling do not read it).env.local (and .env.production if needed for prod) with real valuesbun run typegen from workspace root to regenerate TypeScript types for envbun run typegen
The env object from cloudflare:workers is typed from apps/web/types/env.d.ts (package Alchemy web resource + declare module "cloudflare:workers"). Keep it aligned with apps/web/alchemy.run.ts (and .env.local at runtime).
Do not read bindings from React Router loader/action context (e.g. context.cloudflare.env). In this project, use:
import { env } from "cloudflare:workers";
development
Repo-root commands, typegen and typecheck cadence, lint, deploy, adding packages with bun, and Alchemy app layout. Use at the start of a task, before PR, or when choosing turbo/typegen commands.
development
Fork and template gotchas (env import, routes, typegen, forms, D1, Turbo, HMR, new DO packages). Use when working on apps/web or durable-objects, or when behavior diverges from this stack’s conventions.
testing
Turborepo task configuration patterns for monorepo management. Use when configuring turbo.json tasks, setting up task dependencies, managing cache inputs/outputs, or working with cross-package dependencies in the monorepo.
development
React patterns for callbacks, event handlers, and module-level constants. Use when writing React components, implementing event handlers, or defining constants.