{"slug": "12-rules-for-claude-md-example", "title": "12 Rules for Claude.md (Example)", "summary": "A developer shared a detailed CLAUDE.md configuration file for an iOS app called Trailmark, which is backed by a Node.js REST API. The file establishes strict coding rules, including type safety, naming conventions, and a test-driven development loop, to guide AI assistants in maintaining the project.", "body_md": "| # CLAUDE.md | |\n| Trailmark — an iOS app for logging hikes, backed by a Node.js REST API. | |\n| - `ios/` — SwiftUI app, iOS 17+, native components only | |\n| - `api/` — Node 22, TypeScript, Express, Postgres 16 (Kysely) | |\n| - `packages/contracts/` — zod schemas + generated OpenAPI. Single source of truth for both sides. Change the contract first, then the server, then the client. | |\n| Nested rules live in `api/CLAUDE.md` and `ios/CLAUDE.md`. Longer workflows (release, migration, security review) are skills in `.claude/skills/` — do not paste them here. | |\n| ## Ask before you assume | |\n| Never guess at intent. If a task leaves anything open — which screen, which endpoint, what happens on failure, whether it needs a migration, whether this is user-facing — stop and ask. One question up front is cheaper than half a day of work in the wrong direction. | |\n| - Ask when the request could reasonably mean two different things. | |\n| - Ask before changing a public API shape, a DB schema, or anything in `packages/contracts/`. | |\n| - Do not invent product decisions, copy, or acceptance criteria. | |\n| - Do not widen scope past what was asked. Note the adjacent thing you spotted; don't fix it unprompted. | |\n| - If you had to assume something you couldn't resolve, list it explicitly at the top of your summary. | |\n| ## The loop | |\n| Every change runs through this. A task is not done until it is green. | |\n| ``` bash | |\n| npm run check # tsc --noEmit, eslint, prettier --check | |\n| npm test # vitest, unit + integration | |\n| npm run test:api # supertest against a throwaway Postgres | |\n| npm run db:migrate # never edit an applied migration, always add a new one | |\n| ``` | |\n| ``` bash | |\n| cd ios && xcodegen && xcodebuild test -scheme Trailmark -destination 'platform=iOS Simulator,name=iPhone 17' | |\n| cd ios && swiftlint --strict | |\n| ``` | |\n| Rules for the loop: | |\n| - Write the failing test first. Watch it fail for the right reason, then make it pass. | |\n| - Run `npm run check && npm test` after every meaningful edit, not once at the end. | |\n| - Never report success on a red loop. Never disable, skip, or `.only` a test to get to green. | |\n| - If a test is wrong, say so and explain why before changing it. | |\n| - Do not start long-running processes (`npm run dev`, `expo start`) to \"verify\" — they never exit. Use the commands above. | |\n| ## Type checking | |\n| Both languages are strict. Type errors are not warnings. | |\n| `tsconfig.json` — these stay on: | |\n| ``` json | |\n| { | |\n| \"compilerOptions\": { | |\n| \"strict\": true, | |\n| \"noImplicitAny\": true, | |\n| \"noUncheckedIndexedAccess\": true, | |\n| \"exactOptionalPropertyTypes\": true, | |\n| \"noImplicitOverride\": true, | |\n| \"verbatimModuleSyntax\": true | |\n| } | |\n| } | |\n| ``` | |\n| - No `any`. If a type is genuinely unknown, use `unknown` and narrow it. | |\n| - No `as` to escape an error, and no non-null `!`. Fix the type or narrow properly. | |\n| - No `@ts-expect-error` without a comment naming the upstream issue. | |\n| - All external input (request bodies, query params, API responses) is parsed with a zod schema from `packages/contracts/`. Types are inferred from the schema, never hand-written alongside it. | |\n| - Swift: no force unwraps, no `try!`, no `as!`. Use `guard let`, typed `throws`, and `Result` at boundaries. | |\n| Respect these from the start. Do not write loose code and correct it after the type check fails. | |\n| ## Naming | |\n| Consistency is for the model as much as for us. Pick the existing word, don't coin a new one. | |\n| **Domain vocabulary — one word per concept:** | |\n| | Concept | Use | Never | | |\n| | --------------- | ---------------------- | ----------------------------- | | |\n| | A recorded walk | `hike` | trip, walk, activity, session | | |\n| | The GPS line | `track` | route, path, trace | | |\n| | A saved place | `waypoint` | pin, marker, poi | | |\n| | Account access | `Sign in` / `Sign out` | Login, Log In, Log out | | |\n| **Code:** | |\n| - Functions: `createHike`, `getHike`, `listHikes`, `updateHike`, `deleteHike`. Not `fetch`, `remove`, `save`, `handle`. | |\n| - Booleans read as assertions: `isSyncing`, `hasTrack`, `canEdit`. | |\n| - Routes: kebab-case, plural nouns — `GET /v1/hikes/:hikeId/waypoints`. | |\n| - Postgres: snake_case tables and columns, plural tables — `hikes`, `waypoints`, `started_at`. | |\n| - Swift types are UpperCamelCase and the file is named after the type: `HikeDetailView.swift`. | |\n| - SwiftUI views end in `View`, observable state ends in `Store`: `HikeListView`, `HikeStore`. | |\n| - Test files sit beside the source: `hikes.service.ts` → `hikes.service.test.ts`. | |\n| **User-facing copy:** sentence case for buttons and labels (\"Save hike\", not \"Save Hike\"). Copy strings live in `ios/Trailmark/Resources/Localizable.strings` — no string literals in views. | |\n| ## Project structure | |\n| ``` | |\n| api/ | |\n| src/ | |\n| modules/<domain>/ # route.ts, service.ts, repo.ts, *.test.ts | |\n| db/ # kysely client, generated types | |\n| middleware/ | |\n| lib/ # shared, dependency-free helpers | |\n| migrations/ # timestamped .sql, append-only | |\n| ios/ | |\n| Trailmark/ | |\n| Features/<Feature>/ # View, Store, and their tests | |\n| DesignSystem/ # Tokens.swift, reusable components | |\n| Networking/ # APIClient, generated contract types | |\n| Persistence/ # SwiftData models | |\n| packages/ | |\n| contracts/ # zod schemas, OpenAPI output | |\n| ``` | |\n| - New backend work goes in a module folder. Routes never talk to the DB directly — route → service → repo. | |\n| - New screens go in `Features/`. Views hold no networking and no business logic; that belongs in the Store. | |\n| - Nothing new at the repo root without asking. | |\n| ## Dependencies | |\n| Code is cheap; maintenance isn't. Prefer a well-established package over rolling your own, and prefer the platform over a package. | |\n| Before installing anything, check and state: | |\n| - Weekly downloads over ~100k, a release in the last six months, more than one maintainer. | |\n| - Nothing single-maintainer or freshly published for anything touching auth, crypto, networking, or file I/O. | |\n| - No new dependency for something the standard library, SwiftUI, or an existing dependency already does. | |\n| Ask before adding a dependency. Never add one as a side effect of another task, and never pin to `latest` — exact versions only, lockfile committed. | |\n| Native SwiftUI components only on iOS. No third-party UI frameworks. | |\n| ## Performance | |\n| Every endpoint has a p95 budget of 200ms. Anything slower is a bug, not a tuning opportunity for later. | |\n| - Filter, sort, aggregate, and paginate in SQL. Never pull rows into Node to filter them there. | |\n| - Every list endpoint is paginated: `limit` defaults to 25, hard maximum 100. No unbounded `SELECT`. | |\n| - Select the columns you need. `select *` is not acceptable in a repo function. | |\n| - No N+1. Join or batch — one query per request path, not one per row. | |\n| - New query patterns come with an index in the same migration. Run `EXPLAIN ANALYZE` and include the plan in your summary if a query touches more than 10k rows. | |\n| - Do a performance pass at the end of any feature that reads data, and say what you checked. | |\n| On iOS: no work on the main actor beyond UI updates. Lists over 50 items use `LazyVStack`. Images are downsampled before display. | |\n| ## Error handling | |\n| Fail early, fail loudly, and never swallow. | |\n| - No empty `catch`. No `catch { console.log(e) }`. Either handle it meaningfully or let it propagate. | |\n| - Throw a typed `AppError` with a stable machine-readable code. Unexpected errors surface as 500 and page us — they do not get mapped to a friendly 200. | |\n| - Every error response uses this envelope: | |\n| ``` ts | |\n| { error: { code: \"hike_not_found\", message: \"That hike no longer exists.\", details?: unknown } } | |\n| ``` | |\n| - Messages are useful to the client: what failed, and what to do about it. \"Something went wrong\" is not an error message. | |\n| - Validation failures return 422 with the zod issue list in `details`. | |\n| - iOS maps every error code to real copy and a real recovery action. No silent failures, no generic alert as a catch-all. | |\n| - Log with structured context (`hikeId`, `userId`, `requestId`), never a bare string. | |\n| ## End-to-end testing | |\n| After any feature that spans both sides, exercise it like a person would — not just with unit tests. | |\n| - Boot the API and the simulator, sign in, and drive the actual flow end to end: start a hike, record a track, background the app, foreground it, sync, confirm the row in Postgres. | |\n| - Test the unhappy paths deliberately: airplane mode mid-sync, expired token, force-quit during a recording, duplicate submit. | |\n| - Report what you clicked and what you saw. If a step failed, keep the failure — don't work around it and call it passing. | |\n| You don't need to write Playwright suites for this. Driving the app as a human is the point. | |\n| ## UI testing | |\n| A green test suite tells you nothing about whether the screen looks right. | |\n| - Screenshot every screen you touch, at least once, and look at it before you say it's done. | |\n| - Check the small iPhone and the largest Dynamic Type setting. Check dark mode. | |\n| - Use realistic seed data from `api/seeds/realistic.sql` — real place names, long names, empty states, a 400-item list. Never `Lorem ipsum`, never \"Test User\". | |\n| - Look specifically for: clipped or truncated text, overlapping views, content under the safe area, missing empty state, missing loading state. | |\n| ## Architecture | |\n| Read this before exploring the codebase. If you find yourself searching for something that belongs here, add it (see below). | |\n| Request path: SwiftUI view → Store → `APIClient` → Express route → service → repo → Postgres. Auth is a short-lived JWT in the Keychain with a refresh token; `middleware/auth.ts` populates `req.user` and every route below `/v1` requires it. | |\n| Sync is offline-first. The app writes to SwiftData immediately, queues a mutation, and reconciles on reconnect. Server timestamps win on conflict; the client never invents an `id` — it sends a client-generated `idempotencyKey`. | |\n| **Where things live:** | |\n| | You need | Look in | | |\n| | -------------------------------- | ------------------------------------------- | | |\n| | Auth, tokens, refresh | `api/src/modules/auth/` | | |\n| | Request/response schemas | `packages/contracts/src/` | | |\n| | DB client and generated types | `api/src/db/` | | |\n| | Migrations | `api/migrations/` | | |\n| | Colours, spacing, type scale | `ios/Trailmark/DesignSystem/Tokens.swift` | | |\n| | Networking, retry, auth headers | `ios/Trailmark/Networking/APIClient.swift` | | |\n| | Offline queue and reconciliation | `ios/Trailmark/Persistence/SyncQueue.swift` | | |\n| | Env vars and their defaults | `api/src/config.ts` | | |\n| | CI pipeline | `.github/workflows/ci.yml` | | |\n| ## Keeping this file current | |\n| This file is a failure log, not a wishlist. Every line below exists because it went wrong at least once. | |\n| When you make a mistake, get corrected, or discover something about this codebase that wasn't written down: | |\n| 1. Add one line to the failure log below, in the imperative, describing the correct behaviour. | |\n| 2. Keep it specific to this repo. General advice belongs nowhere. | |\n| 3. If the fix is a workflow rather than a rule, put it in `.claude/skills/` and link it from here. | |\n| 4. Include the change in the same commit and mention it in your summary. | |\n| Keep this file under 500 lines. It is loaded into every session, and long context makes you less reliable, not more. If a section outgrows its usefulness, move it to `api/CLAUDE.md`, `ios/CLAUDE.md`, or a skill. | |\n| ## Failure log | |\n| - Do not run `npm run dev` to verify a change; it never exits. Use `npm run check && npm test`. | |\n| - Run migrations against the test database before `npm run test:api`, or every test fails on a missing column. | |\n| - `xcodebuild` needs `xcodegen` run first — the `.xcodeproj` is generated and gitignored. | |\n| - Do not edit `packages/contracts/dist/`; it is generated by `npm run contracts:build`. | |\n| - Do not add `@MainActor` to `HikeStore` methods that already run on the main actor — SwiftLint flags it and it hides real concurrency bugs. | |\n| - Waypoint ordering comes from `sequence`, not `created_at`. Offline waypoints arrive out of order. | |\n| - Never call `deleteHike` without checking for queued mutations against that hike first. |", "url": "https://wpnews.pro/news/12-rules-for-claude-md-example", "canonical_source": "https://gist.github.com/warrenday/07abe3bbf147720345846f195834fc0b", "published_at": "2026-08-13 11:07:28+00:00", "updated_at": "2026-08-13 13:54:19.117337+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Trailmark", "Node.js", "SwiftUI", "Express", "Postgres", "Kysely", "zod", "OpenAPI"], "alternates": {"html": "https://wpnews.pro/news/12-rules-for-claude-md-example", "markdown": "https://wpnews.pro/news/12-rules-for-claude-md-example.md", "text": "https://wpnews.pro/news/12-rules-for-claude-md-example.txt", "jsonld": "https://wpnews.pro/news/12-rules-for-claude-md-example.jsonld"}}