DEV Community

axtk
axtk

Posted on

Fractal web app design

Motivation

The design discussed here is the result of my attempt to come up with a simple, self-explanatory, and scalable web app structure (primarily for a Node.js app), ultimately comfortable to work with. I've found these qualities with a self-similar structure, hence the name fractal design.

By scalability I mean here mostly the following things:

  • the app should be able to evolve seamlessly from a smaller app to a larger one;
    • preferably without restructuring the app much as its size changes;
    • preferably without imposing too much complexity ahead of time in anticipation of the app's potential growth;
  • to reflect the reality, the app should preferably be able to maintain multiple entry points implementing different rendering strategies (such as SSR, CSR) or using some legacy tech running on the same server;
    • entry points should be loosely coupled and self-contained so that connecting and disconnecting an entry point should be nearly effortless.

Key points

For our comfort, the code should be scalable (in the sense outlined above) and easy to navigate. Here's what can be done for that purpose:

  • Single export per file. Several tightly related type exports alongside the main export (such as a function's parameters type) are allowed. (Single-responsibility principle)
  • Files are named exactly as their export. With the same casing. File names should say exactly what they mean to facilitate browsing the codebase.
  • Entry points replicate the basic file structure of the app. Entry points can be regarded as smaller self-contained quasi-apps.
  • Cross-entry-point imports are discouraged. Files shared by multiple entry points should be lifted to a shared directory.

For the sake of scalability, it makes sense to use non-barrel index files. The effective equivalence of the path locations dir/x/index.ts and dir/x.ts as 'dir/x' in imports allows:

  • to scale up from a single file to a collection of files without changing imports throughout the app;
  • to avoid the tautology like import {Component} from 'ui/Component/Component';.

index files should be used as ordinary files with a single non-type export named exactly as the directory, with the same casing. They shouldn't be used as barrel files just for re-exports. The primary use case for an index file alongside a bunch of other files is a module with a number of local dependencies to that module.

Structure

This approach to structuring the app is tech-stack-agnostic, but here, for the sake of clarity, we are going with an Express server.

The fractal web app design essentially boils down to the following file structure:

(Alternatively, it can be viewed here.)

So, the application code resides in the src directory, the app's server code is in src/server.

Note how the entry points in the src/entries directory replicate the app's structure. Each entry point is a self-contained unit exporting an instance of Express Router from its own server file. An entry point's Express Router can be easily plugged in and out in the app's server, regardless of the tech inside the entry point.

A few typical use cases for different entry points include: an older and newer tech stack in the same app, an outdated and fresher UI within a single app, a main app with a lighter marketing landing page or a user onboarding app, as well as multiple self-contained portions of a single app. All of those can be served at different routes from different entry points with clear boundaries.

The recurrent set of auxiliary directories like const, types, utils can be used almost at any level to contain constants, types, utility functions shared at that level. Again, it should be one non-type export per file named exactly after that export.

Public assets can also be split across entry points and served independently by each entry point through their own public directories to maintain clearer boundaries between the entry points' resources. Nonetheless, to avoid duplication, resources shared across multiple entry points can be located in the app's top-level src/public directory served by the app's server itself.

Finally, the lib directories (such as src/lib) are to contain directories acting like standalone packages: either something to be published soon as a package (which might take time in real life due to various reasons) or patched versions of external libs.

Top comments (0)