Overview
Frontend modules are the fundamental building block for creating applications in O3. They are self-contained pieces of functionality that can be loaded by the app shell. For example, you could have a frontend module that handles rendering concerns related to vitals and biometrics. This module could include the following functionality:
- A component that displays a tabular overview of a patient's vitals and biometrics
- A component that displays chart visualizations of a patient's vitals and biometrics
- A form that allows a user to record a new set of vitals and biometrics readings
- A component that displays a patient's most recent vitals and biometrics readings in the patient's chart header
These components would be defined in the frontend module's src directory. The components defined in the frontend module's src directory are automatically registered with the app shell and can be used in the application. They would get displayed on the screen when the user navigates to the appropriate pages.
Frontend modules in O3 are typically built using React, but because we use single-spa (opens in a new tab) under the hood, they can be built using any frontend framework that single-spa supports. For example, the Form entry frontend module is written in Angular and is a wrapper app around the Angular form engine (opens in a new tab). So in that sense, the app shell is framework-agnostic. In practice, the lower-level workings of the app shell have been abstracted in such a way that frontend engineers can focus on building frontend modules without having to worry about which framework they are using.
Frontend modules are typically organized into domain-specific repositories. For example, frontend modules concerning the management of patients exist in the Patient Management (opens in a new tab) monorepo. Core aggregates the core frontend modules that are reused in all O3 applications. Patient Chart aggregates the frontend modules that make up the widgets in a patient dashboard. These repositories can be either monorepos or standard repositories depending on their scope and purpose. In cases where a frontend module is structured as a monorepo, you'll typically have a packages directory at the root of your monorepo. This directory will contain all of your frontend modules. For example, the Patient Management monorepo has the following structure:
- config-schema.ts
- declarations.d.ts
- index.ts
- routes.json
 
 
Each frontend module is structured as an independent NPM package with:
- Its own package.jsonfile (separate from the root-levelpackage.jsonfile). This file defines the module's dependencies and metadata.
- A srcdirectory that contains the source code for the frontend module. That directory contains the following important files:- A config-schema.tsfile that defines the module's configuration schema
- A declarations.d.tsfile that defines the module's TypeScript declarations
- An index.tsfile that defines the module's entry point
- A routes.jsonfile that defines the module's static metadata
 
- A 
Frontend modules are alternatively referred to as microfrontends in O3. They have the following characteristics:
- They get shipped in the ECMAScript module format (ESM). This is a standard format for shipping JavaScript modules that allows for better code organization and dependency management. This explains the esm-prefix in their name.
- They have a descriptive middle section in their name describing the module's functionality. For example, esm-patient-search-appis a module that handles searching for patients.
- They may have an -appsuffix in their name, but this is not required.
O3 distributions consist of a set of frontend modules that are shipped together. For example, the frontend modules shipped with the community demo reference application (opens in a new tab) are described in this spa-assemble-config.json (opens in a new tab) file.
Anatomy of a frontend module
Every component must have:
- A package.jsonmanifest file that defines the module's dependencies and metadata
- A src/index.tsfile that defines the module's entry point
- A startupAppfunction that defines the module's dynamic metadata
- A src/routes.jsonfile that defines the module's static metadata
Manifest file (package.json)
Each frontend module has a root-level package.json file that defines its dependencies and metadata. Below is a snippet of the package.json file from the form builder frontend module:
{
  "name": "@openmrs/esm-form-builder-app",
  "version": "2.0.1",
  "license": "MPL-2.0",
  "description": "OpenMRS ESM Form Builder App",
  "browser": "dist/openmrs-esm-form-builder-app.js",
  "main": "src/index.ts",
  "source": true,
  "scripts": {
    "start": "openmrs develop",
    "serve": "webpack serve --mode=development",
    "build": "webpack --mode production",
    "analyze": "webpack --mode=production --env.analyze=true",
    "lint": "TIMING=1 eslint src --ext js,jsx,ts,tsx",
    "prettier": "prettier --write \"src/**/*.{ts,tsx}\"",
    "typescript": "tsc",
    "test": "jest --config jest.config.js",
    "test-e2e": "playwright test",
    "verify": "turbo lint typescript coverage",
    "coverage": "yarn test --coverage --passWithNoTests",
    "postinstall": "husky install",
    "extract-translations": "i18next 'src/**/*.component.tsx' --config ./i18next-parser.config.js",
    "ci:bump-form-engine-lib": "yarn up @openmrs/openmrs-form-engine-lib@next"
  }
}Some key things to note from looking at this file include:
- The nameproperty which defines the name of the module. This property is used as the module’s unique identifier in the import map.
- The browserproperty which points to the entry point of the webpack bundle.
- The mainproperty which defines the entry point of the frontend module’s source code, which is typicallysrc/index.ts.
The application entry point (index.ts)
Frontend modules have their entry point (opens in a new tab) defined in src/index.ts.
import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
import rootComponent from "./root.component";
import systemAdministrationFormBuilderCardLinkComponent from "./form-builder-admin-card-link.component";
 
