Remix Cursor Rules: React Web Framework Guide

Cursor rules for Remix development covering nested routes, loader/action patterns, form handling, error boundaries, and server-side rendering with enhanced UX.

July 22, 2025by PromptGenius Team
remixcursor-rulesreactssrweb-framework

Overview

Remix is a full-stack React framework that emphasizes web fundamentals, nested routing, and progressive enhancement for superior user experiences. These cursor rules enforce strict loader/action data flow, robust form handling, and error boundary patterns to help AI assistants generate fast, resilient web applications. Whether you're handling complex mutations or optimizing server-side data loading, these rules ensure your application remains highly performant and accessible without compromising UX.

Note:

Enforces loader/action patterns, form handling conventions, error boundaries, and server-side rendering best practices.

Rules Configuration

---
description: Enforces best practices for Remix development, focusing on loader/action data flow, form handling, error boundaries, and progressive enhancement. Provides comprehensive guidelines for writing clean, resilient full-stack React applications with proper context.
globs: **/*.{js,jsx,ts,tsx}
---
# Remix Best Practices

You are an expert in Remix development and related web technologies.
You understand modern Remix development practices, architectural patterns, and the importance of providing complete context in code generation.

### Context-Aware Code Generation
- Provide route context including params, loader data types, and URL state
- Include relevant configs (remix.config.js, tsconfig.json) when scaffolding
- Generate complete loader/action signatures with typed params and responses
- Document route hierarchy and parent/child data dependencies

### Loader & Action Patterns
- Use loaders for GET requests to fetch and return data server-side
- Use actions for POST/PUT/DELETE mutations with form data
- Return typed JSON responses using json() utility from @remix-run/node
- Use useLoaderData and useActionData for type-safe access in components
- Implement resource routes for API endpoints and file downloads

### Form Handling & Progressive Enhancement
- Use `<Form>` over native `<form>` for JavaScript-enhanced submissions
- Validate and sanitize inputs inside action functions, not client-side
- Return validation errors from action and display via useActionData
- Use useNavigation for loading states and optimistic UI
- Support JavaScript-disabled clients with proper form fallbacks

### Routing & Nested Layouts
- Use file-based routing in /app/routes with flat or nested folder structure
- Leverage parent loaders to avoid redundant data fetching in child routes
- Use `<Outlet>` for nested layout rendering with persistent parent UI
- Implement layout routes with _layout convention for shared shells

### Error Boundaries & Best Practices
- Use ErrorBoundary for 500-level server errors per route
- Use CatchBoundary for thrown Response objects (404, 401)
- Provide fallback UI for each route segment to isolate failures
- Use TypeScript throughout for type-safe loader and action data
- Prefer server-side logic for data fetching; keep client for interactivity
- Add type-safe form validation with zod schemas in actions
- Include proper error handling in all loaders and actions

Installation

Create remix.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.

Examples

// app/routes/posts.$id.tsx — Loader and action with typed responses
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"
import { json } from "@remix-run/node"
import { useLoaderData, Form } from "@remix-run/react"

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id)
  if (!post) throw new Response("Not Found", { status: 404 })
  return json({ post })
}

export async function action({ request, params }: ActionFunctionArgs) {
  const formData = await request.formData()
  const title = formData.get("title")
  await updatePost(params.id, { title })
  return json({ success: true })
}

export default function Post() {
  const { post } = useLoaderData<typeof loader>()
  return (
    <Form method="post">
      <input name="title" defaultValue={post.title} />
      <button type="submit">Save</button>
    </Form>
  )
}