The following represents a proven and scalable approach to structuring modular web apps. It is conceptually simple, relying on a few well defined structural patterns, some of which are repeated in a recursive fashion. A structure like this will often look much like the domain of the app, making it very easy to navigate, understand and reason about.
All file and folder names must be lower case and the only word separator allowed is a single -
.
The only exceptions are tests, which must be named {name}.test.ts
, and localized files, which must be named {name}.{locale}.{ext}
.
In the following, -
and +
indicate expanded and collapsed folders, while ...
denote additional unspecified items.
.
├ - packages
│ ├ + cloud
│ ├ + desktop
│ └ + mobile
├ - src
│ ├ - app
│ │ ├ + components
│ │ ├ + modals
│ │ ├ + modules
│ │ ├ + resources
│ │ ├ + services
│ │ ├ + toasts
│ │ ├ + types
│ │ ├ app.html
│ │ ├ app.scss
│ │ └ app.ts
│ ├ - resources
│ │ ├ + fonts
│ │ ├ + icons
│ │ ├ + images
│ │ ├ + experiments
│ │ ├ + integrations
│ │ ├ - settings
│ │ │ ├ index.ts
│ │ │ └ ...
│ │ ├ + stubs
│ │ │ ├ + responses
│ │ │ └ index.ts
│ │ ├ - styles
│ │ │ ├ + framework
│ │ │ ├ + resources
│ │ │ ├ + settings
│ │ │ ├ index.scss
│ │ │ └ index.ts
│ │ ├ - themes
│ │ │ └ + ...
│ │ └ - translations
│ │ ├ translations.json
│ │ ├ translations.{locale}.json
│ │ └ ...
│ ├ - shared
│ │ ├ - framework
│ │ │ ├ + components
│ │ │ ├ + converters
│ │ │ ├ + services
│ │ │ ├ - styles
│ │ │ │ ├ + foundation
│ │ │ │ ├ + framework
│ │ │ │ ├ + resources
│ │ │ │ ├ + settings
│ │ │ │ ├ index.scss
│ │ │ │ └ index.ts
│ │ │ ├ index.scss
│ │ │ ├ index.ts
│ │ │ └ readme.md
│ │ ├ - infrastructure
│ │ │ ├ + ...
│ │ │ ├ index.ts
│ │ │ └ readme.md
│ │ ├ - localization
│ │ │ ├ + ...
│ │ │ ├ index.ts
│ │ │ └ readme.md
│ │ ├ - patches
│ │ │ ├ + ...
│ │ │ ├ index.ts
│ │ │ └ readme.md
│ │ ├ - types
│ │ │ ├ + ...
│ │ │ ├ index.ts
│ │ │ └ readme.md
│ │ └ - utilities
│ │ ├ + ...
│ │ ├ index.ts
│ │ └ readme.md
│ ├ - typings
│ │ ├ + extensions
│ │ ├ + fixes
│ │ ├ + globals
│ │ └ + modules
│ ├ env.ts
│ ├ index.ejs
│ ├ index.scss
│ └ index.ts
├ + tools
├ package.json
├ readme.md
├ stylelint.json
├ tsconfig.json
└ tslint.json
This contains packages that act as hosts for the app. For example, the cloud
package might implement a Node Express server, while the desktop
package might implement an Electron app, and the mobile
might implement a Cordova app. Note that you generally won't need those packages while developing the app itself, as development should happen using a simple development server.
This is a module
folder, representing the root app module.
Note that this could be broken out into separate packages, shared between multiple projects.
-
The
framework
folder, containing shared components and styles. This should be implemented such that it could theoretically be reused in other apps without changes. Note that you should never import anything from this folder, other thanindex.ts
andindex.scss
. -
The
infrastructure
folder, containing shared infrastructure code. This should be implemented such that it could theoretically be reused in other apps without changes. Note that you should never import anything from this folder, other thanindex.ts
. -
The
patches
folder, containing any monkey-patches needed to patch issues found in dependencies. -
The
types
folder, containing shared types that are not specific to the app. This should be implemented such that it could theoretically be reused in other apps without changes. Note that you should never import anything from this folder, other thanindex.ts
. -
The
utilities
folder, containing shared utilities that are not specific to the app. -
This should be implemented such that it could theoretically be reused in other apps without changes. Note that you should never import anything from this folder, other than
index.ts
.
-
The
experiments
folder, containing the settings and implementations of any experiments being conducted. Each experiment must be implemented in its own folder. -
The
integration
folder, containing any files needed to integrate with browsers, devices or services. An example of this would be thefavicon.ico
file, manifests and similar resources. -
The
locales
folder, containing files that define the formatting settings for each locale. Only the one matching the current locale will be loaded by the app. -
The
settings
folder, containing the settings for the app, infrastructure and framework. Note that feature settings should live in the individual modules and components. This is primarily intended for fundamental settings, such as API base URLs, supported markets and locales, etc. -
The
styles
folder, containing app-specific styles used across the app. This is structured similar to the shared styles. -
The
themes
folder, containing app-specific themes, each in its own folder, including any associated resources. -
The
stubs
folder, containing stubs used during development, e.g. to fake responses from API endpoints that do not yet exist. -
The
translations
folder, containing files that provide the translated content that is injected into view templates and string files during the localization process. Those files will be imported from a translation management system and should therefore not be edited manually. For more information, see gulp-translate.
This contains any tools needed for development, such as build scripts.
Note that for very large apps, modules could be broken out as separate packages, maintained by separate teams.
A module represents a coherent subset of the domain, and contains all the components, modals, pages, resources, services and types related to it.
Modules may reference each others components, modals and services as needed.
- {module-name}
├ + components
├ + modals
├ + pages
├ + resources
├ + services
└ + types
Note that you may use sub-folders under the
components
folder to group closely relatedcomponent
folders. Use this if you have e.g. multiple variations of some card component, and want to group them together.
A component represents a a custom element or attribute used within a view. It should be as self-contained as possible, and may contain sub-components, each structured in the same way.
Sub-components are components that are used internally in the template for the component, but which are not exposed to consumers. For example, while a page-header
component might internally use a logo
component, that is an implementation detail the consumer don't need to know about.
Some components may need to expose related components. For example, a tabs
component might expose both a tabs
and tab-pane
component, both of which are needed to consume the component as a whole. In such cases, the files for the related components should be placed together with the files for the primary component.
- {component-name}
├ + components
├ + modals
├ + resources
├ + services
├ {component-name}.html
├ {component-name}.scss
├ {component-name}.ts
├ {optional-related-component-name}.html
├ {optional-related-component-name}.scss
├ {optional-related-component-name}.ts
└ ...
A page represents a page view with an associated route, usually presented in a <router-view>
. It should be as self-contained as possible, and may contain sub-pages, each structured in the same way.
- {page-name}
├ + components
├ + modals
├ + pages
├ + resources
├ + services
├ {page-name}.html
├ {page-name}.scss
├ {page-name}.ts
└ routes.ts
A modal represents a modal view, such as a dialog, panel or overlay. It should be as self-contained as possible, and may contain sub-modals, each structured in the same way.
- {page-name}
├ + components
├ + modals
├ + resources
├ + services
├ {modal-name}.html
├ {modal-name}.scss
└ {modal-name}.ts
Note that, although generally not recommended, you may use sub-folders under the
services
folder to group closely related services.
Services are injected into view models, allowing the view models to contain only interaction logic and temporary state scoped to that view.
Generally, an app will have two types of services:
- Services that implements and manage a part of the domain model for the app.
- Services that implements and manage state that is specific to some module or component.
Services should be carefully architected to separate concerns, and should be as self contained as possible, with each service exposing its public API through a single index.ts
file. Each service will typically define the domain models it manages, with the exception of very general types, which may reside in one of the types
folders.
Prefer creating more specialized services, rather than creating a few services that take on too many responsibilities.
A resources
folder may exist at the root level, module level and component level, as well as under services. Resources represent assets, such as images, icons, videos and fonts, as well as things that could be said to be consumed by the module or component, such as settings, or localizable things, such as .json
files containing strings for use in models and services.
- resources
├ + fonts
├ + icons
├ + images
├ + settings
├ + strings
├ + videos
└ + ...
Note that the resources
folder at the root level has different content.
An experiment is any kind of test we wish to perform, e.g. to evaluate the user and business impact of a new design for some component. Experiments have two parts:
- An
experiment
folder, which contains the implementation of any framework configuration, components, or services needed for the experiment. - The code that imports and integrates the experiment in the affected parts of the app.
Conceptually, the idea is, that essentially all code related to the experiment lives in this folder, and only if the experiment proves to be a success, will time be allocated to implement it properly in the app itself. This allows experiments to be created with less concern for long term maintainability, and more of a copy-paste-modify approach, as they won't pollute the code base for the app itself - at least not beyond a few simple condition to use e.g. an alternative component implementation provided by the experiment. And when an experiment
folder is deleted, build errors will reveal all the places in which it was integrated, making cleanup easier.
An experiment
folder must be structured exactly like this, and the index file must export a configure
function, so they can be loaded and configured as features in Aurelia:
- {experiment-name}
├ + components
├ + resources
├ + services
├ index.ts
└ readme.md