Publisert - 04.09.2026

Architecture

Overview

Nompd API is a data transformation and distribution layer. It fetches FHIR-structured healthcare data from upstream APIs, converts it into simplified REST models, and exposes it to consumers with versioned diff support.

┌─────────────────────────────────┐
│  Upstream FHIR APIs             │
│  (legemidler-api-test)          │
│  - PlanDefinition               │
│  - ActivityDefinition           │
│  - RegulatedAuthorization       │
└──────────────┬──────────────────┘
                │  Sync triggered by K8s CronJob (`--sync`)
                │  or manually via POST /api/internal/sync
                ▼
┌──────────────────────────────────┐
│  SyncController                  │
│    └─ SyncService                │
│         ├─ UpstreamApiClient     │  Fetch raw FHIR data
│         ├─ VersionService        │  Increment global version
│         ├─ AppDbContext          │  Upsert raw FHIR (versioned)
│         ├─ ConversionService     │  Map FHIR → Output models
│         └─ AppDbContext          │  Upsert output + write history
└──────────────┬───────────────────┘
               │
               ▼
┌──────────────────────────────────────┐
│  Postgres (EF Core)                  │
│  Raw FHIR:                           │
│   - PlanDefinitions                  │
│   - ActivityDefinitions              │
│   - RegulatedAuthorizations          │
│  Converted output:                   │
│   - TreatmentGroups                  │
│   - ReimbursementGroups              │
│  Internal:                           │
│   - SyncMetadata (global counter)    │
│   - SyncHistory (one per sync run)   │
└──────────────┬───────────────────────┘
               │
               ▼
┌──────────────────────────────────────────────────┐
│  Output Controllers                              │
│  - TreatmentGroupController          (v1)        │
│  - ReimbursementGroupController      (v1)        │
│  - TreatmentGroupV2Controller        (v2)        │
│  - ReimbursementGroupV2Controller    (v2)        │
│                                                  │
│  GET /api/v1/treatment-group      (X-API-KEY)    │
│  GET /api/v1/reimbursement-group  (X-API-KEY)    │
│  GET /api/v2/treatment-group      (HelseID/DPoP) │
│  GET /api/v2/reimbursement-group  (HelseID/DPoP) │
│  (with optional ?since-version=N and filters)    │
└──────────────────────────────────────────────────┘
               │
               ▼
         API consumers

Repository Structure

.
├── .opencode/
│   └── agents/
├── Api/                                  Empty (only .idea) - leftover after move to backend/
├── backend/
│   ├── src/
│   │   └── Api/                          API project (see Directory Structure below)
│   └── tests/
│       └── Api.Tests/                    API test project
├── docs/
│   ├── data-model/
│   ├── developer/
│   ├── protokoll/
│   └── system/
├── frontend/
│   ├── public/
│   └── src/
├── manifests/
│   ├── appOfApps/
│   ├── apps/
│   └── scripts/
└── scripts/

Directory structure API

backend/
├── src/
│   └── Api/
│       ├── Controllers/          API endpoints
│       │   ├── SyncController              POST /api/internal/sync
│       │   ├── TreatmentGroupController     GET /api/v1/treatment-group
│       │   ├── ReimbursementGroupController  GET /api/v1/reimbursement-group
│       │   ├── TreatmentGroupV2Controller    GET /api/v2/treatment-group (HelseID)
│       │   ├── ReimbursementGroupV2Controller  GET /api/v2/reimbursement-group (HelseID)
│       │   └── *ControllerBase              Shared query logic for v1/v2
│       ├── HelseId/
│       │   ├── HelseIdExtensions     DPoP (RFC 9449) JwtBearer scheme + scope policy
│       │   ├── Constants             Audience (nhn:nompd), scope (nhn:nompd/api)
│       │   └── Common/ApiDPoPValidation  DPoP proof validation (signature, ath, replay)
│       ├── Data/
│       │   └── AppDbContext         Postgres table accessors + indexes
│       ├── Middleware/
│       │   └── ApiKeyAuthorizationFilter    X-API-KEY header validation
│       ├── Models/
│       │   ├── Fhir/             Upstream FHIR models (deserialized from source APIs)
│       │   ├── Output/           Converted models (served to consumers)
│       │   └── Postgres/         Entity types (versioning, sync metadata)
│       ├── Services/
│       │   ├── ConversionService            FHIR → Output mapping (pure, no I/O)
│       │   ├── SyncService                  Orchestrates fetch → convert → store
│       │   ├── UpstreamApiClient            HTTP client for upstream FHIR APIs
│       │   └── VersionService               Atomic version counter (Postgres)
│       ├── Program.cs                       DI registration and startup
│       ├── appsettings.json                 Base configuration
│       └── appsettings.Development.json     Dev overrides (API keys)
└── tests/
    └── Api.Tests/
        ├── IntegrationTests/    Full-stack tests against a running app
        │   ├── Auth/
        │   ├── Controllers/
        │   ├── Middleware/
        │   └── Services/
        ├── TestData/            Upstream FHIR fixture resources
        │   └── 01-shi/
        ├── TestHelpers/         Shared test utilities (loaders, mocks)
        ├── UnitTests/           Isolated unit tests
        │   ├── Controllers/
        │   └── Services/
        ├── ConversionServiceRoundTripTests.cs
        ├── SmokeTest.cs
        └── global.json

