Code standards before the first feature

Jesse James Richard
|
|
7 min read
#Method
#Architecture
#AI & Agents

In December I started a repository with no product in it. Just code rules, shell tooling, testing standards, and an architecture document. Giant Context, the website platform this blog documents, got its first commit the same day that repository did, and that commit is this template being applied. The project is three and a half weeks old as I write this.

This is everything that was in it, verbatim, and the reason none of it describes a product.

Why the rules come first

The plan was always that AI tooling would write most of the code, and that is why the rules came first.

A generator with no conventions to obey does not produce a codebase. It produces output. Good rules self-reinforce, because AI tooling writes code by reading code. When every existing file matches the rules, every generated file matches them too, and the codebase teaches its own style to whatever writes the next file.

Loose rules run the same mechanism backwards. The tooling amplifies every inconsistency it finds, and you get spaghetti at machine speed. If the rules are not set, do not bother vibecoding.

So the rules are written the way you would write them for a machine: specific, enumerable, and free of judgment calls. Below is the whole template, as it was on day one.

Architecture Gameplan

# Architecture Gameplan
## Overview
Polyrepo architecture with OpenAPI-driven type safety betweendecoupled frontend and backend. A single ApiClient handlesauthentication, errors, and headers. Schema metadata flows frombackend to frontend, enabling auto-generated forms with zeromanual field wiring.

The API describes itself. The clients are generated from the description. The forms wire themselves from schema metadata. There is no product in the repository, but the entire shape of one is pre-announced. An API at the center, a typed database under it, forms that take their validation from the schema instead of an opinion. Everything I have built in the three weeks since is that paragraph executing.

Rules before code

Six rules sit above everything else in the template's CLAUDE.md. They are absolute, they are short enough to hold in your head, and every one of them is a thing you must never do.

The six non-negotiables

1. NEVER use SCREAMING_SNAKE_CASE - Always camelCase:   `maxRetries` not `MAX_RETRIES`2. NEVER do backwards compatibility - Delete old code,   make changes directly3. NEVER define functions inside React components -   All functions go in `utils.ts`4. NEVER use default exports - Always named exports5. NEVER use inline complex logic in JSX - Extract to utils6. Services are HMR - Don't restart services
ElementConventionMax Words
ComponentsPascalCase2-3
FunctionscamelCase2-3
VariablescamelCase2-3
ConstantscamelCase2-3
TypesPascalCase + "Type"2-3

A word budget for identifiers, and below the naming rules sits the anatomy of a file. Every file opens with the same format header, and imports arrive spaced by category in a fixed order: external framework, external libraries, local components, local utilities, local constants, and type imports last.

The header, and imports by category

/** @format */
import { useState, useEffect } from "react";
import { Box } from "@mui/material";
import { UserCard } from "./components/UserCard";
import { formatData } from "./utils";
import { defaultValue } from "./constants";
import type { UserType } from "./types";

Every component is a barrel. Exports in index, presentation in view, and every function in utils, none of them in the view file.

Every component is a barrel

ComponentName/├── index.ts       # Named exports only├── view.tsx       # Presentation├── types.ts       # Types with "Type" suffix├── constants.ts   # camelCase constants├── hooks.ts       # Component hooks├── utils.ts       # ALL functions (none in view.tsx)└── components/    # Child components    └── ChildName/        ├── index.ts        └── view.tsx

And the view file has a mandated order

export const Component = (props: Props) => {  // 1. Context hooks  // 2. State (useState)  // 3. Refs (useRef)  // 4. Custom hooks  // 5. Effects (useEffect)  // 6. Memoized values (useMemo)  // 7. Render (return)};

Behind the CLAUDE.md sits a documentation tree with a directory for every kind of opinion. Seven of them, dozens and dozens of rules, and none of them describing a product, because there wasn't one.

DomainLinesWhat it mandates
standards722Prettier config, naming, import spacing, named exports only, comment rules, React directory structure and component order, Node service pattern, a single ApiClient, env-var naming, and styling through theme tokens rather than styled-components
testing444File and test-file structure, mock patterns, type-safe mock factories, MSW for integration, contract tests against generated schemas, what not to test, coverage thresholds
error-handling463Scoped React error boundaries, a backend error class hierarchy behind one handler, a central response generator, and structured logging with the same API on both sides
tooling277Tiered quality gates, the hook files themselves, lint-staged and commitlint config, the commit message format, and the emergency bypass
shell-scripts285The required and optional script sets, a shared template, flags, arguments, permissions, and how each maps to CI
architecture1,297The gameplan. Repository structure, the type-safety flow, OpenAPI schema extensions, code generation, schema-driven forms, routing, the design system, and CI/CD
global-state0Nothing. The directory exists and the README promises TanStack Query and context patterns. The file is empty

The seventh has a directory, a README line promising TanStack Query and context patterns, and a plan file with nothing in it. I knew I would need an opinion there and did not have one yet, so I scaffolded the slot and left it empty rather than invent a rule I would have to unpick later. A written rule you do not believe is worse than an admitted gap, because the tooling will obey it.

The gates that enforce it

Rules nothing enforces are suggestions, so the template ships four gates. Each is chosen for what it can catch in the time you are willing to wait for it.

StageWhat runsSpeedPurpose
Pre-commitlint-staged (ESLint + Prettier)~5sCatch style issues on staged files
Commit-msgcommitlintInstantEnforce type(scope): message format
Pre-pushtypecheck + tests + build~30s to 2minCatch bugs before CI
CIFull suiteMinutesFinal gate, deploy

The hooks themselves are one line each, which is the point of putting the work in scripts.

The three hooks

# .husky/pre-commitpnpm lint-staged
# .husky/commit-msgpnpm commitlint --edit $1
# .husky/pre-pushpnpm typecheck && pnpm test && pnpm build

Five scripts are required in every project, each mapping one to one onto a package script: dev, build, test, lint, typecheck. More get added as a project needs them, and the template names the ones worth having ready: format, coverage, prepush, regen, deploy, health, smoketest.

Every one of them is these four lines.

The shared script template

#!/usr/bin/env bashset -euo pipefail
pnpm <command>

Exit on error, error on an undefined variable, fail on a broken pipe. The value is not in the script. It is that every project has the same five entry points, so I never have to remember how a particular repository wants to be started, and neither does anything else that reads it.

A stack needs a test

A stack with no product is a theory, so it needed something to build with real forms, real auth, real file handling, real rendering. That turned out to be a block-based site builder that needs no coder, on a stack coders respect, hosting included.

Three weeks in, the repository holds one with a block editor, public rendering, and multi-tenant routing. And the test case is already growing opinions of its own. The planning document that landed just over a week ago describes a website platform that understands its own content, which points somewhere past a page builder and toward marketing that increasingly runs itself. What the test case becomes is still open.

What changed and what did not

Some specifics have already shifted. The plan said polyrepo and got monorepo. The core never moved. The API describes itself, and every client, form and hook is generated from that description rather than written by hand.

That is the point of writing the rules first. The machine writes most of the code now, copying whatever is already there. I set the way down before I knew the what.

#Product
#Growth
#Keystone

A marketing agency is almost pure labor, and payroll is half to two-thirds of what it takes in. Automating the production work, rather than selling an...

Jesse James Richard

|

Jan 2, 2026
Read previous

Hiring an early engineer, or building something like this

Remote, Pacific time, full-time or contract. Get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy
Code standards before the first feature | Jesse James Richard