FHIR for implementation developers — REST API, VN Core validation, and Bundle

This article takes implementation developers from REST API, Vietnamese Patient payloads, search, VN Core profile validation, to Bundle transaction, with emphasis on data that can be checked against VN Core.

This page is for web and mobile developers already comfortable with REST and JSON. The examples target a local endpoint so that its configuration is under the reader's control; a public sandbox is suitable only for interactions declared by its current CapabilityStatement and must not be assumed to have VN Core installed.

TL;DR

  • Four-part implementation path: Hello World → Search → VN Core profile validation → Bundle transaction.
  • Environment: a HAPI FHIR public test server or local HAPI JPA Starter; read [base]/metadata before invoking an interaction.
  • Samples in all four languages: cURL, JavaScript (fetch), Python (fhirpy), Java (HAPI client).
  • The CCCD identifier URI in this trial-use VN Core is http://fhir.hl7.org.vn/core/sid/cccd; clients must use the URI specified by the profile.
  • IG package installation depends on the HAPI version and configuration; verify that the package is installed and that validation actually uses the profile.

1. Set up and inspect server capabilities

FHIR R4 (4.0.1) defines resources and several exchange paradigms, including a RESTful API. This tutorial uses only REST with the JSON representation. Before running an example, choose a test endpoint and inspect that endpoint's CapabilityStatement.

Option A — HAPI FHIR public test server

HAPI FHIR publishes a test endpoint at https://hapi.fhir.org/baseR4. Its availability, data, authentication requirements, and capabilities may change. Never submit personal or real clinical data to a public endpoint.

curl -fsS -H "Accept: application/fhir+json" \
  https://hapi.fhir.org/baseR4/metadata

In CapabilityStatement.rest, inspect the mode, resources, interactions, search parameters, and operations you need. This is the server's declaration; integration tests must still inspect the actual HTTP status, headers, and body.

Option B — local HAPI JPA Starter

The HAPI JPA Starter image can provide a local endpoint. The latest tag is convenient for exploration but is not reproducible; pin a tested tag or digest for project work. The default starter is not a production security configuration and must not receive real data until authentication, authorization, audit, and operational controls are in place.

docker run -d -p 8080:8080 \
  --name hapi-fhir \
  hapiproject/hapi:latest

# Continue only after the endpoint returns a CapabilityStatement
curl -f -H "Accept: application/fhir+json" \
  http://localhost:8080/fhir/metadata

The examples below use http://localhost:8080/fhir. Do not mechanically replace it with a public endpoint: run create, search, transaction, or $validate only when the CapabilityStatement and an actual probe confirm support.

2. Lesson 1 — Create a Vietnamese Patient

This example creates a Patient with a synthetic 12-digit CCCD. When representing CCCD under the trial-use VN Core published on this site, identifier.system uses http://fhir.hl7.org.vn/core/sid/cccd. This is an identifier namespace; the Patient profile canonical is a different URL under StructureDefinition.

curl -X POST http://localhost:8080/fhir/Patient \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -H "Prefer: return=representation" \
  -d '{
    "resourceType": "Patient",
    "identifier": [{
      "system": "http://fhir.hl7.org.vn/core/sid/cccd",
      "value": "001234567890"
    }],
    "name": [{"family": "Nguyễn", "given": ["Thị", "Lan"]}],
    "gender": "female",
    "birthDate": "1985-03-15",
    "address": [{
      "country": "VN",
      "city": "Hà Nội",
      "line": ["123 Lê Lợi"]
    }]
  }'

If the server supports the create interaction and succeeds, FHIR R4 requires HTTP 201 Created and a Location header. The location may be relative or absolute and need not contain _history when the server does not support versioning. Prefer: return=representation asks for the resource in the body; without that header the server may return a body or no body. Inspect both the headers and body rather than assuming meta.versionId is always present.

Note: application/fhir+json is the formal media type for FHIR JSON. Some servers may accept application/json, and FHIR permits that generic type in Accept; do not assume every server treats requests identically. On HTTP 415, inspect the OperationOutcome and endpoint documentation.