const moduleName = "@openmrs/esm-form-builder-app";
 
const options = {
  featureName: "form-builder",
  moduleName,
};
 
export const importTranslation = require.context("../translations", true, /.json$/, "lazy");
 
export function startupApp() {
  defineConfigSchema(moduleName, configSchema);
 
  registerBreadcrumbs([
    {
      path: `${window.spaBase}/form-builder`,
      title: "Form Builder",
      parent: `${window.spaBase}/home`,
    },
    {
      path: `${window.spaBase}/form-builder/new`,
      title: "Form Editor",
      parent: `${window.spaBase}/form-builder`,
    },
    {
      path: `${window.spaBase}/form-builder/edit/:uuid`,
      title: "Form Editor",
      parent: `${window.spaBase}/form-builder`,
    },
  ]);
}
 
export const root = getSyncLifecycle(rootComponent, options);
 
export const systemAdministrationFormBuilderCardLink = getSyncLifecycle(
  (systemAdministrationFormBuilderCardLinkComponent),
  options
);This file is the entry point of the frontend module. It is the first file that gets executed when the frontend module gets loaded. It is responsible for setting up the frontend module and exporting the module’s configuration. Specifically, in this example:
- It exports an importTranslationfunction which is used to load the module’s translations.
- It also exports two named exports, rootandsystemAdministrationFormBuilderCardLink. These are named exports for a page and an extension, respectively. They are used to tell the app shell how to load the frontend module’s content.
- It also exports a startupAppfunction which is used to set up the frontend module. In this case, the frontend module's configuration schema is defined here, as well as the breadcrumbs for the module.
The startupApp function
Each frontend module defines a function named startupApp. This function performs any setup that should occur at the time the module gets loaded. It returns an object that communicates how the app shell should load the module. The startupApp function therefore is where we:
- define the module's configuration schema
- register breadcrumbs
The importTranslation function
This is required for translations to work. It tells the frontend application how to load translation strings. Note that the first argument to require.context is a directory, ../translations. That directory must exist at that location relative to the index.ts file.
Static metadata in routes.json
The routes.json file is used to set up the frontend module's static metadata. These include:
- The pagesthat frontend module provides
- The backenddependencies that frontend module requires. This is an object that tells the frontend application what OpenMRS server modules the frontend module depends on, and what versions. If these dependencies are not met, administrators will be alerted
The structure of this static file is dictated by the OpenMRS Routes standard JSON schema (opens in a new tab).
Creating a page
To create a new page, you'll typically need to follow these steps:
- 
Create a new React component that will be the page's entry point. 
- 
Add a named export for the page in the index.tsfile. For example, here's how a page namedrootis exported from theindex.tsfile:import rootComponent from ""./root.component"; export const root = getSyncLifecycle(rootComponent, options);
- 
Add a page definition for the new page to the routes.jsonfile'spagesarray:{ "pages": [ { "component": "root", // maps to the named export in `index.ts` "route": "form-builder", "online": true, "offline": true } ] }