← Back to blog

From fragmented code to consistent output with AI rules

Most developers use AI without structured guidance. Here's how capability-specific rules change everything.

Roni Ström

Roni Ström

Founder

Most developers use AI without structured guidance. They type prompts, hope the output is usable, and spend time fixing what comes back. This works, but you're leaving a lot on the table.

I've spent the past year doing AI-assisted development. The single biggest productivity lever: investing in rules.

Every tool calls them something different. Cursor Rules. Claude Skills. Windsurf Rules. Copilot Instructions. Junie Guidelines. Same concept: persistent instructions that guide the AI across sessions.

The four maturity levels

How developers provide context to AI varies widely. Here's what I've observed:

AI Context Maturity

Level 1: No rules

This is most common. Developers interact through conversation, prompting back and forth until they get what they need. When they move to another task, everything resets. The AI doesn't know your architecture, your patterns, or your constraints. Each session starts from zero.

The consequence: fragmented codebases. Architecture decisions left to the AI. Inconsistent patterns across files. You're treating the AI as a creative partner when you want a code factory with low tolerances.

Level 2: Architecture docs as rules

Some developers add existing architecture documentation as context. The problem: architecture docs are written for humans to read. They explain concepts and rationale, not step-by-step instructions.

Human docs explain what and why. Machine instructions need how, with code samples.

Level 3: AGENTS.md files

More teams now create AGENTS.md files describing the tech stack, folder structure, key patterns, and common commands. These help, but they're too high-level:

AGENTS.md
 
Next.js 15 app with TypeScript, Tailwind CSS, and Supabase.
 
## Tech Stack
- Framework: Next.js 15 (App Router)
- Database: Supabase (PostgreSQL)
- Styling: Tailwind CSS
- Auth: Supabase Auth
 
## Folder Structure
- /app - Next.js app router pages
- /components - React components
- /lib - Utilities and helpers
 
## Commands
- pnpm dev - Start development server
- pnpm build - Build for production

This tells the AI about the project without giving it specific guidance to produce consistent, correct code. The AI still has to guess at patterns.

Level 4: Capability-specific rules

This is where productivity jumps. Each rule file focuses on one architecture capability:

Runtime Execution Capabilities

This example shows runtime architecture capabilities for a Next.js + Supabase web app. If you're building native mobile apps, backend services, or infrastructure code, your capabilities will look different.

I recommend creating separate rulesets for different architecture layers:

  • Runtime - what runs your app
  • Development - testing, building, linting, CI...
  • Operations - monitoring, logging, alerting...
  • Infrastructure - networking, security, containers...

Each layer has its own patterns.

The principle stays the same: focused rules for specific capabilities. Related capabilities can share a rule when they naturally go together.

Each rule file:

  • Focuses on a specific capability or related group of capabilities
  • Contains explicit instructions written for an LLM to follow
  • Includes actual code examples showing the exact patterns

Anatomy of a good rule

Rules have two parts: front matter and content.

Front matter controls when the rule loads. The globs field tells the AI when this rule is relevant. Form rules load when you're working on TSX files. Database rules load when touching the data layer. This saves context window for what matters right now.

Content gives the AI explicit instructions and code examples. Not prose explaining concepts. Actual patterns to follow.

Generic vs capability-specific rules

You need both. Generic rules for broad patterns, capability rules for specific workflows.

Generic rules cover broad concerns: language conventions, framework patterns, project structure, code style. Some always apply, others load for matching file types.

Here's a project structure rule that always applies:

.cursor/rules/project-structure.mdc
description: Project folder structure and organization
alwaysApply: true
---
 
# Project Structure
 
src/app/
├── (marketing)/              # Public pages group
│   ├── _components/          # Route-specific components
│   ├── blog/
│   │   ├── _components/
│   │   ├── [slug]/
│   │   │   └── page.tsx
│   │   └── page.tsx
│   ├── contact/
│   │   ├── _components/
│   │   ├── _lib/
│   │   │   └── server/
│   │   │       └── server-actions.ts
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx

├── (auth)/                   # Auth pages group
│   ├── sign-in/
│   │   └── page.tsx
│   ├── sign-up/
│   │   └── page.tsx
│   └── layout.tsx

├── (app)/                    # Logged-in user pages
│   ├── _components/
│   ├── _lib/
│   │   └── server/
│   │       └── workspace.loader.ts
│   ├── settings/
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
 
## Key Patterns
 
- `_components/` for route-specific components
- `_lib/` for route-specific utilities
- `_lib/server/` for server-side data loaders
- `(group)` for route groups without URL segments
- `[param]` for dynamic routes
 
## Special Files
 
- `layout.tsx` defines layouts
- `loading.tsx` defines loading states
- `error.tsx` handles errors
- `page.tsx` is the page component
- `route.ts` is an API route handler

No code examples needed. The AI just needs to know where to put things.

Here's a TypeScript rule that loads for matching files:

