Skip to content

Architecture

The following patterns will keep the frontend code maintainable long-term.

We organize files by their domain (the feature), not their technical role (the file type or technology).

Feature folders

Rule: Group modules according to the feature they belong to.

Adding, removing, or evolving one feature should only affect one slice of the app. Changing one feature must not affect another. If a contributor wants to fix the Audio Player, they should only need to look in ~/audio and ~/layout, rather than hunting through global components, stores, and views folders.

File-based routing

See Issue #2522 refactor(front): Simplify routes for better DX

Designing Modules

Components, functions and code blocks should have precise and meaningful interfaces (inputs/outputs).

Deep modules, narrow interfaces

Rule: Place complex, self-contained logic into a single module, but keep its interface (props, emitted events, exports) very strict and narrow. Implement exactly the behavior one would expect when reading the interface.

You only need to read a module's interface to understand how to use it. Long and complex modules and are fine if they behave according to the simple and explicit API. Complex logic inside a template is fine if it is what you'd expect when reading the interface. Long modules are fine as long as the scope is covered by the interface.

Example: The Tokens component routes all events through a single state machine with clearly defined states and transitions, making it easier to debug despite its complexity. The interface (props, model, imports) remains small and declarative.

Locality over DRY

Rule: Keep variables, constants, and closures physically close to where they are used.

When variables have limited scope, we don't need to keep pointers in mind while parsing modules. Local and short relations are preferable over circuitous dependencies.

  • If functionality is repeated in several modules, it has a very small API, and it is easy to explain in words, factor it out. Otherwise, do not precompute results to reuse later; copy the closure.
  • Use DRY only when the intermediate result is meaningful on its own.

Principle of least power

Rule: Always use the least powerful tool necessary to achieve the outcome.

Less powerful tools have fewer side effects and are easier to parse.

  • CSS is better than JS for purely presentational logic
  • Template logic is better than script logic; avoid View logic in the script section.
  • Pure functions are better than stateful functions
  • Local state is better than global state

Declaring state and data-flow

Data flows should be unidirectional, direct, and explicit to avoid situations where two sources of truth compete

Parse, don't validate

Rule: Parse data early at the boundaries (API/Props) and give it the tightest possible shape.

  • Make unwanted states unrepresentable.
  • Avoid defensive programming deeper in the application, which adds noise.
  • Model error states precisely where they are needed for UX or API communication.

Default early, derive late

Rule: Cancel unneeded code paths as early as possible (via early returns or defaulting), but compute derived values as late as possible.

Avoids a soup of intermediate, dangling variables in your script.

Example: Default props using the destructuring pattern. If you need a derivative of a prop, instead of using computed in the script, write the formula directly in the template.

TypeScript and Vue bugs in defineProps

Model states, not individual mutations

Rule: Treat complex logic as a state machine with named states and transitions (events).

It prevents impossible UI states (like isLoading while having an error simultaneously), infinite loops and dead code. And it makes complex behavior manageable.

Example: Instead of separate refs isLoading.value = false; data.value = results, use a strongly typed state.value = { ...state.value, type: success, data: results }

Natural and canonical names

Rules: Stick to domain vocabulary and canonical names. Never abbreviate (index instead of idx) and never include the type in the name (names instead of nameArray). Choose names that make sense in the domain of the module, not the technology or format you use (audio instead of file or result). Where possible, use the same (canonical) name for the same data, beyond module boundaries. When working with API types, always use the original API name instead of renaming it.

Consistent names convey the intention and scope of your code and make it easier to trace data-flows.