Creating a Distribution
One of the reasons for choosing this kind of modularization in the frontend space is to allow maximum flexibility for creating your own distribution. That way, you can use what you find useful and drop what you don’t like to see in your distribution.
This concept is transported to almost all areas including, but not limited to, the available frontend modules, the delivered app shell, and the method of serving the application.
This guide walks you through creating an O3 distribution from start to finish, including building, testing, and deploying it.
Prerequisites
Before you begin, ensure you have:
- Node.js version 20.11 or later installed. The reference application frontend Dockerfile currently uses Node 22. We recommend using nvm or fnm to manage Node.js versions.
- npm (comes with Node.js) - You’ll use this to run the
openmrsCLI tool vianpx - An OpenMRS backend - You’ll need a running OpenMRS backend to connect your distribution to (for testing and production)
Installation
The openmrs CLI tool can be run without installation using npx (which comes with npm):
npx openmrs --helpAlternatively, you can install it globally:
npm install -g openmrsThen you can run commands directly without npx:
openmrs --helpFor CI/CD environments, use an explicit openmrs package version, for example npx --legacy-peer-deps openmrs@<app-shell-version>, so the CLI and app shell used by the build are reproducible.
Quick Start: Complete Workflow
Here’s a complete step-by-step workflow to create your first distribution:
Step 1: Create your project directory
Create a new directory for your distribution:
mkdir my-o3-distribution
cd my-o3-distributionStep 2: Create the assemble configuration
Create a spa-assemble-config.json file that defines which frontend modules to include:
{
"publicUrl": ".",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "latest",
"@openmrs/esm-home-app": "latest"
},
"frontendModuleExcludes": []
}Step 3: Create the build configuration
Create a spa-build-config.json file that configures runtime properties:
{
"spaPath": "/openmrs/spa/",
"apiUrl": "/openmrs/",
"configUrls": [],
"defaultLocale": "en",
"importmap": "./spa/importmap.json",
"routes": "./spa/routes.registry.json",
"supportOffline": false
}Step 4: Assemble frontend modules
Run the assemble command to gather all frontend module assets:
npx openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spaThis creates the importmap.json and routes.registry.json files in the ./spa directory.
Step 5: Build the app shell
Run the build command to create the distributable app shell:
npx openmrs build --build-config spa-build-config.json --target ./spaVerify the build succeeded by checking that ./spa/index.html exists.
Step 6: Test locally
Test your distribution using a simple HTTP server:
cd ./spa
python3 -m http.server 8080Then open the URL that matches how the files are being served. A plain python3 -m http.server from ./spa serves the
directory at the web root, so it is only a good quick smoke test when the build uses "spaPath": "/"; in that case,
open http://localhost:8080/. If your build uses the sample "spaPath": "/openmrs/spa/", use a web server or proxy
that maps /openmrs/spa/ to the built ./spa directory, such as the nginx example below.
Note: For the SPA to fully function, you’ll need to:
- Serve it behind a proxy that forwards API requests to your OpenMRS backend
- Or configure CORS on your backend to allow requests from
http://localhost:8080
Step 7: Deploy
Deploy the ./spa directory contents to your web server. See the Serving Your Distribution section below for detailed deployment options.
Local Build vs CI Setup
You may be tempted to clone the openmrs-esm-core repository for building your distribution. Don’t do this unless you know exactly why you want to work against the repository. The repository is only there for development of the OpenMRS Frontend. It is not there for building distributions.
To build your own distribution a simple Node.js tool called openmrs was created. This allows:
- creating an import map with all resources for the contained frontend modules (
openmrs assemble) - build a new app shell to host frontend modules (
openmrs build) - start a debugging session of the shell and a frontend module (
openmrs debug) - start a debugging session of a frontend module in the shell (
openmrs develop) - start the default app shell locally (
openmrs start)
For creating a distribution you need to complete two steps in order:
- Assemble the import map (
openmrs assemble) - This gathers all frontend module assets and creates the import map and routes registry. - Build the app shell (
openmrs build) - This builds the app shell that will host your frontend modules, using the import map and routes from step 1.
The import map is used to define what frontend modules are included and where these frontend modules are located. The build step requires the output from the assemble step.
Here’s a complete example workflow:
# Step 1: Assemble frontend modules and create import map
openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spa
# Step 2: Build the app shell
openmrs build --build-config spa-build-config.json --target ./spaNote: In CI/CD environments (like Docker), you may want to use npx --legacy-peer-deps openmrs@<app-shell-version> instead of just openmrs to ensure consistent versions. The --manifest flag outputs version information, and --mode config is required when assembling from a config file.
Important: The assemble step must run before build, and both commands should use the same --target directory. The build step reads the importmap.json and routes.registry.json files created by the assemble step.
Customizing the Import Map
By building the app shell you’ll already get a rudimentary version of an import map, which can be used for development purposes. Generally, however, you should provide your own.
An import map can also be specified as a URL. For instance, for the development instance at dev3.openmrs.org we have https://dev3.openmrs.org/openmrs/spa/importmap.json . The contents of this import map are updated once an update to any official frontend module has been pushed. Thus, while this import map may be useful for development, it should be considered unstable. Avoid this for your distribution or any application that should not break unexpectedly.
A custom import map can be created using the openmrs assemble command. If run directly the command will open a command line survey, guiding you through the different options. It will list all OpenMRS frontend modules that can be found on the NPM registry.
For CI/CD purposes we encourage you to use a configuration file spa-assemble-config.json instead. This file defines the wanted frontend modules and configures the whole process. Note that spa-assemble-config.json is different from spa-build-config.json, which is used for runtime configuration properties (see the Configuration overview guide for details).
To use the configuration file, run:
openmrs assemble --mode config --config spa-assemble-config.json --target ./spaYou can also specify additional options:
--target <directory>: The target directory where the gathered artifacts will be stored (default:dist)--fresh: Clean the output directory before the run--manifest: Output a manifest file with version information--mode config: Use config file mode. Without this option, the CLI starts the interactive survey flow.
The file may look as follows:
{
"publicUrl": ".",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "3.0.0"
},
"frontendModuleExcludes": []
}The frontendModuleExcludes array allows you to exclude specific modules that might be included as dependencies of other modules. This is useful when you want to remove a module that would otherwise be included automatically.
Note: frontendModuleExcludes is the property consumed by the
openmrs assemble command. Some older distribution config files may contain
excludedFrontendModules, but that property does not remove dependency
modules during CLI assembly.
The publicUrl may be important for later. If the gathered resources are placed (and served) in the same folder as the SPA resources then . is good. If they are uploaded to say a CDN, then the (base) URL of the CDN should be defined.
Example:
{
"publicUrl": "https://openmrs-cdn-example.com/mf",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "3.0.0"
}
}In this case the resulting importmap.json could look as follows:
{
"imports": {
"@openmrs/esm-patient-chart-app": "https://openmrs-cdn-example.com/mf/openmrs-esm-patient-chart-app-3.2.1/openmrs-esm-patient-chart-app.js",
"@openmrs/esm-patient-registration-app": "https://openmrs-cdn-example.com/mf/openmrs-esm-patient-registration-app-3.0.0/openmrs-esm-patient-registration-app.js"
}
}Either way the assemble command makes sure to have all assets made available properly.
Building the App Shell
After assembling your frontend modules, you need to build the app shell. The build process requires a spa-build-config.json file that configures runtime properties for your distribution.
The spa-build-config.json file should include:
{
"spaPath": "/openmrs/spa/",
"apiUrl": "/openmrs/",
"configUrls": ["/openmrs/spa/config.json"],
"defaultLocale": "en",
"importmap": "./spa/importmap.json",
"routes": "./spa/routes.registry.json",
"supportOffline": false
}Key properties:
spaPath: The path where the SPA will be served (e.g.,/openmrs/spa/)apiUrl: The URL of the OpenMRS backend APIconfigUrls: Array of URLs to frontend configuration JSON filesdefaultLocale: Default language code (e.g.,en,fr)importmap: Path, URL, or inline JSON for the import map created byassemble. Relative file paths are resolved from the directory where you run the command.routes: Path, URL, or inline JSON for the routes registry created byassemble. Relative file paths are resolved from the directory where you run the command.supportOffline: Whether to enable offline support via service worker
The OpenMRS CLI reads spa-build-config.json as literal JSON. It does not expand shell environment variables inside
this file during openmrs build. If you are building static files directly, put the final URLs in the JSON file or pass
the corresponding CLI options.
The Reference Application distribution intentionally uses placeholder values such as $SPA_PATH, $API_URL, and
$SPA_CONFIG_URLS because its frontend Docker container replaces those placeholders at startup:
{
"spaPath": "$SPA_PATH",
"apiUrl": "$API_URL",
"configUrls": ["$SPA_CONFIG_URLS"],
"defaultLocale": "$SPA_DEFAULT_LOCALE",
"importmap": "$SPA_PATH/importmap.json",
"routes": "$SPA_PATH/routes.registry.json",
"supportOffline": false
}To build the app shell, run:
openmrs build --build-config spa-build-config.json --target ./spaAdditional build options:
--target <directory>: The target directory (should match the assemble target)--fresh: Clean the output directory before building--support-offline: Enable offline support (can override config file)
After a successful build, your ./spa directory will contain:
index.html- The main HTML file for the SPAimportmap.json- The import map created by the assemble steproutes.registry.json- The routes registry created by the assemble step- JavaScript bundles and other assets needed to run the application
Build verification: After running the build command, check that ./spa/index.html exists. If it doesn’t, the build failed. Check the build logs for errors.
Important: If you’re using configUrls in your spa-build-config.json, you’ll also need to ensure those configuration JSON files are available and served alongside your built files. For example, if your config references /openmrs/spa/config.json, that file must be accessible at that URL when the SPA loads.
Serving Your Distribution
After building your distribution, you need to serve the built files using a web server. The built files are static assets that can be served by any web server (nginx, Apache, etc.).
Basic Setup
The simplest way to serve your distribution is using a static file server. The built files should be served at the path specified in your spaPath configuration (e.g., /openmrs/spa/).
Important considerations:
-
Config files: If you’re using
configUrlsin yourspa-build-config.json, make sure those configuration JSON files are also accessible via HTTP/HTTPS at the specified URLs. The SPA will fetch them at runtime. -
Backend connection: Ensure your web server can proxy API requests to your OpenMRS backend, or configure CORS on your backend to allow requests from your frontend domain.
-
Path configuration: The
spaPathin your build config must match the path where you serve the files. For example, ifspaPathis/openmrs/spa/, your web server should serve the files at that path.
Example: Using nginx
Here’s a basic nginx configuration to serve your distribution:
server {
listen 80;
server_name localhost;
# Serve the SPA at /openmrs/spa/
location /openmrs/spa/ {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /openmrs/spa/index.html;
}
# Proxy API requests to OpenMRS backend
location /openmrs/ {
proxy_pass http://backend:8080/openmrs/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Testing Locally
To test your distribution locally, you can:
-
Use a simple HTTP server:
cd ./spa python3 -m http.server 8080This serves the contents of
./spaathttp://localhost:8080/. Use it with a build whosespaPathis/, or switch to nginx/Docker when you need to preserve a nested path such as/openmrs/spa/. -
Use the
openmrs startcommand for development (though this uses a default import map, not your custom one). -
Use Docker with nginx, similar to the reference distribution setup.
Deployment
For production deployments:
- Docker: Copy the built
./spadirectory into an nginx container (see the reference distribution’s Dockerfile for an example) - Static hosting: Upload the
./spadirectory contents to your static hosting service (CDN, S3, etc.) - Traditional web server: Copy the files to your web server’s document root
Remember to also deploy any configuration JSON files referenced in your configUrls and ensure they’re accessible at the specified URLs.
Canary vs Stable
Regarding the versioning you’ll have three options:
- Go for the
latesttag - Go for the
nexttag - Go for a specific (i.e., explicit) version
In general we recommend to stay on non-preview (e.g., 3.2.1) versions. Preview versions (e.g., 3.2.1-pre.0) are for development purposes and may not be stable.
For creating a working distribution ideally you’ll stick to explicit versioning of non-preview versions. If you use latest then individual frontend modules may work as expected, but incompatibilities (e.g., if a certain frontend module was updated but is now incompatible to another frontend module that you also use) may then exist - making additional testing required. With an explicit version you can be sure that a working system remains as such in rebuild scenarios.