Consistency is crucial for developing reliable Next.js applications. A clear file organization, reusable component patterns, and a well-structured boilerplate not only streamline development but also ensure that projects remain maintainable. The real challenge, however, is enforcing these conventions as the team and codebase grow.
Cursor’s project rules provide an automated system for upholding development standards. Instead of relying on documentation or the memory of individual developers, these rules can be integrated directly into the repository, guaranteeing that every contribution aligns with the team’s conventions.
This guide will explain how project rules work in Cursor, outline the steps for setting them up in a Next.js project, and provide practical examples that cover pages, components, and API routes. Additionally, it will highlight the long-term benefits of implementing project rules, especially when scaling and maintaining larger Next.js applications, whether you’re deploying with Galaxy or any other hosting platform.
Understanding Project Rules in Cursor
Project rules in Cursor function as programmable guardrails that the editor evaluates before generating or modifying code. They act as embedded constraints around naming conventions, folder organization, import paths, styling choices, and more. Because these rules live directly in the repository, they are versioned with the project and automatically enforced across the team, which removes the need to rely solely on documentation or memory.
Cursor applies rules hierarchically: you can define global rules that affect every project, alongside project-specific rules scoped to a particular codebase. Each rule is written in a format that can include conditions such as file-type patterns or “always apply” flags, ensuring that the right standards are applied only when relevant. For example, enforcing TypeScript rules in .tsx files or structuring only the api/ directory.
Configuring Project Rules for Your Workspace
Rules are stored in the .cursor/rules directory at the root of your repository. Each rule is defined in a .md file, written in plain language and, when helpful, accompanied by code snippets. Since these files are tracked in source control, they become part of the project’s single source of truth and evolve alongside the codebase.
This ensures consistency across local development and production environments, whether you’re hosting on Vercel, deploying to your own servers, or scaling with Galaxy.
For instance, if you want to enforce that all pages use the Next.js `app` Router, you could add a rule in .cursor/rules/pages.md.
All newly created pages should follow these guidelines:
- Must be server components (do not include `use client`).
- Should reside within the app/ directory.
- Must use a default export for the page component.
- Styles should be imported using CSS modules or Tailwind, avoiding global CSS.
Now, whenever you instruct Cursor to generate a page, it will automatically create server components and place them in the app/ directory, avoiding client components and the legacy pages/ folder.
Enforcing Standards for Components
In larger projects, maintaining consistent component patterns is crucial. For instance, it’s important that every component within the components/ directory adheres to specific conventions:
- Filenames should use PascalCase
- TypeScript prop interfaces should be declared and exported from the same file
- Components should utilize named exports rather than default exports.
These guidelines can be enforced by creating a definition in `.cursor/rules/components.md`.
The code snippet below shows an example of a component generated by Cursor after enforcing the component conventions.
// components/Navbar.tsx
import React from "react";
export interface NavbarProps {
title: string;
links?: { label: string; href: string }[];
}
export function Navbar({ title, links = [] }: NavbarProps): JSX.Element {
return (
<nav className="flex items-center justify-between p-4 border-b border-gray-200">
<h1 className="text-lg font-bold">{title}</h1>
<ul className="flex space-x-4">
{links.map((link) => (
<li key={link.href}>
<a href={link.href} className="text-sm text-gray-700 hover:underline">
{link.label}
</a>
</li>
))}
</ul>
</nav>
);
}The code snippet demonstrates how conventions for naming, props, typing, and exports are automatically enforced to generate a clean, consistent component.
Automating API Route Boilerplate Code
API routes can often involve repetitive boilerplate code, making it essential to establish clear best practices. All routes should be organized under the directory app/api/{route}/route.ts. Each file should export functions that correspond to the supported HTTP methods, such as GET and POST. It’s important to validate every request using Zod to ensure data integrity. Additionally, all responses should return JSON along with the appropriate status codes.
A generated implementation of an API route is shown below. It illustrates how rules for file placement, method exports, request validation, and structured responses come together to form a clean and reliable endpoint:
// app/api/users/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const parsed = userSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.format() },
{ status: 400 }
);
}
// Simulated success response (replace with DB call, etc.)
return NextResponse.json(
{ message: "User created successfully", data: parsed.data },
{ status: 201 }
);
} catch (err) {
return NextResponse.json(
{ error: "Unexpected server error" },
{ status: 500 }
);
}
}Instead of manually building everything, developers are provided with a validated and consistent API route by default.
Integrating Rules into Next.js Development
Organizing Files: App Router vs Pages Router
Rules help enforce consistency by ensuring that the legacy `pages/` router is never mixed with the modern app directory. All routes must reside within the app/ directory, and the pages/ folder should not be used at all. Additionally, each route directory is required to include a page.tsx entry point to maintain a clear and predictable structure.
The snippet below illustrates how Cursor generates a route that follows established routing rules. By placing the file in the app/dashboard/ directory and naming it page.tsx, this approach ensures compatibility with the Next.js App Router while maintaining a clean and predictable structure:
// app/dashboard/page.tsx
export default function DashboardPage() {
return (
<section className="p-6">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-gray-600">Welcome to your dashboard.</p>
</section>
);
}Unifying Imports and API Request Patterns
To keep your codebase clean and predictable, you can enforce consistent import practices and centralize how API calls are made. Cursor rules make this possible by locking in absolute imports and requiring a shared helper for requests.
All imports should utilize the `@/` alias defined in `tsconfig.json` instead of long relative paths. Relative imports, such as `../../components/…`, are not allowed. Additionally, every API call must be routed through the `lib/api.ts` helper. This ensures that requests are consistent and easier to maintain throughout the project.
The snippet below illustrates how a component generated under these rules looks in practice:
// components/UserList.tsx
import { get } from "@/lib/api";
interface User {
id: string;
name: string;
}
export async function UserList() {
const users: User[] = await get("/api/users");
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Centralizing API calls in a shared helper keeps your codebase clean and consistent. Instead of scattering `fetch` logic across multiple components, all requests go through a single utility, making it easier to add features like error handling, authentication headers, or logging later. Below is an example of how the lib/api.ts file can define a reusable `get` function.
// lib/api.ts
export async function get<T = unknown>(url: string): Promise<T> {
const res = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);
}
return res.json() as Promise<T>;
}Applying TypeScript and ESLint Consistency
By embedding expectations directly into your project, you can ensure that every generated file stays consistent with your standards. For example, you might require all new files to use either .ts or .tsx extensions, enforce that props are always defined with explicit TypeScript interfaces or type aliases, and mandate that return types are declared for all functions. On top of that, the ESLint configuration in .eslintrc.json must be respected, guaranteeing that both style and quality checks remain uniform across the entire codebase.
Clear TypeScript and ESLint rules help keep components predictable and easy to maintain. Instead of leaving typing optional or relying on implicit return values, Cursor enforces conventions such as explicit prop definitions, declared return types, and consistent styling. The following `Button` component demonstrates these rules in action:
// components/Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
}
export function Button({ label, onClick }: ButtonProps): JSX.Element {
return (
<button
onClick={onClick}
className="rounded-md px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 transition-colors">
{label}
</button>
);
}Rule Example: Structuring API Routes
To ensure a consistent API design, you should define route rules in the .cursor/rules/api.md file. These rules require that all routes are structured within app/api/{name}/route.ts, with Zod being used for request validation to ensure data integrity. Responses must always be in JSON format and include the appropriate status codes. Each handler should be written as an asynchronous function with a clearly defined return type. This approach keeps your API predictable, strongly typed, and easy to scale.
With API route rules in place, every endpoint follows the same structure for validation, typing, and responses. The example below shows a `POST` route for creating posts, where Zod ensures the request body is valid before returning a structured JSON response:
// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
const postSchema = z.object({
title: z.string().min(1, "Title is required"),
content: z.string().min(1, "Content is required"),
});
export async function POST(request: Request): Promise<Response> {
try {
const body = await request.json();
const parsed = postSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid input", details: parsed.error.format() },
{ status: 400 }
);
}
// Simulate saving the post
return NextResponse.json(
{ message: "Post created", data: parsed.data },
{ status: 201 }
);
} catch {
return NextResponse.json(
{ error: "Something went wrong" },
{ status: 500 }
);
}
}Rule Example: Organizing Pages and Layouts
To keep the application structure consistent, you can enforce that every route directory includes both a page.tsx and a layout.tsx file. The layout component should always wrap its children in a `<section>` element, using Tailwind classes to handle spacing and ensure a uniform look across the project. This approach makes it easier to maintain predictable layouts and styling as the application grows.
To maintain consistency in structure and styling across all routes, it is essential to establish clear rules for pages and layouts. Each route directory should include a `page.tsx` file for the content and a `layout.tsx` file to provide a uniform wrapper that ensures proper spacing and alignment. Below is an example implementation for a profile section:
// app/profile/page.tsx
export default function ProfilePage() {
return <h1 className="text-xl font-semibold">User Profile</h1>;
}
// app/profile/layout.tsx
export default function ProfileLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-sm">
{children}
</section>
);
}Rule Example: Consistent Component Styling
When using Tailwind, it’s important to establish consistent styling practices by defining clear rules for your components. All styling should be managed with Tailwind utility classes to prevent reliance on inline styles or extensive global CSS imports. Additionally, it’s essential to use semantic HTML elements for your components to ensure both accessibility and clean markup.
Styling rules ensure that every component follows the same design language across the application. With Tailwind enforced as the styling convention, components like cards can remain lightweight, reusable, and visually consistent without relying on inline styles or global CSS. The example below shows a `Card` component that aligns with these expectations:
// components/Card.tsx
interface CardProps {
title: string;
children: React.ReactNode;
}
export function Card({ title, children }: CardProps): JSX.Element {
return (
<article className="rounded-xl shadow-sm p-6 border border-gray-200 bg-white">
<h2 className="text-lg font-semibold mb-3 text-gray-800">{title}</h2>
<div className="text-gray-600">{children}</div>
</article>
);
}Conclusion
Cursor’s project rules convert team conventions into enforceable guidelines, ensuring consistency across Next.js projects from the very beginning. By integrating these standards into the development process, teams can prevent drift and minimize repetitive setup. The result is cleaner code, quicker collaboration, and a more scalable foundation for growth. And when it’s time to take your Next.js app to production, platforms like Galaxy make it easy to deploy and scale confidently.