Cursor Rules for TypeScript Next.js 2026: 5 Rules That Actually Change Your Output
If you're building with TypeScript and Next.js 15 in 2026, you already know Cursor can generate code fast. The problem is it generates the wrong code fast — any types everywhere, Pages Router patterns in App Router projects, placeholder TODOs that become production bugs.
The fix isn't more prompting. It's writing .cursorrules (or .cursor/rules/*.mdc files) that are specific to your TypeScript + Next.js stack.
Here are 5 rules I use daily, with before/after examples showing exactly what changes.
Rule 1: Ban any — Force Real Types
This is the single most impactful rule for TypeScript projects:
Never use the `any` type. Use `unknown` for truly unknown values,
then narrow with type guards. For API responses, define explicit
interfaces. For event handlers, use React's built-in event types.
Without this rule, Cursor defaults to any constantly:
// Cursor's default output
export async function getProducts(category: any) {
const res = await fetch(`/api/products?cat=${category}`);
const data: any = await res.json();
return data.products.map((p: any) => ({
name: p.name,
price: p.price,
}));
}
Three any types. Zero type safety. Bugs will hide here for weeks.
With this rule, Cursor generates typed code:
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface ProductsResponse {
products: Product[];
}
export async function getProducts(category: string): Promise<Product[]> {
const res = await fetch(`/api/products?cat=${encodeURIComponent(category)}`);
const data: ProductsResponse = await res.json();
return data.products;
}
Every value has a type. Your editor catches mistakes before they reach production.
Rule 2: Enforce App Router Conventions
Cursor's training data includes years of Pages Router code. Without explicit rules, it mixes paradigms:
This project uses Next.js 15 App Router exclusively.
Use server components by default. Only add "use client" when
the component needs useState, useEffect, or event handlers.
Use the file conventions: page.tsx, layout.tsx, loading.tsx,
error.tsx, not-found.tsx. Never use getServerSideProps or
getStaticProps — use async server components and fetch() instead.
Without this rule:
// Cursor generates Pages Router patterns
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async () => {
const products = await fetch('https://api.example.com/products');
return { props: { products: await products.json() } };
};
export default function ProductsPage({ products }) {
return <ProductList items={products} />;
}
This won't even work in an App Router project.
With this rule:
// Correct App Router server component
import { ProductList } from '@/components/product-list';
export default async function ProductsPage() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
return <ProductList items={products} />;
}
No "use client" needed. No getServerSideProps. The component itself is async — that's how App Router works.
Rule 3: Kill Placeholder TODOs
This rule pays for itself in the first hour:
Never write placeholder comments like "// TODO: implement",
"// Add logic here", or "// Handle error". Implement the actual
logic. If you need information to complete the implementation,
ask me what's needed instead of leaving a stub.
Without this rule:
export async function createCheckoutSession(userId: string, priceId: string) {
// TODO: Initialize Stripe
// TODO: Create session
// TODO: Handle errors
return { url: '/checkout' };
}
This function does nothing. It returns a hardcoded string. It will pass every test that doesn't check behavior.
With this rule:
export async function createCheckoutSession(userId: string, priceId: string) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const session = await stripe.checkout.sessions.create({
customer: userId,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
});
return { url: session.url };
}
Real code. Real implementation. No follow-up needed.
Rule 4: Async/Await Error Handling Pattern
Next.js 15 server components and route handlers are async by nature. This rule ensures errors don't vanish silently:
For all async operations in server components and route handlers:
wrap in try/catch, return typed error objects (never throw raw strings),
and log errors with context: console.error('[FunctionName]', error).
In route handlers, always return proper Response objects with status codes.
Without this rule:
// app/api/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return Response.json(users);
}
One database error and this returns a 500 with no useful information.
With this rule:
// app/api/users/route.ts
export async function GET() {
try {
const users = await db.user.findMany();
return Response.json(users);
} catch (error) {
console.error('[GET /api/users]', error);
return Response.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
Structured error handling. Useful logs. Proper HTTP status codes.
Rule 5: Use loading.tsx and error.tsx Instead of Manual States
This is an App Router-specific pattern that Cursor almost never generates on its own:
For data loading states, create loading.tsx files in the route
directory instead of managing loading state with useState.
For error handling, create error.tsx files (with "use client")
instead of wrapping everything in try/catch in the UI.
Use Suspense boundaries for granular loading states within a page.
Without this rule:
'use client';
export default function DashboardPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/dashboard')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
if (error) return <ErrorMessage />;
return <Dashboard data={data} />;
}
Client component for no reason. Manual state management. No streaming.
With this rule, Cursor generates three files:
// app/dashboard/page.tsx — server component
export default async function DashboardPage() {
const data = await getDashboardData();
return <Dashboard data={data} />;
}
// app/dashboard/loading.tsx
export default function Loading() {
return <Spinner />;
}
// app/dashboard/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return <ErrorMessage error={error} onRetry={reset} />;
}
Server component. Streaming. Built-in error recovery. Zero useState.
Get the Full Rule Set
These 5 rules are a starting point. A production TypeScript + Next.js project needs rules for Zod validation, server actions, Prisma patterns, testing conventions, and more.
I've packaged 50+ battle-tested rules into two packs:
-
Cursor Rules Pack v2 — production-ready
.cursorrulesand.cursor/rules/*.mdcfiles for TypeScript, Next.js, React, Prisma, and more. -
CLAUDE.md Rules Pack — the same rule quality, formatted for Claude Code's
CLAUDE.mdconfiguration.
Both are organized by framework, priority, and use case — so your AI assistant generates the right code from the first prompt.
Top comments (0)