Claude Code for Remix and React Router: Practical loader/action Guide
Build Remix-style React Router apps with Claude Code: loaders, actions, error boundaries, SEO, and review prompts.
The 2026 baseline for Remix work
When people say “Remix development” in 2026, they usually mean two related things: maintaining Remix v2 apps and using the Remix design model inside React Router v7 Framework Mode. The official Remix docs now point readers toward React Router for the latest framework features, and the React Router v7 release explains that Remix features have moved into React Router. That matters when you ask Claude Code for help. If the prompt only says “build this in Remix”, you may get a mix of older @remix-run/* imports, new React Router imports, and plain client-side fetch code.
The beginner-friendly mental model is simple: Remix-style apps put server work and UI close together at the route level. A loader reads data before a page renders. An action handles form submissions and writes data. The component renders the page, and the route can also define an error boundary and SEO metadata. This makes the work very reviewable: you can ask Claude Code to inspect one route and check its read path, write path, user states, error states, and search snippet at the same time.
This guide builds a small copy-pasteable React Router v7 Framework Mode app with product listing, product detail, and a contact form. It covers three core use cases: loading data with a loader, handling a form with an action, and keeping failures inside a route-level ErrorBoundary. It also shows how to ask Claude Code for a useful review instead of only code generation. The primary references checked for this update are Remix Docs, the React Router v7 release post, Route Module docs, Error Boundaries docs, and the Form API.
flowchart LR
A["URL and route"] --> B["loader reads data"]
B --> C["UI component renders"]
C --> D["Form submits"]
D --> E["action validates and writes"]
E --> B
B --> F["ErrorBoundary handles read errors"]
E --> F
Choose the mode before writing code
React Router v7 can be used in Declarative, Data, or Framework Mode. The Remix-like experience is Framework Mode: route modules, server rendering, loaders, actions, type generation, and deployment conventions are part of the project shape. For a greenfield business app, this is usually the clearest starting point.
| Situation | Good choice | What to tell Claude Code |
|---|---|---|
| New internal app | React Router v7 Framework Mode | Use create-react-router and route modules |
| Existing Remix v2 app | Keep the current Remix imports | Match the existing @remix-run/* code style |
| Existing React SPA | Introduce Data Mode first | Move one screen toward loader/action patterns |
| Marketing page with a form | Framework Mode or SPA Mode | Decide whether SSR and server actions are needed |
This up-front decision prevents a lot of cleanup. Claude Code is fast, but it will happily combine patterns unless the boundary is explicit. In this article the target is a new React Router v7 Framework Mode app, while still explaining it as Remix-style development.
Create the runnable mini app
Create a small app and keep the first version deliberately boring. A product list, a product detail page, and a contact form are enough to exercise the key pieces.
npx create-react-router@latest rr-claude-shop
cd rr-claude-shop
npm install
npm run dev
Define the route map in app/routes.ts.
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("products", "routes/products.tsx"),
route("products/:productId", "routes/products.$productId.tsx"),
route("contact", "routes/contact.tsx"),
] satisfies RouteConfig;
For the first pass, keep data in a server-only module. Replacing this module with Prisma, Drizzle, Supabase, or a private API later is straightforward because the route code already calls functions instead of touching storage directly.
// app/data/products.server.ts
export type Product = {
id: string;
name: string;
description: string;
price: number;
};
const products: Product[] = [
{
id: "starter",
name: "Claude Code Starter Kit",
description: "Small prompts and review checklists for the first team rollout.",
price: 9800,
},
{
id: "team",
name: "Team Workflow Pack",
description: "Route reviews, test prompts, and deployment checklists for teams.",
price: 29800,
},
];
const leads: Array<{ id: string; email: string; message: string }> = [];
export async function listProducts(query = "") {
const q = query.trim().toLowerCase();
if (!q) return products;
return products.filter((product) =>
`${product.name} ${product.description}`.toLowerCase().includes(q),
);
}
export async function getProduct(productId: string) {
return products.find((product) => product.id === productId) ?? null;
}
export async function saveLead(input: { email: string; message: string }) {
const lead = { id: crypto.randomUUID(), ...input };
leads.push(lead);
return lead;
}
Use case 1: read data with a loader
A loader should gather the data needed to render the route. It is a better default than putting initial page data behind a useEffect, because the route owns the loading, error, and rendering contract. In this example the product list reads a query string, filters products on the server, and renders SEO elements in the same route component.
// app/routes/products.tsx
import { Form, Link, useLoaderData, useNavigation } from "react-router";
import { listProducts } from "~/data/products.server";
export async function loader({ request }: { request: Request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q") ?? "";
const products = await listProducts(q);
return { q, products };
}
export default function ProductsRoute() {
const { q, products } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const searching = navigation.location?.pathname === "/products";
return (
<main>
<title>Products | Claude Code Shop</title>
<meta
name="description"
content="Browse Claude Code workflow products and team enablement kits."
/>
<h1>Products</h1>
<Form method="get" role="search">
<label>
Search
<input name="q" defaultValue={q} placeholder="workflow" />
</label>
<button type="submit">{searching ? "Searching..." : "Search"}</button>
</Form>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link to={`/products/${product.id}`}>{product.name}</Link>
<p>{product.description}</p>
<strong>{product.price.toLocaleString()} JPY</strong>
</li>
))}
</ul>
</main>
);
}
Notice the SEO choice. React Router route module docs recommend React 19’s built-in <title> and <meta> elements for new code. Some existing apps still use the route meta export, and that is fine when the project already has that convention. For new work, matching the current docs keeps the code easier to explain during a Claude Code review.
Use case 2: process a form with an action
An action receives form submissions and other mutations. <Form> still works as a normal HTML form before JavaScript loads, then React Router enhances it after hydration. This is ideal for business forms because the no-JavaScript path, validation path, submitting state, and success state stay in one route.
// app/routes/contact.tsx
import { Form, useActionData, useNavigation } from "react-router";
import { saveLead } from "~/data/products.server";
type ActionData =
| { ok: true; leadId: string }
| { ok: false; errors: { email?: string; message?: string } };
export async function action({ request }: { request: Request }): Promise<ActionData> {
const formData = await request.formData();
const email = String(formData.get("email") ?? "").trim();
const message = String(formData.get("message") ?? "").trim();
const errors: { email?: string; message?: string } = {};
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = "Enter a valid email address.";
}
if (message.length < 20) {
errors.message = "Tell us at least 20 characters about your situation.";
}
if (Object.keys(errors).length > 0) {
return { ok: false, errors };
}
const lead = await saveLead({ email, message });
return { ok: true, leadId: lead.id };
}
export default function ContactRoute() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const submitting = navigation.state === "submitting";
return (
<main>
<title>Contact | Claude Code Shop</title>
<meta
name="description"
content="Ask about Claude Code workflow products and team training."
/>
<h1>Contact</h1>
<Form method="post">
<label>
Email
<input name="email" type="email" aria-describedby="email-error" />
</label>
{actionData && !actionData.ok && actionData.errors.email ? (
<p id="email-error">{actionData.errors.email}</p>
) : null}
<label>
Message
<textarea name="message" rows={6} aria-describedby="message-error" />
</label>
{actionData && !actionData.ok && actionData.errors.message ? (
<p id="message-error">{actionData.errors.message}</p>
) : null}
<button type="submit" disabled={submitting}>
{submitting ? "Sending..." : "Send"}
</button>
</Form>
{actionData?.ok ? <p>Thanks. Lead ID: {actionData.leadId}</p> : null}
</main>
);
}
In a real app you would add CSRF protection, rate limiting, spam checks, email delivery, and CRM integration. The important prompt detail is to ask for those concerns explicitly. “Build a contact form” is too vague. “Use an action, validate on the server, show field errors, disable the button while submitting, and return a success message” is much more reliable.
Use case 3: keep failures inside an error boundary
Route-level error boundaries are one of the most useful Remix-style features. If a product does not exist, the product route can show a local 404 without crashing the entire app. If an unexpected error happens, the boundary can avoid leaking stack traces or secrets.
// app/routes/products.$productId.tsx
import { data, isRouteErrorResponse, Link, useLoaderData } from "react-router";
import { getProduct } from "~/data/products.server";
export async function loader({ params }: { params: { productId?: string } }) {
const productId = params.productId;
if (!productId) {
throw data("Missing product id", { status: 400 });
}
const product = await getProduct(productId);
if (!product) {
throw data("Product not found", { status: 404 });
}
return { product };
}
export default function ProductDetailRoute() {
const { product } = useLoaderData<typeof loader>();
return (
<main>
<title>{product.name} | Claude Code Shop</title>
<meta name="description" content={product.description} />
<h1>{product.name}</h1>
<p>{product.description}</p>
<strong>{product.price.toLocaleString()} JPY</strong>
<p>
<Link to="/contact">Ask about this product</Link>
</p>
</main>
);
}
export function ErrorBoundary({ error }: { error: unknown }) {
if (isRouteErrorResponse(error)) {
return (
<main>
<h1>{error.status === 404 ? "Product not found" : "Could not load product"}</h1>
<p>{error.data}</p>
<Link to="/products">Back to products</Link>
</main>
);
}
return (
<main>
<h1>Unexpected error</h1>
<p>Please try again later.</p>
</main>
);
}
When Claude Code writes this kind of boundary, review the output for two things. First, expected errors such as 404 and missing parameters should produce useful copy. Second, unexpected errors should not expose error.stack, environment variables, raw database messages, or internal URLs to the browser.
Use case 4: include SEO in route design
SEO should not be a final polish task for Remix-style apps. Route files already know the page purpose, URL shape, data source, and user action. That is exactly where title and description decisions belong. Product listing pages should explain what can be compared. Detail pages should start with the product name. Contact pages should say what kind of help the visitor can request.
| Route | Title pattern | Description pattern |
|---|---|---|
/products | Make the list intent obvious | Explain what the user can compare |
/products/:productId | Put the product name first | Summarize who the product helps |
/contact | Make consultation or contact clear | State what kind of support is available |
For a Claude Code prompt, add constraints such as “keep titles around 50-60 characters”, “keep descriptions under 120 characters”, “avoid duplicate titles”, and “use beginner search intent”. Internal links also matter. A Remix article can naturally point readers to React development with Claude Code, API development with Claude Code, and error handling patterns.
Claude Code review prompt
Paste this prompt when you want a review instead of another pile of generated code.
Review this React Router v7 Framework Mode route module.
Context:
- We want a Remix-style route design: loader/action/UI/ErrorBoundary close together.
- loader should only read data needed for rendering.
- action should handle form validation and mutations.
- For new React 19 code, prefer route-local <title> and <meta> where appropriate.
Please check:
1. Whether the loader returns secrets, internal fields, or oversized data.
2. Whether the action validates input, shows field errors, handles submitting state, and returns success UI.
3. Whether ErrorBoundary separates 404/400/unknown errors and avoids leaking stack traces.
4. Whether the form still works as a basic HTML form.
5. Whether title and description are unique and search-friendly.
6. Whether old Remix v2 imports and React Router v7 imports are mixed accidentally.
Output:
- List findings by severity.
- Propose the smallest safe diff.
- End with the checks I should run, such as npm run typecheck and npm run build.
Common mistakes
The first mistake is mixing old and new imports without noticing. Existing Remix v2 code may use @remix-run/react and @remix-run/node. New React Router v7 Framework Mode examples usually import runtime APIs from react-router and route config helpers from @react-router/dev/routes. During migration, follow the existing app. For new work, follow the current React Router docs.
The second mistake is returning too much from a loader. If the browser does not need cost price, internal notes, admin flags, or private user fields, do not return them. Server code can see private data, but anything returned by the loader is part of the client-facing contract.
The third mistake is trusting client validation. type="email" and required are useful UI hints, not security controls. The server action must validate every important field again.
The fourth mistake is making the error boundary either too vague or too revealing. Users need a useful next step; attackers do not need stack traces. Claude Code often writes quick debug output unless the prompt tells it not to.
The fifth mistake is treating SEO as automatic. React Router docs explain details around route metadata and React’s built-in meta elements. Whatever convention your app uses, each route should still have a unique title, a concise description, and sensible internal links.
The sixth mistake is forgetting revalidation after actions. After a form action, loader data may be revalidated. That is helpful for fresh data, but expensive loaders can make a simple form feel slow. Review whether shouldRevalidate is needed after the first version works.
Turning the article into a monetization path
Remix and React Router topics are good monetization entry points because they sit near actual business value. A reader who needs forms, SEO, data loading, errors, and deployment is often closer to a real project than someone reading only a syntax tip. That makes the article a useful bridge to implementation templates, team training, and consulting.
For individual practice, start with the free cheat sheet and use the review prompt above on one route. For teams that need React Router v7, Remix v2 maintenance, form design, and Claude Code review workflows, the next step is Claude Code training and implementation consulting. If you want reusable prompts and checklists, browse the product library.
What happened when Masa tried this
Masa tested the product list, product detail 404, contact form validation, and success state in a small React Router v7 project. A vague “build this in Remix” prompt produced mixed imports and a hand-written client fetch flow. The better prompt named Framework Mode, route modules, loader, action, ErrorBoundary, and route-local title/meta. That produced smaller diffs and a reviewable structure. The main lesson is that Claude Code becomes much more useful when the route responsibility is fixed before code generation starts.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.