#
App Architecture
#
Directory Structure
When creating a new application using our React App Template, the main structure of the application is automatically created to satisfy our needs. The root folders that a developer is most commonly going to interact with are the following:
@types/: This folder should contain all TypeScript types that are shared across the suite and don't belong to a specific application. Before committing any types here, consider if it would be appropriate to include them in the mfb-types library instead.mocks/: This folder contains the instructions to setupmsw, a library that allow us to run the application in "mock mode", capturing all network requests and providing the caller with mocked responses. These settings should rarely change, the main file that needs to be updated by the developer ismockHandlers.ts, which bundles all handlers for the different apis used across the suite (seeAPI section ).src/: This is the main project folder that contains the application code, including the UI code (apps/andcomponents/), static assets such as images and.svgfiles, and general utilities and api code (lib/). The main focus of this document is the architecture of this folder.
#
The Source (src/)
The source folder should be organized as follows:
src/
├── apps/
├── assets/
├── components/
├── lib/
├── styles/
├── main.tsx
├── main.module.css
└── vite-env.d.ts
The core of the application code is within the apps/components/lib/
- Vite declarations (
vite-env.d.ts): Defines types for Node'sprocessvariable, mainly useful to type our environment variables. - Assets (
assets/): Stores static assets such as images and.svgfiles. - Styles (
styles/): Stores all global styles for the application. - Main Entry Point (
main.tsx): This file (and related styles inmain.module.css) is the entry point to the application. Here is where we set up all global React context providers (such asVinylProviderandAuthzProvider), and where we render the main , the root component that encompasses all our UI code.Appcomponent
#
General APIs and Utilities (lib/)
Check External Libraries
Before writing new code here, check our suite of supporting libraries (e.g. mfb-utils) for already existing functionality. If the needed utilities are not present already, consider adding them to the appropriate library.
This folder contains general hooks, APIs, and various utilities, to be used across the suite's applications, but not related to any specific one. This is the general structure:
lib/
├── api/
├── hooks/
├── utils/
├── constants.ts
└── enums.ts
- Constants (
constants.ts): Includes constants valid throughout the application suite. The main use is to include the values of our environment variables. These values should be exported as named exports. Enums (
enums.ts): Includes TypeScript enum variables. These should be exported as named exports. This file should include at least an enum for navigation purposes, describing the base path for the different applications in the suite, which should also match the application folders insideapps/(seeapplications section).export enum NavBasePath { SECURITY_MODEL = "security-model", EATTS = "eatts", //... }This will allow to use the enum values in code (e.g.
const base = NavBasePath.EATTS). If we want to also globally use the enum as a type (e.g. as a function param, or part of a type/interface), we need to include the type definition in the@types/index.d.tsfile found in the root of the project, using the following syntax:type NavBasePath = import("../src/lib/enums").NavBasePath;- Hooks (
hooks/) and Utils (utils/): Include various React hooks and JS/TS utility functions respectively, each of which should be a named export. Notably, here we can find thecreateAxiosInstanceutility function, used to setup global and application specific api clients (seehere ). - APIs (
api/): Includes all common API-related files and functions, not directly linked to any application. Mainly this will includeemployeeandsecurity-modelrelated apis. The internal structure of theapi/folder is going to be very similar to its application-specific counterparts.
#
Global Components (components/)
Check VinylEP
Before writing new components here, check if a compatible one is already present in the VinylEP library. If the component you want to add could be reused across different applications, consider adding it to VinylEP.
Here we can store general use components that are not provided by the Vinyl library. It should contain folders, one for every component, and an index.ts file to re-export them. Each component folder should be named to match the component name. Inside each folder should be an index.tsx file, with the component code exported as a named export, and optionally any other "private" children components or supporting stylesheets.
components/
├── ComponentName/
│ ├── index.tsx
│ └── styles.module.css
├── ...
└── index.ts
#
App Component and Navigation
The App component defines the overall layout of the suite, and sets up the main navigation between applications.
Layout: A
Layoutcomponent is generally defined here, which specifies the base layout and style of all pages, including aHeaderand anOutlet(usually wrapped in anErrorBoundary).The
Headercontains a dropdown to switch between applications, and a dynamic navbar with links to the different pages of each applications. (TODO: Update with new NavBar information). TheOutletwill be "populated" with the element determined by the navigation route.Navigation: We use
react-routerv.6 to perform routing. TheAppcomponent returns aRouterProvider, that takes in a router prop generated by thecreateBrowserRouterutiliy, which in turns takes in the app routes defined bycreateRoutesFromElements. Here we define the routes to the different applications, while the responsibility to define routing within each application is left to the applications themselves (seehere ). The general structure is as follows:import { app1Routes } from "@apps/app-1/Routes"; import { app2Routes } from "@apps/app-2/Routes"; const appRoutes = createRoutesFromElements( <Route path="/" element={<Layout />}> <Route index element={<Navigate to="app-1" replace />} /> <Route path="app-1/*">{app1Routes()}</Route> <Route path="app-2/*">{app2Routes()}</Route> // ... <Route path="*" element={<NotFound />} /> </Route> ); const App = () => <RouteProvider router={createBrowserRouter(appRoutes)} />;
#
Suite Applications (apps/)
Contains a sub-directory for every application in the suite. Each one will include all code related to the application, from UI to API related code, following this general structure (with small variations depending on each application's needs, e.g. added utils/ or hooks/ folders):
apps/
├── app-name
├── api/
├── components/
├── pages/
└── Routes.tsx
#
Routes
The Routes.tsx file defines the routing within the application, which will then be exported and used in the App component routing (see Route, with all sub-pages nested within. The route elements are lazy-loaded from the pages/ folder, and can either be rendered directly, or through a ProtectedRoute component if permissions are required (see
In the examples below, the importIndex function determines the lazy-load strategy.
Full application: Importing all pages at once from the
pages/index.tsfile will lazy-load them all together. Use the following at the top of the file:const importIndex = async () => import("./pages");Single pages: To lazy-load only a single page at a time, import the specific component within the lazy function in each
Route:<Route path="page" lazy={async () => { const importIndex = async () => import("./pages/PageComponent"); const { PageComponent } = await importIndex(); return { Component: PageComponent }; }} />
<Route
path="simple-pages"
lazy={async () => {
const { PageComponent } = await importIndex();
return { Component: PageComponent };
}}
/>
<Route
path="protected-page"
lazy={async () => {
const { Claims } = await import("./api/actions");
return {
element: (
<ProtectedRoute
deniedRoute="./NotFound"
requiredAction={Claims.ACTION_NAME}
/>
),
};
}}
>
<Route
index
lazy={async () => {
const { PageComponent } = await importIndex();
return { Component: PageComponent };
}}
/>
</Route>
#
Components
Includes all components that are reusable within the application, single-use components should be nested within their parent page/component. Each one should be in its separate folder, named after the component, and exported as a named export.
components/
├── Component1/
│ ├── index.tsx
│ └── styles.module.css
└── Component2/
├── PrivateComponent/
│ ├── index.tsx
│ └── styles.module.css
├── index.tsx
└── styles.module.css
#
Pages
Includes all pages of the application, organized in folders named after the page name. Depending on the lazy-loading strategy (see pages/index.ts file that would re-export all pages at once (lazy-loading all application pages together).
#
Api
Includes all API related files and functions specific to this application. Each application will have its own dedicated Axios API client, created using the createAxiosInstance utility function (see api/index.ts file, and imported as needed into the various resources hook files.
// api/index.ts
export const applicationAxiosInstance = createAxiosInstance(
APPLICATION_API_BASE_URL
);
Each API resource should have a separate folder with the same name as the resource itself (in its plural version), which should contain all related items, such as hooks, types, mocks and mock handlers. The api/ folder should also include a file listing the definition of all api routes/endpoints, a file with the application actions, and all types shared amongst the different resources (if necessary).
api/
├── applications/
│ ├── index.ts
│ ├── hooks.ts
│ ├── mock.ts
│ ├── mockHandlers.ts
│ └── types.ts
├── roles/
│ └── ...
├── users/
│ └── ...
├── index.ts
├── apiRoutes.ts
├── actions.ts
└── types.ts
api/types.ts: Includes all types that are shared across multiple resources. If types can be shared across applications, consider placing them in the@typesfolder, or in themfb-typeslibrary.api/actions.ts: Includes all actions related to this application's permissions. Defines anenumwith all actions names, and an object based on these actions, created using thecomposeApplicationClaimsutility function, which combines the application guid (found in the environment variables) with each action name. These claims are used across the application to restrict access to certain pages or functionality within pages (see an example in theRoutes section ).enum ApplicationActions { READ = "Read-Action", CREATE = "Create-Action", }; const ApplicationClaims = composeApplicationClaims(APPLICATION_API_GUID, ApplicationActions); /* Result >> ApplicationClaims = { READ: 'guid:Read-Action', CREATE: 'guid:Create-Action', } */api/apiRoutes.ts: Includes an object containing all possible API endpoints used across the application. These endpoints are defined here so they can be reused in hooks, mocks and possibly tests, and are typically exportedas const, so that TypeScript will type them with their constant string value rather than plainstring.This object should contain fields named after each API resource (in its singular version), and each field should be another object with all endpoints related to that resource. In the case of nested enpoints, include a
contextfield, that would define a set of endpoints "in the context of" the parent resource.Example with two resources, Application and Roleexport const ApiRoutes = { application: { getAll: "/v1/application", // Get all application getByRef: "/v1/application/:ref", // Get application by 'ref' context: { getAllRoles: "v1/application/:ref/role", // Get all roles "in the context" of one application }, }, role: { getAll: "/v1/role", // Get all roles }, } as const;api/index.ts: This file will define the Axios instance, and re-export all type, actions and resource related files to be used across the application.export const applicationAxiosInstance = createAxiosInstance( APPLICATION_API_BASE_URL ); export * from "./resource1"; export * from "./resource2"; // ... export * from "./actions"; export * from "./types";api/<resource-name>/types.ts: Includes all types related to this resource. Typically will include a type for the resource itself, and multipleArgsandResponsetypes for the different API endpoints. For theResponsetypes, prefer using theMidlandResultgeneric type frommfb-typeslibrary, e.g.type GetResource = MidlandResult<Resource[]>;.api/<resource-name>/hooks.ts: Contains all hooks to interface with the API layer. Define query keys to cache the results of api calls through@tanstack/react-query, and export each hook as a named export.api/<resource-name>/mockHandlers.ts: Defines a list of mock handlers, matching the API hooks defined in the./hooks.tsfile, so that when running the application in mock mode,mswcan intercept the HTTP requests and respond with the appropriate mock object (defined in the./mocks.tsfile).api/<resource-name>/mocks.ts: Defines all mock objects that should be returned by the api hooks when running the suite in mock mode.api/<resource-name>/index.ts: Exports everything from the resource folder for ease of use across the application.