.cursor/rules/typescript.mdc
description: TypeScript conventions and patterns
globs: src/**/*.ts,src/**/*.tsx
alwaysApply: false
---
 
# TypeScript
 
- Write clean, clear, well-designed, explicit TypeScript
- Make sure types are validated strictly
- Use implicit type inference, unless impossible
- Consider using classes for server-side services, but export a function instead of the class
 
// service.ts
class OrderService {
  getOrder(id: number) {
    // ... implementation ...
    return { id, status: 'pending' };
  }
}
 
export function createOrderService() {
    return new OrderService();
}
 
- Follow the Single Responsibility Principle
- Favor composition over inheritance
- Avoid using `any` type. If necessary, use `unknown`

Short. Direct. Front matter plus instructions with one code example.

Capability rules go deeper on specific architecture concerns. Here's an excerpt from a forms rule:

.cursor/rules/forms.mdc
description: Writing forms with React Hook Form, Server Actions, Zod
globs: apps/**/*.tsx,packages/**/*.tsx
alwaysApply: false
---
 
# Forms
 
- Use React Hook Form for form validation and submission
- Use Zod for form validation
- Use the `zodResolver` function to resolve the Zod schema
- Use Server Actions for server-side code handling
- Never add generics to `useForm`, use Zod resolver to infer types
 
Follow this example to create all forms:
 
## Define the schema
 
Zod schemas should be in the `schema` folder and exported for reuse:
 
// _lib/schema/create-order.schema.ts
import { z } from 'zod';
 
export const CreateOrderSchema = z.object({
  productId: z.string().min(1),
  quantity: z.number().min(1),
});
 
## Create the Server Action
 
// actions/create-order.action.ts
'use server';
 
import { enhanceAction } from '@/lib/actions';
import { CreateOrderSchema } from '../schema/create-order.schema';
 
export const createOrderAction = enhanceAction(
  async function (data, user) {
    // data has been validated against the Zod schema
    // user is the authenticated user
    // ... your code here
    return { success: true };
  },
  {
    auth: true,
    schema: CreateOrderSchema,
  },
);
 
## Create the Form Component
 
// components/create-order-form.tsx
'use client';
 
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition } from 'react';
import { toast } from 'sonner';
 
import { CreateOrderSchema } from '../schema/create-order.schema';
import { createOrderAction } from '../actions/create-order.action';
 
export function CreateOrderForm() {
  const [pending, startTransition] = useTransition();
 
  const form = useForm({
    resolver: zodResolver(CreateOrderSchema),
    defaultValues: { productId: '', quantity: 1 },
  });
 
  const onSubmit = (data) => {
    startTransition(async () => {
      await toast.promise(createOrderAction(data), {
        loading: 'Creating order...',
        success: 'Order created',
        error: 'Failed to create order',
      });
    });
  };
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        {/* FormField components here */}
      </form>
    </Form>
  );
}

Here's another capability rule for API routes:

.cursor/rules/route-handlers.mdc
description: Next.js API Route Handlers
globs: src/**/route.ts
alwaysApply: false
---
 
# Route Handlers
 
- Use Route Handlers when fetching data from Client Components
- Always use the `enhanceRouteHandler` wrapper for consistent error handling and auth
 
// app/api/orders/route.ts
import { z } from 'zod';
import { enhanceRouteHandler } from '@/lib/routes';
import { NextResponse } from 'next/server';
 
const OrderSchema = z.object({
  productId: z.string(),
  quantity: z.number().min(1),
});
 
export const POST = enhanceRouteHandler(
  async function({ body, user, request }) {
    // 1. "body" is validated against OrderSchema
    // 2. "user" is the authenticated user
    // 3. "request" is NextRequest
    return NextResponse.json({ success: true });
  },
  {
    schema: OrderSchema,
  },
);
 
// Unauthenticated route
export const GET = enhanceRouteHandler(
  async function({ request }) {
    // No auth required, user is null
    return NextResponse.json({ success: true });
  },
  {
    auth: false,
  },
);

Capability rules cover complete workflows. The AI gets the exact pattern, not just principles.

The feedback loop

When the AI makes a mistake, treat it as a trigger. Go back to your rules and improve them so it doesn't happen again.

AI suggesting inline styles instead of your design system? Add a rule. Wrong import paths? Add a rule. Inconsistent error handling? Add a rule.

Over time, your rules get better. The AI makes fewer mistakes. You spend less time fixing output.

Getting started

If you're new to rules, start with generic rules covering your language and framework conventions. Add capability rules as you notice the AI making repeated mistakes in specific areas.

Two resources:

Rules don't replace architecture

You still need architecture documentation for humans. Teams need shared understanding of design decisions, tradeoffs, constraints.

Rules translate those human decisions into machine-readable instructions. Architecture docs explain why you chose a pattern. Rules tell the AI how to implement it.

If you take one thing from this post: invest in your rules. The upfront effort pays back every time you prompt.