Skip to content

Build a middleware

Who this is for: the developer building a service that sits between another system and DHIS2: it takes FHIR from one side and reads and writes DHIS2 on the other, inside an application of your own.

Before you start: Python 3.13, uv, and a DHIS2 you can reach. You need no project, no compile, and no docker.

You will be able to:

  • start a working service from an empty directory
  • say which package to install, and which you do not need
  • find the page for each thing a middleware does

The runnable version of this section is examples/facade/: eleven small steps, each using what the one before it showed.

Do you need one?

You want Use
A finished FHIR server, with capture screens, that you run and configure d2w fhir serve
Your own service, with your own routes, login, and deployment This section
To compute over FHIR data, with no DHIS2 involved FHIRPath, CQL, and quality measures
To call a facade somebody else runs Consume the FHIR API

From nothing

$ uv init myfacade && cd myfacade
$ uv add dhis2w-fhir-engine uvicorn

Put this in main.py:

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path

from dhis2w_client import BasicAuth, Dhis2Client
from dhis2w_fhir_engine.facade import FacadeConfig, open_facade
from dhis2w_fhir_engine.facade.api import attach_facade, mount_facade
from fastapi import FastAPI

config = FacadeConfig(canonical="http://example.org/fhir/demo", root=Path(".facade"))


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    auth = BasicAuth(username="admin", password="district")
    async with Dhis2Client(base_url="http://localhost:8080", auth=auth) as client:
        async with open_facade(config, client=client) as facade:
            attach_facade(app, facade)
            yield


app = FastAPI(lifespan=lifespan)
mount_facade(app, read_as_caller=True)

Run it:

$ uv run uvicorn main:app --port 8399
$ curl localhost:8399/metadata
$ curl -u admin:district 'localhost:8399/Questionnaire?_count=1'

That is a FHIR facade over your DHIS2: /metadata, read and search over the forms and the people, $generate, and POST /QuestionnaireResponse, which sends each capture to DHIS2 while the caller waits, so they hear DHIS2's answer. Each line of it is a choice you can change:

The line What it does Read more
FacadeConfig(canonical=..., root=...) Names your guide and where this service keeps its files Choose where the guide comes from
open_facade(config, client=client) Loads the guide, here built off the instance as the service starts Choose where the guide comes from
mount_facade(app, read_as_caller=True) Mounts the routes, and states who may do what Who can do what

Read Who can do what before anyone else can reach your service. read_as_caller is what makes this example safe in front of people's records, and admin here is for a local demo instance only.

Which package holds what

graph TD
    service["Your middleware"]
    serve["dhis2w-fhir-serve: d2w fhir serve, auth postures, SQLite projection, capture UI"]
    fhir["dhis2w-fhir: d2w fhir, generate, scaffold, validate, doctor"]
    engine["dhis2w-fhir-engine: evaluation, R4 models, facade library, routes"]
    client["dhis2w-client: DHIS2 connection and models"]

    service --> engine
    serve --> fhir
    fhir --> engine
    engine --> client

You install the engine. dhis2w-fhir is what you add to generate and publish an Implementation Guide, and dhis2w-fhir-serve is the finished server. A middleware needs neither.

What a middleware does

Take these in order, or go straight to what your service needs. Every piece works on its own.

Page What it covers Examples
Who can do what The one decision you cannot skip: whose rights DHIS2 applies Steps 8 and 9
Choose where the guide comes from Live off the instance, compiled, or a published package Step 2
Receive captures Submit a capture and hear DHIS2's answer, or validate, translate, post, and spool it step by step Steps 2 to 5
Read forms and people The guide, its codes, and a person out of DHIS2 as FHIR Step 6
Round trip FHIR into DHIS2 and back out, with FHIRPath and CQL over what came back Step 11
Mount the routes Put it in your FastAPI application, and test it Steps 7 to 9
Serve as a library The whole of d2w fhir serve, from a project on disk, with no port
Write the route yourself Your own route over translate_response, level by level
graph LR
    evaluate["1 Evaluate"]
    open["2 Open and validate"]
    translate["3 Translate"]
    post["4 Post"]
    drain["5 Spool and drain"]
    read["6 Read"]
    mount["7 Mount routes"]
    compose["8 Your own auth"]
    caller["9 Read as the caller"]
    nothing["10 From nothing"]
    round["11 Round trip"]

    open --> translate --> post --> drain
    open --> read
    drain --> mount
    read --> mount
    mount --> compose --> nothing
    mount --> caller --> nothing
    drain --> round
    read --> round
    evaluate --> round

Step 1, evaluating an expression, needs no DHIS2. Round trip is where it meets the rest: it evaluates what DHIS2 handed back.