Actionable coding standards for building full-stack Remix applications with TypeScript and Prisma.
Building full-stack applications with Remix, TypeScript, and Prisma shouldn't feel like juggling three different worlds. You know the drill: fighting type mismatches between your database schema and route handlers, wrestling with loader/action patterns that break at runtime, and spending hours debugging edge cases that proper validation would have caught upfront.
Every Remix developer hits these productivity killers:
Database-to-UI Type Gaps: Your Prisma schema generates perfect types, but they don't flow cleanly through your loaders to your components. You end up with any types, runtime errors, and constant manual type assertions.
Loader/Action Inconsistency: Without clear patterns, your data fetching becomes a mess of mixed concerns—business logic scattered between loaders and components, error handling that breaks under edge cases, and mutations that don't follow consistent validation patterns.
Development Velocity Bottlenecks: Every feature requires context-switching between database queries, route logic, component props, and error boundaries. No standardized approach means reinventing patterns for each new route.
Production Reliability Issues: Inconsistent error handling, missing input validation, and performance anti-patterns that only surface under load.
These Cursor Rules establish a complete development pipeline that eliminates type boundaries and context-switching. Instead of managing three separate mental models, you get one cohesive workflow that flows from database schema to UI components.
The rules create automatic type flow: Prisma schema → Zod validation → TypeScript interfaces → React components. Your database changes propagate through your entire application with compile-time safety.
// Your loader automatically gets proper typing
export const loader: LoaderFunction = async ({ params }) => {
const user = await db.user.findUnique({
where: { id: params.userId },
select: { id: true, email: true, profile: true } // Exact selection, no over-fetching
});
if (!user) throw new NotFoundError("User not found");
return json(user); // Type-safe JSON response
};
// Component automatically receives correct types
export default function UserProfile() {
const user = useLoaderData<typeof loader>(); // Perfect type inference
// user.email is string, user.profile is Profile | null - no manual typing needed
}
Eliminate Type-Related Debugging: Catch mismatches at compile time instead of discovering them in production. The rules enforce strict TypeScript configuration with proper utility type usage, so your IDE catches problems before they become runtime errors.
Standardized Error Handling: Never write another inconsistent error handler. The domain error system gives you AuthError, NotFoundError, and ValidationError classes that automatically map to proper HTTP status codes and user-friendly messages.
Zero-Config Input Validation: Every action gets automatic Zod validation at the entry point. Form data, JSON payloads, and URL parameters all get validated and typed in one step:
export const action: ActionFunction = async ({ request }) => {
const schema = z.object({
email: z.string().email(),
age: z.number().int().positive()
});
const { email, age } = schema.parse(await request.json());
// email and age are now properly typed AND validated
};
Performance by Default: The rules build in performance patterns—selective Prisma queries, proper caching headers, streaming for large datasets, and bundle optimization strategies that keep your app fast without extra configuration.
Before: Building a user profile page meant writing separate types for your Prisma query, your loader return value, your component props, and your error states. Each piece required manual coordination and frequent runtime debugging.
After: Define your Prisma selection, and the entire type chain flows automatically. Your component knows exactly what data shape to expect, your error boundaries handle the right error types, and your forms validate with the same schema used server-side.
Before: Adding form validation required choosing between client-side and server-side validation, then manually keeping them in sync and handling the different error formats.
After: Write one Zod schema that handles parsing, validation, and type generation for both client and server. Form errors automatically map to the right UI states.
Before: Database changes broke your application in unpredictable ways, requiring careful manual updates across multiple files.
After: Prisma schema changes flow through your TypeScript compilation, catching breaking changes immediately and guiding you through necessary updates.
Initialize your development environment with the proper toolchain:
npx create-remix@latest --template typescript
cd your-remix-app
npm install prisma @prisma/client zod
npm install -D vitest @playwright/test
Update your tsconfig.json with the required strict settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true
}
}
Create your global Prisma client in app/utils/db.server.ts:
import { PrismaClient } from '@prisma/client';
let db: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
if (process.env.NODE_ENV === 'production') {
db = new PrismaClient();
} else {
if (!global.__db) {
global.__db = new PrismaClient();
}
db = global.__db;
}
export { db };
Establish your error handling foundation in app/utils/errors.server.ts:
export class ServerError extends Error {
constructor(
public status: number,
public data: unknown,
message?: string
) {
super(message);
}
}
export class NotFoundError extends ServerError {
constructor(message = "Resource not found") {
super(404, { message }, message);
}
}
export class ValidationError extends ServerError {
constructor(issues: any[]) {
super(400, { issues }, "Validation failed");
}
}
Copy the provided rules into your .cursorrules file in your project root. The rules will immediately start guiding your development patterns.
Create a route that demonstrates the full type flow:
// app/routes/users.$userId.tsx
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { z } from "zod";
import { db } from "~/utils/db.server";
import { NotFoundError } from "~/utils/errors.server";
const ParamsSchema = z.object({
userId: z.string().uuid()
});
export const loader: LoaderFunction = async ({ params }) => {
const { userId } = ParamsSchema.parse(params);
const user = await db.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
createdAt: true
}
});
if (!user) throw new NotFoundError();
return json(user);
};
export default function UserDetail() {
const user = useLoaderData<typeof loader>();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Development Speed: Eliminate 60-80% of type-related debugging time. Features that previously required careful coordination across multiple files now flow automatically through the type system.
Code Quality: Achieve 90%+ test coverage with patterns that make testing natural. The functional, declarative patterns promoted by these rules create code that's inherently more testable.
Production Stability: Catch edge cases during development instead of production. The strict validation and error handling patterns prevent the runtime errors that commonly slip through traditional development workflows.
Team Consistency: New team members follow the same patterns automatically. The rules establish conventions that make code reviews focus on business logic instead of implementation inconsistencies.
Refactoring Confidence: Database schema changes and API modifications become safe operations guided by TypeScript's compiler. Breaking changes surface immediately instead of hiding until runtime.
These aren't just coding standards—they're a complete development methodology that transforms how you build full-stack applications. The rules eliminate the cognitive overhead of managing multiple type systems and give you a single, coherent development experience from database to UI.
Your Remix applications become more maintainable, your development velocity increases, and your production deployments become predictable. The type safety isn't just theoretical—it directly translates to fewer bugs, faster feature development, and more reliable software.
You are an expert in Remix, TypeScript, React, Prisma, Zod, Jest/Playwright, TailwindCSS.
Key Principles
- Prefer end-to-end type safety: TypeScript → Zod → Prisma.
- Keep data-loading logic in Remix loaders; write mutations exclusively in actions.
- Separate server-only code from browser bundles with `import type` and `isBrowser` guards.
- Optimize for the network: small chunks, streaming, edge deployment where possible.
- Favor functional, declarative patterns; avoid class components and OOP repositories.
- Follow the "happy-path last" style—handle errors/edge cases first, return quickly.
- Commit early & often with Prettier + ESLint/TypeScript strict checks blocking CI.
- All new code must be unit- and integration-tested (Vitest/Jest, Playwright for e2e).
- Directory names: lowercase-dash (`app/routes`, `app/components`, `app/utils`).
TypeScript
- `tsconfig.json` must enable: `strict`, `noImplicitAny`, `strictNullChecks`, `exactOptionalPropertyTypes`, `importsNotUsedAsValues`, `useUnknownInCatchVariables`.
- Never use `any`; use `unknown` + explicit narrows, or `satisfies` operator.
- Use `type` aliases for data shapes, `interface` for public contracts (e.g., React props) that may be merged.
- Use utility types (`ReturnType<typeof loader>`) to infer data instead of duplicating types.
- Inline generics when simple, otherwise extract (`type LoaderData<T> = {...}`).
- Favor readonly arrays/tuples (`readonly string[]`) for immutability.
- Export exactly one thing per file unless grouping is clearer (e.g., constants).
Error Handling & Validation
- Define a `ServerError` base class with `{status: number; data: unknown}`; extend for domain errors (`AuthError`, `NotFoundError`).
- Throw domain errors in loaders/actions; handle in route-level `CatchBoundary`.
- Use `json` helper with explicit status codes: `return json<LoaderData>(data, {status: 201})`.
- Validate all untrusted input with Zod schemas at the start of actions. Coerce → parse → narrow.
```ts
const schema = z.object({ email: z.string().email(), age: z.number().int().positive() });
const { email, age } = schema.parse(await request.json());
```
- Wrap Prisma calls with `try/catch`; convert known errors: `if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') throw new DuplicateError(...)`.
- Use `useRouteError` in UI to differentiate `Error` vs `ServerError` vs Remix `CatchResponse`.
Remix Framework Rules
- Route files live under `app/routes`. Use nested folders for segments: `app/routes/admin/users.$userId.tsx`.
- Loader signature: `export const loader: LoaderFunction = async ({ request, params, context }) => {}`.
- Action signature: `export const action: ActionFunction = async ({ request }) => {}`.
- Return typed data: `type LoaderData = Awaited<ReturnType<typeof loader>>`.
- Use `defer` for streaming large payloads and `Suspense` on the client.
- For client-only utilities, prefix file with `browser.`: `utils/browser.local-storage.ts`.
- Keep meta functions colocated: `export const meta: MetaFunction = ({ data }) => [...]`.
- Cache critical resources via `headers` helper: `export const headers = () => ({ 'Cache-Control': 'max-age=60' });`.
- Implement `links` for style imports; use Tailwind JIT build to minimize CSS.
- Prefer resource routes (`app/routes/api/user.ts`) for pure JSON endpoints.
- Add a root `ErrorBoundary` to log with Sentry/NewRelic; never display raw stack traces to users.
Prisma Rules
- Instantiate a single `PrismaClient` (global for dev, per Lambda in prod). Example global:
```ts
import { PrismaClient } from '@prisma/client';
let db: PrismaClient;
if (!global.__db) {
global.__db = new PrismaClient();
}
db = global.__db;
export { db };
```
- Enable `referentialActions = "prisma"` & strict schema constraints.
- Use `select`/`include` to limit columns; never `await db.user.findMany()` without selection.
- Wrap multi-step changes in `db.$transaction([...])` to keep atomic.
- Prefer soft deletes using `deletedAt` + partial indexes over hard deletes.
- Write idempotent seed scripts in `prisma/seed.ts`; run via `npm run prisma:seed` in CI.
- Generate Prisma client with `npx prisma generate --data-proxy` when targeting Data Proxy.
Testing
- Unit: Vitest for pure TS logic, jest-like API. Example:
```ts
test('slugify trims', () => {
expect(slugify(' Hello')).toBe('hello');
});
```
- Loader/action integration: Remix `createRequestHandler` + mocked context.
- e2e: Playwright in headless Chromium; seed DB with fixture factories; run against `npm run dev`.
- Achieve ≥90% statement & branch coverage; fail CI below threshold.
Performance
- Set performance budgets: JS < 200 KB, CSS < 100 KB gzipped.
- Use `preload` link headers in loader for critical assets.
- Use incremental cache revalidation with `stale-while-revalidate` where safe.
- Prefer `edge` runtime on Vercel/Fly for latency-sensitive routes.
- Analyze route bundles with `npm run analyze` (Remix built-in) & prune shared imports.
Security
- Enforce HTTPS; set `Strict-Transport-Security` header in `loader` root.
- Use `helmet` via Remix middleware to add CSP, frameguard.
- Sanitize user HTML content with `DOMPurify`; never `dangerouslySetInnerHTML` raw.
- Rotate Prisma database credentials; use `.env` only for dev.
- All cookies HTTP-Only, Secure, SameSite=lax; sign with `createCookie` secret.
File/Folder Conventions
- `app/`
• `components/` – reusable UI (tsx)
• `routes/` – Remix route files
• `utils/` – pure helpers (ts)
• `services/` – integration boundaries (db, external APIs)
• `styles/` – Tailwind imports or vanilla extract
• `tests/` – colocated `*.test.ts` & e2e specs
- Static asset paths use kebab-case (`/images/user-avatar.png`).
Commit & CI Pipeline
- Husky pre-commit: `lint`, `type-check`, `test`.
- GitHub Action matrix: Node 18/20, `pnpm install --frozen-lockfile`, `npm run build`.
- Automatic Prisma migration in `preview` environment; blocked on prod promotion until manual review.
Documentation
- Every exported helper/function/component must have JSDoc. Describe params, return, thrown errors.
- Keep architecture decision records in `/docs/adr/*`.
- Update README with newly added scripts, env variables, or breaking changes.