3. Lesson 2 — Search Patient

FHIR defines a search framework and standard search parameters, but an R4 server is not required to implement every parameter or modifier. Before running these queries, verify that the server declares search-type for Patient and the relevant parameters. A token search on identifier should use the stored system–value pair.

# --data-urlencode handles | and Vietnamese characters safely
curl --get "http://localhost:8080/fhir/Patient" \
  --data-urlencode "identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"

# Search by family name
curl --get "http://localhost:8080/fhir/Patient" \
  --data-urlencode "family=Nguyễn"

# Female patients born between 1980 and 1989
curl --get "http://localhost:8080/fhir/Patient" \
  --data-urlencode "gender=female" \
  --data-urlencode "birthdate=ge1980" \
  --data-urlencode "birthdate=lt1990"

# Pagination + sort
curl --get "http://localhost:8080/fhir/Patient" \
  --data-urlencode "family=Nguyễn" \
  --data-urlencode "_count=10" \
  --data-urlencode "_sort=birthdate"

The response is a Bundle with type="searchset". Each entry contains one matching Patient. To get the next page, follow Bundle.link with relation="next" — DO NOT hand-craft the pagination URL yourself, because servers may use cursors or offsets depending on implementation.

For string search, :exact requests a whole-value, case-sensitive match. Support for :contains, :missing, date/number prefixes, _sort, and string processing can depend on the server. Use them only after checking the CapabilityStatement, endpoint documentation, and observed behavior.

4. Lesson 3 — Validate against the VN Core profile

A successful create response only shows that the server accepted the request under its configuration; it does not demonstrate VN Core conformance. To test this trial-use VN Core's constraints — including the CCCD slice, permitted alternative identifiers or data-absent-reason, and local extensions — the validator must load the correct package and version. The profile canonical is http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient.

Install the VN Core IG into HAPI

Important note: the public HAPI test server must not be assumed to support VN Core. HAPI IG installation varies by version and enabled modules. Pin a HAPI version, follow that version's documentation, and do not treat storing a few StructureDefinition resources over REST as equivalent to installing a package with its dependencies and terminology.

Illustrative configuration for a HAPI JPA Starter version that supports package installation:

# application.yaml (mount into /app/config/application.yaml in the container)
hapi:
  fhir:
    implementationguides:
      vn-core:
        name: hl7.fhir.vn.core
        version: 0.8.0
        packageUrl: https://downloads.fhir.hl7.org.vn/core/0.8.0/packages/hl7.fhir.vn.core-0.8.0.tgz
        installMode: STORE_AND_INSTALL

These keys must match the pinned HAPI version. After startup, inspect error logs, retrieve StructureDefinition/vn-core-patient, and run a known-negative validation case. Only then conclude that installation succeeded; a running container does not prove that the package was downloaded or used by the validator.

Verify that the artifact is installed:

# This succeeds only if the artifact is stored under the corresponding logical id
curl -fsS -H "Accept: application/fhir+json" \
  http://localhost:8080/fhir/StructureDefinition/vn-core-patient

Validate a Patient against the profile

FHIR defines $validate, but a server is not required to implement the operation. Check the CapabilityStatement and probe it with the intended profile/version:

curl -X POST \
  "http://localhost:8080/fhir/Patient/$validate?profile=http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient" \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -d @patient-vn.json

Evaluate both the HTTP status and OperationOutcome.issue; detailed classification can differ among servers:

  • severity="fatal" or error means the request/resource failed the corresponding check; inspect code, details, diagnostics, and expression.
  • warning and information require project policy. A missing Must Support element does not automatically produce a warning because Must Support semantics are defined by the IG and use case.
  • Do not infer “pass” from an information issue alone. Confirm that there are no fatal/error issues, that the intended canonical/version was used, and that all dependencies resolved.

5. Lesson 4 — Bundle transaction

