| 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 | ``` |
| 165 | |
| 166 | |
| 167 | ## 11. Changes to Infra or env variables must be propagated to ` |
| 168 | |
| 169 | If a new item to the workspace is added, the apps/*/Dockerfile needs to update else the build fails. and self hosting setup(in /docker) and docs in (apps/docker) needs to be updated too. |
| 170 | |
| 171 | |
| 172 | ## 12. Controllers pass, services do (` |
| 173 | |
| 174 | A controller, a GraphQL resolver, a git or SSH handler: each reads what its transport delivered, hands the values to a service, and responds with what the service returns. It runs no queries, makes no decisions about the domain, and does not reshape what comes back. What it may do is the transport's own part: read a header, pick the representation the client asked for (JSON or raw bytes), set a status code or a `Location` header, wrap a stream. |
| 175 | |
| 176 | A service takes plain values and returns plain data. It does not know whether it was called over HTTP, GraphQL, SSH or a queue: no `Request`, `Response`, headers, status codes, |
| 177 | |
| 178 | ```ts |
| 179 | // No: the controller decides what the domain allows and reshapes the result. |
| 180 | @Patch(':id') |
| 181 | async update(@Param |
| 182 | const current = |
| 183 | if (body.tag_name |
| 184 | throw new UnprocessableEntityException( |
| 185 | const updated = |
| 186 | return { ...updated |
| 187 | } |
| 188 | |
| 189 | // Yes: the controller passes the values on and returns the answer. |
| 190 | @Patch(':id') |
| 191 | update(@Param('id |
| 192 | return this.releases |
| 193 | } |
| 194 | ``` |
| 195 | |
| 196 | ## 13. No function for a line or two |
| 197 | |
| 198 | A function earns its name by being used in many places, by hiding real complexity, or by being tested on its own. A two-line helper declared for one or two call sites does none of that: the reader jumps away to learn what it does, then jumps back. Write it where it is used. |
| 199 | |
| 200 | ```ts |
| 201 | // No: a declaration to read for a check that fits on one line. |
| 202 | function refuseDiscussion(category: |
| 203 | if (category !== |
| 204 | } |
| 205 | refuseDiscussion(body.discussion_category_name |
| 206 | |
| 207 | // Yes. |
| 208 | if (body.discussion_category_name |
| 209 | ``` |
| 210 | |
| 211 | ## Philosophy |
| 212 | |
| 213 | This codebase will outlive you. Every shortcut becomes someone else's burden. Every hack compounds into technical debt that slows the whole team down. |
| 214 | |
| 215 | You are not just writing code. You are shaping the future of this project. The patterns you establish will be copied. The corners you cut will be cut again. |
| 216 | |
| 217 | Fight entropy. Leave the codebase better than you found it. |