- Consistency is paramount. That applies to UI, UX, architecture, code and APIs.
- We must understand a problem-area before we start work on a solution
- Aim for simplicity and readability and avoid complexity
- Humans and computer-agents have limited context-windows. Scope should reflect this, and changes should be applied in digestible phases.
- Architecture decisions must be reviewed by at least one other engineer
- Review your own code-changes
- Prefer functional-style over object-oriented
- Prefer composability over inheritance
- We don't do DRY (Don't Repeat Yourself). We do WET (Write Everything Twice). Abstractions need to be warranted.
- Prefer utility-functions over library dependencies. Check which utilities already exist before creating new ones.
- When evaluating 3rd party libraries make sure to consider the size of their recursive dependencies as well
- Dead code must be deleted
- Look up best-practices, but consider how they fit with how the project is currently structured
- Return early. Avoid if/elseif/else.
// Badfunction getLabel(status: Status) { if (status === 'active') { return 'Active' } else if (status === 'inactive') { return 'Inactive' } else { return 'Unknown' }} // Goodfunction getLabel(status: Status) { if (status === 'active') return 'Active' if (status === 'inactive') return 'Inactive' return 'Unknown'}
- Aim for "single-responsibility-principle" when possible. Functions should avoid doing multiple unrelated things.
- Separation of concerns should prioritise business-logic. If there are purely technical concerns, they can be grouped together.
- Don't rely on undocumented library behaviour.
- Write idiomatic code. Use modern-syntax and established patterns for the given language.
- Code-style should be handled by automatic formatters, not debated in reviews
- Ternary usage should never span more than 2 lines. If they do, it's a code-smell indicating we need to extract some code.
- Immediately invoked function expressions are valid for computed values, but should otherwise be avoided
- Treat deprecations as warnings and suggest fixes
- It's good to extract business-logic into services. Group the services by domain-entities.
- Use descriptive names that use business-domain wording where applicable
- Length of variable names should be longer/shorter depending on their scope. Examples below:
- Global variables or functions
const navigationMenuItems = []refers to the place where it's being used and what the purpose is - Modules can omit the module scope, but explain purpose. E.g. in a
navigation.tsmodule,const menuItems = []where the purpose is explained, but module name is omitted. - Functions can omit the function scope, but explain purpose.
- Avoid single character variable names, except where it's idiomatic to do so.
- Global variables or functions
- Filenames must be lower-case. Not all file-systems are case-sensitive and this can cause issues.
- Use snake-case
- Comments should be added when they add value. Not to re-iterate the name of functions or variables.
- Comments must explain why, not what the code means
// Bad: re-states the code // Set the border width to 15 const BORDER_WIDTH = 15; // Good: explains the reasoning // Must match the design token in Figma (updated 2024-12) const BORDER_WIDTH = 15;
- Magic constants (e.g.
const BORDER_WIDTH = 15px) must have a comment explaining where this magic constant comes from and why it's required - Formatting should be handled by utility functions and only overwritten when necessary. When overwritten a comment should explain the use-case warranting it
- Label functions with good patterns with a comment
@goodpattern [short argument for why it's a good pattern] - If you make signifigant changes that affect our documentation. Then you must update the documentation.
- All decisions are documented with a very short and concise description in
DECISIONS.md. Each decision has a pullet-point, a date, the git-email of who made the decision and a quick summary. - Never add comments for sections. E.g. "styles", "implementation". That's noise.
- We must write type-safe code when using a type-safe language
- For data-models that span across boundaries (such as REST/API/RPC), use schemas
- Backend must validate user-input
- Frontend should validate user-input, preferably with a schema from the backend
- Be aware of security implications of code
- Backend must validate user-input
- Never mix async/await with
.then()syntax. Use one or the other. - Don't use
async function()when the function doesn't return a promise(like) response - Types should be inferred where possible. Explicitly set when necessary.
- Prefer
unknownoverany. Only useanywhere it's absolutely necessary. - Never type-cast (e.g.
as SomeType) unless necessary. Prefersatisfies SomeType.// Bad: silently discards type errors const config = { timeout: "500" } as Config; // Good: validates the shape while preserving the narrower type const config = { timeout: 500 } satisfies Config;
- Don't group exports with barrel files. It messes with tooling and can cause files to be initialised in the wrong order.
- Use Path-aliases. Never use
../file. Use~/where the~/refers to the project-root.// Bad import { formatDate } from "../../../utils/dates"; // Good import { formatDate } from "~/utils/dates";
- Use Zod or other standard-schema compatible library
- If there's overlap between TypeScript types and a schema definition; then infer the type from the schema type
// Bad: duplicates the shape, can drift out of sync const UserSchema = z.object({ name: z.string(), age: z.number() })type User = { name: string; age: number } // Good: single source of truth const UserSchema = z.object({ name: z.string(), age: z.number() })type User = z.infer<typeof UserSchema>
- Tests are for behaviour, not implementation specifics
- Avoid writing redundant tests
- Use test-matrices where applicable
- When implementing new features with non-trivial behaviours, write a unit-test first
- When refactoring old features with non-trivial behaviours, write a unit-test first
Verify your work in the fastest way possible. Or, in order:
- Check syntax, using editors or language-server-protocols (LSP)
- Check compiler output and address any errors.
- Check linter output. Address both warnings and errors.
- Run Unit-tests
- If the changes are web-UI, use Playwright to test the feature
- When working with APIs use
curlto test if the API is responding in the expected way - If an API has schema, validate against the schema
- Business logic should live on the backend, when possible
- Prefer server-rendering over client-side JavaScript
- Roundness, spacing, colours and font-usage should be consistent across the solution and adhere to the design system
- UI must look and feel consistent across the project
- User feedback is critical.
- If an operation is pending, it must be visible to the user. Prefer skeleton loaders, but spinners are also acceptable
- If an operation failed, it must be visible to the user
- If there was a validation error of user-input the error must be visible to the user
- Interactions that expect user attention should use animations
- Animations should be whimsical, subtle and not distracting
- Use CSS for animations and interactions over JavaScript where applicable
- Modals should not have modals
- Be aware of device margins and available view-port space
- If you make changes to translation keys, you must update translation files
- Users with screen readers should be able to use our solution
- Users who prefer reduced motion should not be shown animations
- Prefer using the web-platform (HTML/CSS) over framework specific solutions
- Use Tailwind v4 and look at similar components and our theme to get a feel for how it should look
- Gradually build out a design-system and extract our CSS into logical-files using @import where applicable.
- When class-names exceed ~80 characters, extract commonly used patterns into Tailwind utilities
- When we need animations, use transitions or animations
- Survey existing key-frames before adding a new one
- Don't use CSS features that are not "generally available" in mainline browsers. You can check https://caniuse.com to see how it's supported.
- Hook dependencies should not change during render. E.g.
new Date()creates a new object every render, causing infinite re-renders.// Bad: new object every render const [start] = useState(new Date()); useEffect(() => { fetch(`/api?since=${start}`); }, [start]); // Good: stable value const [start] = useState(() => new Date()); useEffect(() => { fetch(`/api?since=${start}`); }, [start]);
- Hook order must be consistent between re-renders. See React's rule of hooks.
- Never put multiple components in one file
- Never place multi-conditional rendering logic into JSX. Extract it into logical variables above the JSX instead.
- UI components should never use
useEffectdirectly. Create a custom hook for the use-case instead. - When a
useMemoimplementation is longer than 5 lines, move it into a custom hook.
- Use ternary with null instead of && for conditional rendering in React Native to avoid accidental text node rendering.
- Avoid state where possible. E.g. view-components or where letting HTML elements handle it is sufficient
- Components may use
useState()when it is necessary for the component to own the data being stored and the data stored is relatively simple.- When the complexity of state increases try to extract functionality into logical sub-components
- If that's not possible, try
useReducer()
- Page components should use page parameters, and URL state for state that belongs to the page
- Context can be used when we are prop-drilling (passing same props multiple levels of components) but otherwise avoided
- Global state should be avoided, and only be used when multiple areas of the system are modifying the same state. Consider state-management libraries, state-machines or state-charts where this is necessary.
- Filters should affect the level where they are applied. Sub-queries should have their own filters
- The graph should represent a business domain and use wordings from the business domain
- The graph should be traversable. E.g. using sub-entities instead of simply listing sub-entity-ids
- Be aware of how platform quirks can create bugs for other platforms. Create platform specific wrappers when necessary.