An official developer portal of the Government of the Republic of Zambia ยท Test environmentVerify a certificate
Pamodzi Developer HubBuild, integrate and learn the e-Services framework

Anatomy of a module and the generator

A module spans up to three apps: the API (zcm-backend), the back office (back-office-ui) and, only for generic blocks, the public portal (public-portal-ui). In this lesson you learn where each part lives, what the manifest says, and how to generate a working module in about ten minutes.

Where a module lives

zcm-backend/modules/<id>/          module.js (manifest), policy.js, contributions.js, routes/, controllers/,
                                   lib/, models/, jobs/, test/ (unit), test/integration/, README.md
zcm-backend/migrations/<ts>-<id>-<what>.js     its migrations, in the ONE migrations folder
back-office-ui/src/modules/<id>/   index.js, api.js, pages/, tests
public-portal-ui/src/modules/<id>/ index.js (generic, data-driven contributions only), tests

A new module needs no edit outside its own folders, except one line in each registry: modules/index.js in the backend and src/modules/registry.js in each front end. The generators add that line for you. Being listed in a registry makes a module bundled. Bundled does not mean on.

The manifest

modules/<id>/module.js exports a plain object. lib/modules/manifest.js validates it at boot, and an invalid manifest stops the process. Functions in it are called lazily, so nothing loads until it is needed.

KeyMeaning
idRequired. ^[a-z][a-z0-9-]{1,30}$. The folder name, and the prefix of every policy action, menu key and portal key.
name, version, kindRequired. version is semver; kind is ministry or platform.
descriptionShown to administrators before they switch the module on (at least 10 characters).
dependsOnOther module ids. They must be on first and cannot be switched off while this one is on.
defaultEnabledDefault false. Keep it false.
tablesThe tables the module owns, each with a model in models/.
routes[{ mount, router }], mounted at boot behind the module gate, by convention at /api/modules/<id>.
policyThe module's policy rules; every action starts with <id>..
jobs[{ name, cron, tz, catchUp, run }], scheduled only while the module is on.
serviceCapabilitiesCapabilities the module owns; the service designer offers them only while it is on.
contributes() => require('./contributions'): the extension point contributions.
menuBack-office entries: key starts with <id>., path under /m/<id>/, action must exist.
portalKeep it []. Every portal slot is an exception the product owner decides.
pos{ capabilities: [] } unless the module has POS screens.
settingsA settings schema, per installation and overridable per MDA.
impactThe module's own figures in the switch-off impact preview, for example "active leases".
healthRead-only state shown under System administration โ†’ Modules.

A real manifest from the labour module shows the shape (trimmed):

module.exports = {
  id: 'labour',
  name: 'Labour',
  version: '1.0.0',
  kind: 'ministry',
  dependsOn: [],
  defaultEnabled: false,
  tables: ['labour_records', 'labour_establishments', 'labour_agreements', 'labour_complaints'],
  routes: [{ mount: '/api/modules/labour', router: () => require('./routes') }],
  policy: () => require('./policy').RULES,
  jobs: [{ name: 'labour-daily', cron: '45 5 * * *', tz: 'Africa/Lusaka', catchUp: false,
           run: () => require('./jobs/daily').run() }],
  contributes: () => require('./contributions'),
  menu: [{ group: 'Labour', key: 'labour.establishments', label: 'Register',
           path: '/m/labour/establishments', action: 'labour.read', order: 1 }],
  portal: [],
  pos: { capabilities: [] },
};

Generate the backend part

The generator writes a module that passes every check at once.

cd zcm-backend
node scripts/newModule.js fisheries --name "Fisheries licensing" \
  --description "Fishing licences and catch returns for the Department of Fisheries."

It writes:

  • modules/fisheries/module.js: off by default, one route, one job, one menu entry, no portal slot;
  • policy.js with fisheries.read (staff) and fisheries.write (officers);
  • routes/index.js with GET /api/modules/fisheries/records behind can('fisheries.read');
  • a controller, a model, a daily job, an empty contributions.js and a README;
  • a unit test and an integration test that switches the module on with h.enableModule;
  • migrations/<timestamp>-fisheries-init.js creating the module's table.

It adds require('./fisheries/module') to modules/index.js and refreshes docs/modules/module-keys.json.

The generator refuses an id that does not match the pattern or is reserved (me, admin, core and others), is already bundled or already a folder, is the prefix of a core policy action (case, payment and so on), has a core route at /api/<id>, or clashes with an existing migration, model, table or job.

Useful options: --depends estate (needs another module), --dry-run (list the files without writing) and --no-register (skip the registry line).

Generate the front-end parts

Use the same id in each app:

cd ../back-office-ui   && node scripts/newModule.js fisheries --name "Fisheries licensing"
cd ../public-portal-ui && node scripts/newModule.js fisheries --name "Fisheries licensing"   # only for generic blocks

After any later change to the backend manifest's menu or portal entries, sync the keys in each front end:

node scripts/newModule.js --sync-keys ../zcm-backend

Each app's moduleContract.test.js compares its registry with module-keys.json, so a menu key or path that does not match the backend fails the build.

The back-office part

back-office-ui/src/modules/<id>/index.js exports the module's routes, menu entries and optional contributions such as dashboardWidgets, caseTabs, binTypes, feeTypes and stepTypes. Core screens ask useModules() for contributions and never import a module. A module renders only when GET /api/modules/me lists it, and a menu entry shows only when the server lists its key: the server's policy decides, not the front end. Pages use the shared UI kit in src/components/ui (PageHeader, Card, Table, Button, Banner, EmptyState, Dialog). Entries default to the Ministry functions section of the menu.

Run the checks

cd zcm-backend
npm test
npm run test:integration -- modules/fisheries/test/integration/fisheries.test.js
TEST_SCHEMA=migrations npm run test:integration -- modules/fisheries/test/integration/fisheries.test.js
npm run db:schema-diff && npm run check:modules && npm run check:mda

Point DATABASE_URL and TEST_DATABASE_ADMIN_URL at your own local PostgreSQL with PostGIS first. Never use a shared server.

Check your understanding

  • Which files outside a module's own folders does a new module touch, and who edits them?
  • What happens at boot when a manifest is invalid?
  • Name four reasons the generator refuses a module id.
  • Why should portal stay an empty list?
  • Which test catches a back-office menu key that does not match the backend manifest?

Sign in to record your progress.

Previous: When to build a module Next: Policy, routes, data, jobs and settings