Key Frontend Architectural Patterns for Complex Applications
The most important programming-style and architectural decisions I make as a frontend (or full-stack) developer building a complex application:
-
TypeScript types are the glue that holds the application together and ensure changes can be made quickly later when you revisit older code. Server-client communication is secured with end-to-end type-safety (also a classic source of bugs in applications — the API changes but you forget to update the dependent clients).
-
Domain-driven design is the architectural foundation for any complex application, as it ensures related functionality lives together and is not spread across distant and overcrowded folders. Concretely this means a subfolder in
src/modules/(or whatever you prefer) that contains everything related tousers,subscriptions,posts, and so on. Each module is built as follows:
src/
- modules
- users/
- components/
- stores/
- utils/
- actions/ (end-to-end type-safety endpoints a la AstroJS)
... other folder or files depending on application
- subcriptions/
- authentications/
The database, infrastructure, and shared UI components are used by the modules but reside independently in a parent folder.
-
Abstract data state away from the UI components. React is a presentation library that shines when it contains rendering logic that only changes depending on the
propsit receives. When data state is abstracted, UI components become much simpler and greatly reduce the risk of bugs (useEffectis a classic example when data manipulation is pulled into a React component and is a major source of bugs). -
Pure functions are easy to test, easy to write, and easy to replace. Extract complicated business or data state logic into pure functions, isolated from I/O operations.
-
Avoid library bloat by evaluating whether you can write the added functionality yourself, and by checking a library’s dependencies in
package.json. If it has too many, it contributes to making the application heavy and increases the risk of supply-side attacks. Often there are lightweight alternatives. -
Avoid over-engineering solutions with libraries that hide complexity. Prefer honest and explicit complexity over over-engineering a solution with libraries. An example of this is the advanced data fetching and state management library Apollo, which offer a large API but is difficult to easily customize and you have to look up the API often. As an alternative, just write your own
getSubscriptionStatus(with end-to-end type-safety), add the response a state management library like Nanostores or Redux or Zustand. -
Avoid abstracting functions too quickly; code repetition makes any future abstraction clearer and better.