|
| 1 | +| ← Previous | ↑ | Next → | |
| 2 | +| :--------- | :-------------------------------: | ------------------------------------------: | |
| 3 | +| - | [Go to index](../README.md#index) | [Routes and shell](./2.routes-and-shell.md) | |
| 4 | + |
| 5 | +# Architecture |
| 6 | + |
| 7 | +This simple example app is built on [the base template](https://github.com/FullStacksDev/angular-and-firebase-template/blob/main/README.md). Make sure to go through the base template's [README](https://github.com/FullStacksDev/angular-and-firebase-template/blob/main/README.md) and [ARCHITECTURE](https://github.com/FullStacksDev/angular-and-firebase-template/blob/main/ARCHITECTURE.md) docs beforehand, to get an understanding of the tech stack components, high level architecture and design decisions, then come back here to learn the specifics of this app. |
| 8 | + |
| 9 | +## Frontend |
| 10 | + |
| 11 | +This simple example app is frontend-heavy in that the majority of the functionality is built client-side, in the Angular app, which consists of: |
| 12 | + |
| 13 | +- A new [`app/src/app/logbook`](../app/src/app/logbook) folder containing the logbook feature (detailed below). |
| 14 | +- A new [`app/src/app/shared/models.ts`](../app/src/app/shared/models.ts) file where types are defined for the data model (and other related types), just for use on the frontend. |
| 15 | +- Updated routes in the [`app/src/app/app.routes.ts`](../app/src/app/app.routes.ts) file to lazily load the logbook feature routes. |
| 16 | +- A link to "Open logbook" in the navigation of the website's static pages (i.e. in [`app/src/app/website/website-shell.component.ts`](../app/src/app/website/website-shell.component.ts)). |
| 17 | + |
| 18 | +| **:white_check_mark: Pattern** | |
| 19 | +| :-- | |
| 20 | +| We don't use [Angular modules (i.e. `@Module`)](https://angular.dev/guide/ngmodules) for our own code — we've chosen to go all-in on [Angular's recent **standalone** approach](https://angular.dev/guide/components/importing#standalone-components). So we only ever define (and prefer to import) standalone components, directives, etc.<br><br> The base template has configured the Angular CLI generator to always set the `standalone: true` flag on any components, directives, etc. you generate. | |
| 21 | + |
| 22 | +| **:brain: Design decision** | |
| 23 | +| :-- | |
| 24 | +| Since this app is frontend-heavy we've decided to put all the data model types (and other useful types) in a file within the `app` folder, as opposed to putting these in [`firebase/common/models.ts`](../firebase/common/models.ts) (as provided by the base template) where they would be available to both the frontend and backend. | |
| 25 | + |
| 26 | +> [!NOTE] |
| 27 | +> |
| 28 | +> In the advanced example app we show how to share types between the frontend and backend. |
| 29 | +
|
| 30 | +### The logbook feature |
| 31 | + |
| 32 | +```text |
| 33 | +app/src/app/logbook |
| 34 | +└─ data |
| 35 | + └─ db |
| 36 | + └─ {db services} |
| 37 | + └─ {stores} |
| 38 | +└─ feature |
| 39 | + └─ {pages and smart components} |
| 40 | +└─ ui |
| 41 | + └─ {presentational components} |
| 42 | +└─ logbook-shell.component.spec.ts |
| 43 | +└─ logbook-shell.component.ts |
| 44 | +└─ logbook.routes.ts |
| 45 | +``` |
| 46 | + |
| 47 | +- The routes and shell files are in the root of the logbook feature folder. |
| 48 | +- All other files are split between `data`, `feature` and `ui` subfolders. |
| 49 | + - The `data` folder contains the services and stores that handle the data layer. |
| 50 | + - All services that wrap Firebase access are in the `data/db` subfolder. |
| 51 | + - The `feature` folder contains the pages and smart components that make up the feature itself. |
| 52 | + - The `ui` folder contains presentational components that are used by the feature. |
| 53 | + |
| 54 | +We'll dig into these in more detail in later documents. |
| 55 | + |
| 56 | +> [!NOTE] |
| 57 | +> |
| 58 | +> Think of **smart components** as more involved components that have access and awareness of the broader application state and logic, via stores (and other services). They don't function just as black boxes and are not usually reusable across different parts of the app. |
| 59 | +> |
| 60 | +> Think of **presentational components** as simple and naive components that only know their inputs and outputs, making no assumptions of the overall application state and structure. They should be easy to test (as a black box) and easy to reuse. |
| 61 | +
|
| 62 | +| **:white_check_mark: Pattern** | |
| 63 | +| :-- | |
| 64 | +| As mentioned in the base template, we highly recommend separating the code within the top-level feature folders into the following subfolders: `data`, `feature`, `ui` and `util`. And trying to keep both the top-level feature folders and these subfolders at one hierarchical level. We've found that this is a great starting folder structure (and general architecture) which helps you quickly find stuff, whilst spending minimal time on figuring out what goes where.<br><br>Page and smart components go in the `feature` folder, whilst presentational components go in the `ui`folder.<br><br>This is a recommended folder structure based on [Nx's suggested library types](https://nx.dev/concepts/more-concepts/library-types).<br><br>For features within the `shared` folder you should follow the same structure, except you probably won't need a `feature` subfolder since these are shared bits of code for use elsewhere.<br><br>As things grow you may need to adapt and tweak this structure (e.g. to add another level in the hierarchy) — we'll see how to tackle this in the advanced example app. | |
| 65 | + |
| 66 | +> [!IMPORTANT] |
| 67 | +> |
| 68 | +> As a reminder, all the Angular components (including ones generated through the Angular CLI) have been configured to use the [`OnPush` change detection strategy](https://angular.dev/best-practices/skipping-subtrees#using-onpush) by default. |
| 69 | +> |
| 70 | +> This is a more performant approach that [works well with Angular's signals](https://angular.dev/guide/signals#reading-signals-in-onpush-components), and since we use NgRx SignalStore you are unlikely to hit the cases where change detection is not triggered when it should be. |
| 71 | +> |
| 72 | +> With the caveat that forms _sometimes_ don't behave well with OnPush change detection, so in rare cases you'd need to use the `ChangeDetectorRef` to manually mark a component for change detection — this is something we'll explore in the advanced example app. |
| 73 | +> |
| 74 | +> As long as you stick to the approaches promoted in the example apps you should not encounter any change detection issues (i.e. where underlying data changes but the UI does not update). |
| 75 | +
|
| 76 | +### Data flows, app logic and UI components architecture |
| 77 | + |
| 78 | +Within the frontend app, it's important to have an architecture in place for reasoning about data flows, app logic and UI components with some "rules" to make things predictable and easy to scale up with more features, and manage growing complexity. Thus, knowing where things go and how data flows between backend, services, and components is crucial. |
| 79 | + |
| 80 | +We highly recommend the following generalized data and logic flows — we follow this extensively in the example apps: |
| 81 | + |
| 82 | +```mermaid |
| 83 | +sequenceDiagram |
| 84 | + participant Db as Db and external access services<br />(within the `/data/db` folder) |
| 85 | + participant S as Stores<br />(within the `/data` folder) |
| 86 | + participant SC as Page and smart components<br />(within the `/feature` folder) |
| 87 | + participant PC as Presentational components<br />(within the `/ui` folder) |
| 88 | + S->>Db: Access and update things<br/>(usually via observables) |
| 89 | + SC->>S: Bind to state<br/>(using signals) |
| 90 | + SC->>S: Trigger app logic<br/>(via store methods) |
| 91 | + S->>S: Update state |
| 92 | + SC->>PC: Provide input data |
| 93 | + PC->>SC : Emit events |
| 94 | +``` |
| 95 | + |
| 96 | +(Ignore the time-ordering of this sequence diagram, it's just a way to visualize the data flow — it's not a strict sequence of events.) |
| 97 | + |
| 98 | +- Use Angular services to wrap ALL access to databases and external services. |
| 99 | +- Use state management "stores" to encapsulate as much of the app's state and behavior as possible, leaving components to focus on UI needs and responding to state changes. |
| 100 | +- Use smart components to interact with stores to bind state and trigger application logic. |
| 101 | +- Use presentational components (within the template of smart components) to abstract out UI presentation and logic in a way that does not need to know about the overall application state and structure, communicating all actions/events back to the parent smart component. |
| 102 | + |
| 103 | +We'll cover these in more detail, in the context of the simple logbook feature, in later documents. |
| 104 | + |
| 105 | +### Stores |
| 106 | + |
| 107 | +> [!IMPORTANT] |
| 108 | +> |
| 109 | +> As you can see from the previous diagram, stores are the main mechanism for state management and can be considered the central _hubs_ or _engines_ for your application's state and logic. They are the single source of truth for the app's state and are the only place where this state is updated, as well as being the only place where related app logic is triggered. This makes it easy to reason about the app's state and behavior, and to test and debug it, by decoupling it from the UI. |
| 110 | +
|
| 111 | +> [!TIP] |
| 112 | +> |
| 113 | +> Another approach to thinking about stores and state management is, rather than thinking: |
| 114 | +> |
| 115 | +> _"How do these actions and events change the user interface?"_ |
| 116 | +> |
| 117 | +> … think: |
| 118 | +> |
| 119 | +> _"How do these actions and events change the state of the application? How can I use this state to drive UI and flows, or what additional state do I need to model?"_ |
| 120 | +
|
| 121 | +There are different levels of stores, scoped to particular contexts: |
| 122 | + |
| 123 | +- **Global stores** — a single instance available to be injected in any component or service. |
| 124 | + - Marked with `providedIn: 'root'` in the `@Injectable` decorator so Angular knows to make them available globally and only maintain one instance. |
| 125 | + - The [`app/src/app/shared/auth/data/auth.store.ts`](../app/src/app/shared/auth/data/auth.store.ts) (provided by the base template) is an example of a global store. |
| 126 | +- **Feature stores** — provided at the route level (for lazily loaded feature routes) and available to all components within the feature. |
| 127 | + - The stores in the logbook feature are feature stores — they are provided at the route level (covered in a later document). |
| 128 | +- **Component stores** — provided and injected in a component and available to it and its children only. |
| 129 | + - Can be provided in multiple components, where Angular will create and maintain _separate_ instances. A useful way to have a store that is scoped to a particular part of the UI, but also reused in multiple places (e.g. a component store for a list item). |
| 130 | + - The [`app/src/app/login/feature/login-flow.store.ts`](../app/src/app/login/feature/login-flow.store.ts) is an example of a component-level store. |
| 131 | + |
| 132 | +> [!TIP] |
| 133 | +> |
| 134 | +> When we say an Angular "service" (or "store" — which is just an Angular service under the hood) is "provided" somewhere we mean that it's registered with Angular's dependency injection system so that it can be injected into the services and components within the scope of that provider context (i.e. either globally, or within a smaller context like a particular set of lazily loaded routes or a particular component tree). |
| 135 | +> |
| 136 | +> You can read more about Angular's dependency injection system in [the official guide](https://angular.dev/guide/di). |
| 137 | +
|
| 138 | +> [!NOTE] |
| 139 | +> |
| 140 | +> Components (both smart and presentational) can sometimes manage their own internal state directly if having a separate component-level store is overkill. For example, a simple form component that manages all form state directly within the component and doesn't _really_ benefit from a separate store attached to the component. |
| 141 | +> |
| 142 | +> Though note that once the component starts to grow in complexity, it's often a good idea to move the state management to a separate component-level store, to keep the component focused on UI concerns. |
| 143 | +
|
| 144 | +## Backend |
| 145 | + |
| 146 | +We rely on [Firebase's security rules](https://firebase.google.com/docs/rules) to control access to Firestore and Realtime Database from the frontend. |
| 147 | + |
| 148 | +We use [Firestore indexes](https://firebase.google.com/docs/firestore/query-data/indexing) to support the queries we make from the frontend and disable ones we don't need (to avoid unnecessary costs). |
| 149 | + |
| 150 | +We don't make use of Firebase Functions in this simple example app, partly so it can run within Firebase's "no-cost" tier, and also to showcase how to build a simple (but capable) web app without them. |
| 151 | + |
| 152 | +> [!IMPORTANT] |
| 153 | +> |
| 154 | +> If you're used to a more traditional client-server access model where the server controls all access to a database via an API then Firebase's approach of making direct database calls from the client-side may seem counter-intuitive (and even scary) at first. If it helps, consider that there is still _some form of an API_ that these database calls go through — the security rules, which essentially encode the business logic on what can and cannot be accessed, in its own domain specific language (albeit limited to access and basic validation). |
| 155 | +> |
| 156 | +> You can still achieve a more traditional server-side API style with Firebase Functions and the Firebase Admin SDK, but we don't use that in this simple example app. |
| 157 | +
|
| 158 | +> [!NOTE] |
| 159 | +> |
| 160 | +> We use Firebase functions extensively in the advanced example app, for capabilities that need proper backend support and can't be achieved with just client side code and Firebase's security rules. |
| 161 | +
|
| 162 | +> [!WARNING] |
| 163 | +> |
| 164 | +> It's best to have as much encoded in the codebase as possible, especially the security rules and indexes, in their relevant files, which will be pushed to Firebase as part of the deployment run. |
| 165 | +> |
| 166 | +> So don't make changes to these directly in the Firebase console, as they _might_ be lost on the next deploy, or you may lose track of what is live and end up with multiple sources of truth. |
| 167 | +
|
| 168 | +--- |
| 169 | + |
| 170 | +| ← Previous | ↑ | Next → | |
| 171 | +| :--------- | :-------------------------------: | ------------------------------------------: | |
| 172 | +| - | [Go to index](../README.md#index) | [Routes and shell](./2.routes-and-shell.md) | |
0 commit comments