The following example submits Patient, Encounter, and Observation resources in a Bundle with type="transaction". Atomic behavior — all changes succeed or all are rolled back — applies only when the server supports the system-level transaction interaction. Confirm that support in its CapabilityStatement first.

A fullUrl in urn:uuid: form is useful when new resources reference one another before logical ids exist. During transaction processing the server maps those URNs to assigned identities under the transaction rules; clients should read entry.response.location instead of guessing ids.

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-1",
      "resource": {
        "resourceType": "Patient",
        "identifier": [{
          "system": "http://fhir.hl7.org.vn/core/sid/cccd",
          "value": "001234567890"
        }],
        "name": [{"family": "Nguyễn", "given": ["Thị", "Lan"]}],
        "gender": "female",
        "birthDate": "1985-03-15"
      },
      "request": {"method": "POST", "url": "Patient"}
    },
    {
      "fullUrl": "urn:uuid:encounter-1",
      "resource": {
        "resourceType": "Encounter",
        "status": "in-progress",
        "class": {
          "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
          "code": "AMB",
          "display": "ambulatory"
        },
        "subject": {"reference": "urn:uuid:patient-1"},
        "period": {"start": "2026-04-30T08:00:00+07:00"}
      },
      "request": {"method": "POST", "url": "Encounter"}
    },
    {
      "fullUrl": "urn:uuid:obs-1",
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "code": {
          "coding": [{
            "system": "http://loinc.org",
            "code": "8867-4",
            "display": "Heart rate"
          }]
        },
        "subject": {"reference": "urn:uuid:patient-1"},
        "encounter": {"reference": "urn:uuid:encounter-1"},
        "valueQuantity": {
          "value": 78,
          "unit": "/min",
          "system": "http://unitsofmeasure.org",
          "code": "/min"
        }
      },
      "request": {"method": "POST", "url": "Observation"}
    }
  ]
}

Submit the Bundle with a POST to the root endpoint:

curl -X POST http://localhost:8080/fhir \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -d @bundle-transaction.json

A successful transaction returns a transaction-response Bundle. For the three POST requests in this example, successful entries normally report 201 Created and may include response.location; do not apply 201 to entries using other methods. On failure the server must leave no partial changes, but inspect the actual HTTP status and placement of any OperationOutcome in the response.

Encounter uses class.system = "http://terminology.hl7.org/CodeSystem/v3-ActCode" with code AMB (ambulatory) — that is the standard HL7 v3 CodeSystem, NOT a placeholder URL. Observation uses LOINC code 8867-4 for heart rate and the UCUM unit /min.

6. Samples in 4 languages — create a Vietnamese Patient

The same illustrative payload is shown through four clients. cURL appears in Lesson 1; with every library, still inspect the target server's CapabilityStatement, HTTP status, headers, and any OperationOutcome.

JavaScript / TypeScript (plain fetch)

This example uses fetch, available in modern JavaScript runtimes. If a project uses a client library, pin its version and verify how it handles responses without a body.

const baseUrl = 'http://localhost:8080/fhir';

const patient = {
  resourceType: 'Patient',
  identifier: [{
    system: 'http://fhir.hl7.org.vn/core/sid/cccd',
    value: '001234567890',
  }],
  name: [{ family: 'Nguyễn', given: ['Thị', 'Lan'] }],
  gender: 'female',
  birthDate: '1985-03-15',
};

const res = await fetch(`${baseUrl}/Patient`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/fhir+json',
    'Accept': 'application/fhir+json',
    'Prefer': 'return=representation',
  },
  body: JSON.stringify(patient),
});

if (!res.ok) {
  throw new Error(`FHIR error ${res.status}: ${await res.text()}`);
}

const responseText = await res.text();
const created = responseText ? JSON.parse(responseText) : null;
const location = res.headers.get('Location');
console.log('Created Patient:', created?.id ?? location);

Python (fhirpy)

