
How I Automated My Entire Dev Workflow With 8 Cursor Rules (and What Changed) It was 11:47...
It was 11:47 PM on a Tuesday when I realized I'd just spent forty minutes fixing a bug that Cursor had written for me that morning.
The AI had happily generated a fetch() call with no error handling, no types on the response, a stray console.log('here') left in for good measure, and a commit message that read fixes stuff. Every one of those choices was fine in the moment. Every one of those choices cost me an hour at midnight.
That's when I stopped prompting and started writing rules.
Over the next few weeks I replaced the nagging, the re-prompting, and the "please remember we use TypeScript strict" with eight entries in my .cursorrules file. My workflow stopped being something I executed and became something the AI executed for me — correctly, every time.
Here are the eight rules, with concrete before/after examples and what changed once they were in place.
Before implementing any function, write a failing test for it first.
Place the test next to the source file as *.test.ts. Only then
write the minimum code needed to make the test pass.
Before: I'd accept a 40-line function, then try to retrofit tests around its actual (often wrong) behavior.
// Cursor's default: implementation first, tests never
export function parseDuration(input: string): number {
// ...40 lines of regex and edge cases, no tests
}
After: Cursor generates the test first, which forces it to clarify the contract before writing any logic.
// parseDuration.test.ts — written first
describe('parseDuration', () => {
it('parses "2h 30m" as 9000 seconds', () => {
expect(parseDuration('2h 30m')).toBe(9000);
});
it('throws on invalid input', () => {
expect(() => parseDuration('banana')).toThrow();
});
});
// parseDuration.ts — written to pass the tests
export function parseDuration(input: string): number {
// minimal implementation, every branch covered
}
One rule, zero retrofitted tests.
Every commit message must follow Conventional Commits:
<type>(<scope>): <description>. Types: feat, fix, refactor, test,
docs, chore, perf. Keep the subject under 72 characters. Add a body
for any non-trivial change.
Before: My git log was a graveyard of update, fix bug, and wip.
* wip
* fix
* update stuff
* more changes
After: Every commit tells me what it does without opening the diff.
* feat(auth): add refresh token rotation
* fix(billing): prevent double charge on retry
* refactor(api): extract error middleware
* test(auth): cover expired-token edge case
Semantic-release picks up the types, my changelog writes itself, and I can actually git bisect because every commit means something.
console.log in Committed Code — Use a LoggerNever use console.log, console.error, or console.warn in committed
code. Import the project logger (e.g., `import { logger } from
'@/lib/logger'`) and use logger.info / logger.error with a structured
context object. Debug prints must be removed before the commit.
Before: Prod logs were a graffiti wall of console.log('here'), console.log('hmm'), and the occasional unredacted API key.
console.log('user', user); // leaks PII
console.log('token', token); // leaks secrets
console.log('here'); // leaks my sanity
After: Structured logs, every time, with levels that actually mean something.
logger.info('user.login', { userId: user.id, method: 'oauth' });
logger.error('payment.failed', { orderId, error: err.message });
Datadog queries work. PagerDuty alerts work. I stopped getting paged at 3 AM by logs I couldn't search.
Every async function that can fail must have explicit try/catch and
return a discriminated union: { ok: true, data: T } | { ok: false,
error: AppError }. Never let unhandled rejections propagate. AppError
must have a `code` field for programmatic handling.
Before: Errors vanished into .catch(console.error) and reappeared as user-facing white screens.
async function charge(amount: number) {
const res = await stripe.charges.create({ amount });
return res; // what if stripe throws? who knows
}
After: Every call site is forced to handle both branches.
async function charge(amount: number): Promise<Result<Charge>> {
try {
const res = await stripe.charges.create({ amount });
return { ok: true, data: res };
} catch (err) {
return { ok: false, error: { code: 'STRIPE_FAILED', cause: err } };
}
}
const result = await charge(1999);
if (!result.ok) return handleChargeError(result.error);
// TypeScript now knows result.data exists
The compiler caught three unhandled error paths the day I added this rule.
process.env DirectlyNever access process.env directly outside of src/config/env.ts.
That file must validate all env vars with zod at startup and export
a typed `env` object. All other code imports `env` and gets
autocomplete + runtime guarantees.
Before: Every file reached into process.env like it was a shared snack drawer.
const apiKey = process.env.STRIPE_KEY; // string | undefined, maybe?
stripe.setApiKey(apiKey!); // the ! is doing a lot of work
After: One source of truth, validated on boot.
// src/config/env.ts
import { z } from 'zod';
export const env = z.object({
STRIPE_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
}).parse(process.env);
// everywhere else:
import { env } from '@/config/env';
stripe.setApiKey(env.STRIPE_KEY); // typed, non-null, validated
The app now refuses to start if an env var is missing. No more "works on my machine" outages.
A file must have a single responsibility. Max 200 lines. If a file
grows past 200 lines, split it by responsibility — not by arbitrary
chunks. Co-locate related files in a folder rather than stacking
them into one mega-module.
Before: utils.ts was 900 lines of unrelated functions, and every new utility got dumped into it.
src/
utils.ts ← 900 lines, 47 exports, nobody knows what's in it
After: Cursor refuses to grow files past the limit and proposes a folder split instead.
src/
utils/
date/
formatDate.ts
parseDuration.ts
string/
slugify.ts
truncate.ts
money/
formatCents.ts
Every file fits on one screen. Reviewers can actually read PRs.
Every HTTP handler must validate its input (body, query, params) with
a zod schema before any business logic runs. Reject invalid input
with a 400 and a structured error. Never trust `req.body`.
Before: Business logic was defensive-coded against missing fields, wrong types, and whatever else the internet threw at it.
app.post('/api/orders', async (req, res) => {
const { items, userId } = req.body; // any? undefined? an attack?
if (!items) return res.status(400).send('bad');
if (!Array.isArray(items)) return res.status(400).send('bad');
// ...20 more lines of manual validation
});
After: One schema, one parse call, typed body downstream.
const CreateOrderBody = z.object({
userId: z.string().uuid(),
items: z.array(z.object({
sku: z.string(),
quantity: z.number().int().positive(),
})).min(1),
});
app.post('/api/orders', async (req, res) => {
const body = CreateOrderBody.parse(req.body); // throws → 400 middleware
await createOrder(body); // body is fully typed here
res.status(201).end();
});
SQL injection attempts, missing fields, and the intern sending quantity: -1 all die at the door.
Before declaring any task complete, Cursor must run and report:
1. Typecheck passes (npm run typecheck)
2. Tests pass (npm test)
3. Lint passes (npm run lint)
4. No new `any` types introduced
5. No `console.log` in committed code
If any step fails, fix it before claiming the task is done.
Before: "Done" meant "the code compiles on my branch," and CI caught it three hours later.
me: "done!"
CI: ❌ 4 type errors, 12 lint warnings, 1 failing test
me: (fixing for the next hour)
After: Cursor self-checks before handing the PR back.
✓ typecheck (0 errors)
✓ tests (142 passed)
✓ lint (0 warnings)
✓ no new `any` types
✓ no console.log
Ready for review.
My CI pipeline now passes on the first try about 90% of the time. The other 10% is usually a flaky integration test, not a mistake I made.
Four weeks after these rules went live, I measured.
The meta-lesson: prompting is a loop. Rules are a state machine. Every rule you write is one conversation you never have to repeat.
Drop this into .cursorrules or .cursor/rules/workflow.mdc:
# Workflow Automation Rules
## Testing
- Write a failing test before any implementation
- Tests live next to source as *.test.ts
## Commits
- Conventional Commits format: <type>(<scope>): <description>
- Subject under 72 chars; body for non-trivial changes
## Logging
- Never use console.log/error/warn in committed code
- Use the project logger with structured context
## Errors
- Every async function returns { ok: true, data } | { ok: false, error }
- AppError must have a `code` field
- No unhandled promise rejections
## Config
- Never access process.env outside src/config/env.ts
- Validate all env vars with zod at startup
## File Structure
- One responsibility per file
- Max 200 lines; split by responsibility when exceeded
## Input Validation
- Every HTTP handler validates input with zod before logic runs
- Never trust req.body/query/params
## PR-Ready
- Before declaring done: typecheck + tests + lint must pass
- No new `any` types; no console.log
These 8 rules are the workflow layer. The rest of my stack — React, TypeScript, Next.js, Prisma, testing patterns — runs on 50+ more rules I've built out over the last year.
I packaged them into the Cursor Rules Pack v2 — organized by framework, priority-tagged, and production-tested. If you've ever felt like you're re-prompting Cursor for the same fixes every day, this is the file that stops it.
Stop prompting. Start ruling.
cursorCursor Automations in 2026: wire event-triggered coding agents to Slack, CI, and...
aiThe specs exist. The AI just can't see them. I've always been the type who builds hobby...
amazonbedrockConnect Claude Code, Cursor and Codex to Amazon Bedrock's new console (2026) Summary. On 5...
aiThere is a weird uncanny valley with LLM-generated UI right now. The code functions perfectly, but if...
aiI went down a rabbit hole this morning reading the late-2025 Juejin AI roundups side by side, and the...
mcpInstall guide and config at curatedmcp.com Zendesk MCP: Let Claude Handle Your Support...
Workflows from the Neura Market marketplace related to this Cursor resource