# 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:

  1. @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.
  2. mocks/: This folder contains the instructions to setup msw, 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 is mockHandlers.ts, which bundles all handlers for the different apis used across the suite (see API section).
  3. src/: This is the main project folder that contains the application code, including the UI code (apps/ and components/), static assets such as images and .svg files, 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/ and lib/ folders. Here is a brief description of the rest:

  1. Vite declarations (vite-env.d.ts): Defines types for Node's process variable, mainly useful to type our environment variables.
  2. Assets (assets/): Stores static assets such as images and .svg files.
  3. Styles (styles/): Stores all global styles for the application.
  4. Main Entry Point (main.tsx): This file (and related styles in main.module.css) is the entry point to the application. Here is where we set up all global React context providers (such as VinylProvider and AuthzProvider), and where we render the main App component, the root component that encompasses all our UI code.

# General APIs and Utilities (lib/)

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
  1. 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.
  2. 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 inside apps/ (see applications 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.ts file found in the root of the project, using the following syntax:

    type NavBasePath = import("../src/lib/enums").NavBasePath;
  3. 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 the createAxiosInstance utility function, used to setup global and application specific api clients (see here).
  4. APIs (api/): Includes all common API-related files and functions, not directly linked to any application. Mainly this will include employee and security-model related apis. The internal structure of the api/ folder is going to be very similar to its application-specific counterparts.

# Global Components (components/)

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 Layout component is generally defined here, which specifies the base layout and style of all pages, including a Header and an Outlet (usually wrapped in an ErrorBoundary).

    The Header contains 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). The Outlet will be "populated" with the element determined by the navigation route.

  • Navigation: We use react-router v.6 to perform routing. The App component returns a RouterProvider, that takes in a router prop generated by the createBrowserRouter utiliy, which in turns takes in the app routes defined by createRoutesFromElements. Here we define the routes to the different applications, while the responsibility to define routing within each application is left to the applications themselves (see here). 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 here). Each page of the application is a separate 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 api section).

In the examples below, the importIndex function determines the lazy-load strategy.

  1. Full application: Importing all pages at once from the pages/index.ts file will lazy-load them all together. Use the following at the top of the file:

    const importIndex = async () => import("./pages");
  2. 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 };
     }}
    />
Example of simple route
<Route
	path="simple-pages"
	lazy={async () => {
		const { PageComponent } = await importIndex();
		return { Component: PageComponent };
	}}
/>
Example of protected route
<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 Routes section), we can either export each page separately (lazy-loading each one), or include a 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 lib/utils section) and the base url defined in the environment variables. For ease of use within the application, this instance can be defined and exported from the 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).

Example with 'Application', 'Role' and 'User' resources
api/
├── applications/
│   ├── index.ts
│   ├── hooks.ts
│   ├── mock.ts
│   ├── mockHandlers.ts
│   └── types.ts
├── roles/
│   └── ...
├── users/
│   └── ...
├── index.ts
├── apiRoutes.ts
├── actions.ts
└── types.ts
  1. api/types.ts: Includes all types that are shared across multiple resources. If types can be shared across applications, consider placing them in the @types folder, or in the mfb-types library.
  2. api/actions.ts: Includes all actions related to this application's permissions. Defines an enum with all actions names, and an object based on these actions, created using the composeApplicationClaims utility 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 the Routes 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',
    }
    */
  3. 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 exported as const, so that TypeScript will type them with their constant string value rather than plain string.

    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 context field, that would define a set of endpoints "in the context of" the parent resource.

    Example with two resources, Application and Role
    export 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;
  4. 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";
  5. api/<resource-name>/types.ts: Includes all types related to this resource. Typically will include a type for the resource itself, and multiple Args and Response types for the different API endpoints. For the Response types, prefer using the MidlandResult generic type from mfb-types library, e.g. type GetResource = MidlandResult<Resource[]>;.
  6. 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.
  7. api/<resource-name>/mockHandlers.ts: Defines a list of mock handlers, matching the API hooks defined in the ./hooks.ts file, so that when running the application in mock mode, msw can intercept the HTTP requests and respond with the appropriate mock object (defined in the ./mocks.ts file).
  8. api/<resource-name>/mocks.ts: Defines all mock objects that should be returned by the api hooks when running the suite in mock mode.
  9. api/<resource-name>/index.ts: Exports everything from the resource folder for ease of use across the application.