fhirpy provides synchronous and asynchronous Python clients for interactions such as CRUD and search. Check the pinned version's documentation before relying on its validation or error-handling behavior. Install with pip install fhirpy.

from fhirpy import SyncFHIRClient

client = SyncFHIRClient('http://localhost:8080/fhir')

patient = client.resource(
    'Patient',
    identifier=[{
        'system': 'http://fhir.hl7.org.vn/core/sid/cccd',
        'value': '001234567890',
    }],
    name=[{'family': 'Nguyễn', 'given': ['Thị', 'Lan']}],
    gender='female',
    birthDate='1985-03-15',
)
patient.save()
print('Created Patient id:', patient.id)

Java (HAPI FHIR client)

HAPI FHIR ships both server and client in the same SDK. Maven dependencies: ca.uhn.hapi.fhir:hapi-fhir-structures-r4 + ca.uhn.hapi.fhir:hapi-fhir-client.

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.r4.model.*;

FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

Patient patient = new Patient();
patient.addIdentifier()
    .setSystem("http://fhir.hl7.org.vn/core/sid/cccd")
    .setValue("001234567890");
patient.addName()
    .setFamily("Nguyễn")
    .addGiven("Thị")
    .addGiven("Lan");
patient.setGender(Enumerations.AdministrativeGender.FEMALE);
patient.setBirthDateElement(new DateType("1985-03-15"));

MethodOutcome outcome = client.create().resource(patient).execute();
System.out.println("Created Patient id: " + outcome.getId().getIdPart());

7. Recommended tooling

The table below lists several server, client, validator, and authoring options. Check licensing, maintenance status, and version-specific support before selecting a tool for a project.

Tool Purpose License
HAPI FHIRJava server and clientApache 2.0
Microsoft FHIR ServerServer for the Azure / .NET stackMIT
Firely .NET SDKClient and parser for .NETBSD-3
fhirpyPython client (async + sync)MIT
fhir.resourcesPydantic models for FHIRBSD-3
FHIR Validator (CLI)Offline validation against IGsApache 2.0
SUSHIFSH → FHIR JSON compilerApache 2.0
Postman + FHIR collectionManual REST testingFree tier

8. Ten implementation checks

This list focuses on assumptions to eliminate before integrating with a specific FHIR endpoint.

  1. Media type: send FHIR JSON with Content-Type: application/fhir+json and declare Accept. Some servers accept application/json, but clients should not rely on that exception; inspect any 415 response.
  2. Identifier namespace: use system and value consistently. http://fhir.hl7.org.vn/core/sid/cccd is a system URI, not a profile canonical.
  3. Primitive format: birthDate uses FHIR syntax such as 1985-03-15. An unparseable resource commonly leads to 400; profile or business-rule failures may lead to 422. Servers do not always distinguish tightly, so inspect the OperationOutcome.
  4. Transaction references: relative, absolute, and URN references each have roles under FHIR rules. Use urn:uuid: for interdependent new resources in one Bundle; do not claim it is the only valid form.
  5. Transaction entries: every entry needs a request.method and request.url valid for that method, and the server must declare the system-level transaction interaction.
  6. Profile resolution: before invoking $validate, confirm operation support and that the server resolves the intended VN Core package, version, dependencies, and terminology.
  7. Search support: standard syntax does not guarantee that a server implements a parameter or modifier. Compare the CapabilityStatement and test both matching and non-matching cases.
  8. Pagination: do not construct the next-page URL; follow Bundle.link[relation="next"] returned by the server.
  9. Security metadata: meta.security carries labels for a policy system to interpret; the label itself does not enforce authorization. meta.tag is general classification metadata and should not replace access-control policy.
  10. Encoding: FHIR JSON uses UTF-8; Accept-Charset is unnecessary. Ensure the client actually emits UTF-8 and test Vietnamese text round trips.

9. References

This page was authored by the Omi HealthTech editorial team, with technical review by HungPM (Phan Mạnh Hùng) and factual checks on HAPI CLI semantics, identifier URI, and the Encounter.class CodeSystem.