Separation of Concerns

Layer Responsibility I/O
UpstreamApiClient Fetch raw FHIR data from external APIs HTTP
ConversionService Transform FHIR models to output models None (pure)
VersionService Manage global version counter Postgres
SyncService Orchestrate the full sync pipeline All (coordinates above)
AppDbContext Provide typed Postgres table access Postgres
ApiKeyAuthorizationFilter Validate API keys on incoming v1 requests Configuration
HelseIdExtensions + DPoPProofValidator Validate HelseID access tokens and DPoP proofs on v2 requests Configuration, JWKS (HelseID), replay cache
*ControllerBase (treatment/reimbursement group) Shared v1/v2 query logic: filtering, versioned diff, single-item lookup Postgres (read-only)
Output Controllers Bind route + auth scheme, delegate to the controller base Postgres (read-only)

Key Design Decisions

  1. Stateless versioning — The server does not track per-client state. Clients are responsible for storing the currentVersion from responses and sending it back via ?since-version=N.

  2. Soft deletes — When upstream data disappears, records are marked IsDeleted = true with the current version number rather than physically removed. This ensures diff consumers see the deletion.

  3. Sync via --sync / HTTP endpoint — Instead of a BackgroundService, the Kubernetes CronJob (sync-job) runs the API image with --sync, executing the sync in-process. The POST /api/internal/sync endpoint is retained for manual triggering for debugging or recovery. Both paths provide job history, logging, and alerting via K8s.

  4. ConversionService is pure — No injected dependencies, no database access. This makes it trivially testable and ensures the mapping logic is isolated from infrastructure concerns.

  5. Generic VersionedDocument — A single wrapper type handles versioning for any model (raw or converted), avoiding duplication of version/delete tracking logic.

  6. Content-hash-based change detection — Each VersionedDocument<T> stores a SHA256 hash of its JSON representation. On sync, only records whose hash actually changed (or whose deletion state flipped) get a new version. This is what makes the version-based diff API meaningful: clients calling ?since-version=N receive only the records that genuinely changed, not every record after every sync.

  7. Raw FHIR persistence — Upstream FHIR resources are stored alongside their converted output, sharing the same version number per sync. This enables re-conversion if mapping logic changes, debugging by comparing raw vs converted, and audit traceability back to the original source.

  8. Sync history — Every sync run produces a SyncHistoryEntry with timestamps, durations, per-collection counts, and the business keys of each affected record. This provides durable audit trail and answers "what changed in sync N?" without inspecting the data collections directly.

  9. Two authentication schemes, one data layer — v1 endpoints are protected by the X-API-KEY header (ApiKeyAuthorizationFilter); v2 endpoints are protected by HelseID using DPoP (RFC 9449). The v2 scheme is a named JwtBearer scheme (dpop_token_authentication_scheme) that:

    • Rejects Bearer tokens — only the DPoP authorization scheme is accepted (OnMessageReceived).
    • Validates the access token against the HelseID authority (HelseId:Authority) with audience nhn:nompd, and requires the nhn:nompd/api scope via the can_access_api_policy policy (missing scope → 403).
    • Validates the per-request DPoP proof (DPoPProofValidator): typ: dpop+jwt, public-key jwk whose thumbprint matches the token's cnf.jkt, signature over a supported RSA/ECDSA algorithm, ath bound to the access token, htm/htu bound to the request (path without query string), a fresh iat, and jti replay detection via IReplayCache (in-memory IDistributedCache).
  10. Controller bases for v1/v2 — Query logic (filters, ?since-version diff, single-item lookup) lives in TreatmentGroupControllerBase and ReimbursementGroupControllerBase. Each version-specific controller only binds the route, the auth scheme/policy, and rate limiting, then delegates to the base. Adding a new version is a thin controller, not a copy of the query code.

Søk i Utviklerportalen

Søket er fullført!