| 1 | # Code standards |
| 2 | |
| 3 | Rules for every file in this repository. They are not style preferences; a change that breaks one of them is a change that gets sent back. |
| 4 | |
| 5 | ## 1. DRY and SOLID |
| 6 | |
| 7 | Write a thing once. Two call sites doing the same keyset pagination, the same binary-content sniff, the same permission check means one function and two callers. |
| 8 | |
| 9 | Duplication that is about to diverge is not duplication — do not merge two things that happen to look alike today. |
| 10 | |
| 11 | Each unit does one thing: a service owns a domain, a module wires it, a controller maps HTTP to it, a DTO describes the wire. A service that spawns `git` and also decides HTTP status codes is two things. |
| 12 | |
| 13 | Depend on the narrow thing. A function that needs a git directory takes a git directory, not the repository row it could be read from. |
| 14 | |
| 15 | ## 2. No reshaping functions |
| 16 | |
| 17 | `toCommitDTO`, `toCommentDTO`, `toSummaryOf |
| 18 | |
| 19 | Either the function that produces the data produces the exact fields the caller needs, or it produces nothing and the caller builds the object where it is returned. |
| 20 | |
| 21 | ```ts |
| 22 | // No: the row is fetched, then renamed. |
| 23 | function toCommitSummary(commit: |
| 24 | return { sha: |
| 25 | } |
| 26 | |
| 27 | // Yes: the query selects the fields the response is made of. |
| 28 | const [commit] = |
| 29 | .select({ sha: |
| 30 | .from(schema.commit |
| 31 | ``` |
| 32 | |
| 33 | The same goes for a local builder that assembles a value out of a page's state for another component to consume, such as `repositoriesHref` or `hrefFor` building pagination links. The component that renders the value takes the parts and builds it once, so every page stops writing its own copy. |
| 34 | |
| 35 | ```tsx |
| 36 | // No: each page builds its own pagination links. |
| 37 | const repositoriesHref = ( |
| 38 | <CursorPagination firstHref={ |
| 39 | |
| 40 | // Yes: the pagination takes the parts. |
| 41 | <CursorPagination pathname={ |
| 42 | ``` |
| 43 | |
| 44 | Value conversion is not reshaping: parsing a string into a `Date`, turning a service name into a binary name, formatting a colour for an `<input>`. Those stay. |
| 45 | |
| 46 | ## 3. Strings are never concatenated for layout |
| 47 | |
| 48 | No `'...' + '...'` to keep a line short. This applies to OpenAPI `description`, error messages, log lines, everything. |
| 49 | |
| 50 | ```ts |
| 51 | // No. |
| 52 | description: |
| 53 | 'Commits signed with it read as verified once the key carries an ' + |
| 54 | 'address this account has verified.', |
| 55 | |
| 56 | // Yes. |
| 57 | description: 'Commits signed with it read as verified once the key carries an address this account has verified.', |
| 58 | ``` |
| 59 | |
| 60 | Concatenation is for building a string out of parts that vary — interpolation is preferred there anyway. |
| 61 | |
| 62 | ## 4. Comments earn their place |
| 63 | |
| 64 | A comment explains why, never what. If the code says what it does and the reason is obvious, write no comment. |
| 65 | |
| 66 | Ban: paragraph essays above a function, restating the signature, narrating each step, explaining the language, apologising for a decision, or telling a story about the alternative that was rejected — that belongs in `docs/`, as a numbered decision. |
| 67 | |
| 68 | Keep: a non-obvious constraint, an invariant a reader cannot see from the code, a reason a surprising line is correct. |
| 69 | |
| 70 | ```ts |
| 71 | // No. |
| 72 | /** |
| 73 | * Directories first, then files, each group alphabetical. |
| 74 | * |
| 75 | * Submodules are grouped with directories: they are directory-shaped to a |
| 76 | * reader, whatever git calls them. |
| 77 | * |
| 78 | * Partitioning in one pass and sorting the two groups separately beats one |
| 79 | * comparator that has to check the type on every comparison, but neither is |
| 80 | * close to being worth optimizing. |
| 81 | */ |
| 82 | |
| 83 | // Yes. |
| 84 | /** Directories first, then files, each alphabetical. Submodules sort as directories. */ |
| 85 | ``` |
| 86 | |
| 87 | ## 5. No wrapped comments or strings |
| 88 | |
| 89 | One comment is one line, however long. One string literal is one line, however long. Editors wrap; source does not need to. |
| 90 | |
| 91 | ```ts |
| 92 | // No. |
| 93 | // The stored expiry doubles as the "last sent" stamp: a token issued less |
| 94 | // than a minute ago still has all but a minute of its life left. |
| 95 | |
| 96 | // Yes. |
| 97 | // The stored expiry doubles as the "last sent" stamp: a token issued less than a minute ago still has all but a minute of its life left. |
| 98 | ``` |
| 99 | |
| 100 | Multi-line block comments are allowed only when the lines are genuinely separate items, such as `@param` tags or an example. |
| 101 | |
| 102 | ## 6. ` |
| 103 | |
| 104 | A file under `services/` exports an `@Injectable()` class. Nothing else belongs there. |
| 105 | |
| 106 | Pure functions, types, error classes, codecs and test fakes live under `lib/`, mirroring the same folder names. `lib/git/tree/list-tree.ts` is a function; |
| 107 | |
| 108 | The same applies elsewhere: a folder is named for what it holds, and a file that does not fit the name moves rather than stretching it. |
| 109 | |
| 110 | ## 7. Production quality is the default |
| 111 | |
| 112 | Every branch is reachable and reached in tests, or the branch does not exist. Errors carry a cause the caller can act on. Nothing is left `any`. Nothing ships commented out. Trust boundaries validate, and the check lives where the boundary is, not at each call site. |
| 113 | |
| 114 | No speculative abstraction: an interface with one implementation, a factory for one product, or a configuration value that never varies is deleted on sight. |
| 115 | |
| 116 | ## 8. One file holds one component family |
| 117 | |
| 118 | A `.tsx` file exports components that belong together, or a single component. `components/ui/avatar.tsx` holds |
| 119 | |
| 120 | A page is not a family. `ProfileReadme`, `PeopleList`, ` |
| 121 | |
| 122 | Where the file goes depends on who uses it: |
| 123 | |
| 124 | - Used by one route only: next to it, in the same folder as the `page.tsx` that renders it. |
| 125 | - Used by more than one route: under `components/`, in the folder named for its domain. |
| 126 | |
| 127 | ```tsx |
| 128 | // No: app/[username]/page.tsx |
| 129 | export default async function |
| 130 | function PeopleList() { |
| 131 | function PinnedRepositories() { |
| 132 | |
| 133 | // Yes: only this route renders them. |
| 134 | // app/[username]/page.tsx |
| 135 | // app/[username]/people-list.tsx |
| 136 | // app/[username]/pinned-repositories.tsx |
| 137 | |
| 138 | // Yes: rendered by more than one route. |
| 139 | // components/auth/user/user-avatar.tsx |
| 140 | ``` |
| 141 | |
| 142 | ## 9. Look before you write a component |
| 143 | |
| 144 | Before adding a component, search `components/` and the route folders for one that already does the job. If it exists, use it; if it almost fits, extend it with a prop rather than writing a sibling that differs by one class name. |
| 145 | |
| 146 | Two components that render the same list of avatars and links are a violation of rule 1, however far apart they live. |
| 147 | |
| 148 | ## 10. Every suspended boundary has a loading state |
| 149 | |
| 150 | Anything that waits for data shows a skeleton while it waits. A route segment that fetches gets a `loading.tsx`. An async server component rendered inside a page gets a `<Suspense>` with a skeleton fallback, and the skeleton lives in the same file as the component it stands in for, the way |
| 151 | |
| 152 | `<Suspense fallback={null}>` is not a loading state: the layout jumps when the content arrives. The skeleton takes the shape and size of what it replaces. |
| 153 | |
| 154 | ```tsx |
| 155 | // No. |
| 156 | <Suspense fallback={ |
| 157 | <OrganizationTeamList slug= |
| 158 | </Suspense> |
| 159 | |
| 160 | // Yes. |
| 161 | <Suspense fallback={ |
| 162 | <OrganizationTeamList slug= |
| 163 | </Suspense> |
| 164 | ``` |