DEV Community

Davi Lucas
Davi Lucas

Posted on AI-assisted

Configurando um Servidor HAPI FHIR com BR-Core

Adequação do Servidor FHIR ao BR-Core

Primeiro, é necessário entender como irá funcionar a arquitetura da nossa aplicação como um todo, levando em consideração principalmente o Back-end, Front-end, Servidor FHIR e a RNDS. Nesse modelo não vou entrar em detalhes da infraestrutura porque não é o foco deste artigo. Quero focar na estrutura principal da aplicação e, principalmente, na parte que envolve o servidor FHIR e sua adequação ao BR-Core.

Disclaimer: este artigo surgiu a partir dos problemas que enfrentei durante a implementação de um ambiente FHIR para um sistema do PET-Saúde de Sobral. Por isso, alguns exemplos, nomes de containers, bancos e credenciais utilizados ao longo do artigo são baseados nesse ambiente.

As credenciais apresentadas aqui são exclusivamente de desenvolvimento/teste, criadas para facilitar que os alunos envolvidos no projeto consigam lembrar, reproduzir e modificar o ambiente local sem precisar ficar procurando quais valores utilizar. Elas não correspondem a credenciais de acesso a ambientes de produção ou a serviços reais.

A ideia deste artigo é justamente documentar o caminho que percorri durante a implementação, incluindo alguns dos problemas e decisões que apareceram no processo. Então, se você encontrar alguma solução melhor para alguma das etapas, fique à vontade para compartilhar. 😄

Overview das implementações FHIR

No documento que disponibilizei anteriormente para entendimento do que se trata o HAPI FHIR, também dei ênfase no problema que passamos ao tentar trabalhar com pacientes brasileiros. O servidor conseguia trabalhar normalmente com recursos FHIR, mas não conhecia os perfis específicos do BR-Core, pois eles ainda não haviam sido instalados no ambiente.

Observação: eu ainda não postei o outro artigo porque fiquei muito feliz depois de conseguir resolver esse problema e decidi postar esse primeiro hehe. 😅

Nos repositórios que encontrei, dois projetos acabaram chamando bastante atenção:

  • hapifhir/hapi-fhir

  • hapifhir/hapi-fhir-jpaserver-starter

O primeiro é o projeto principal do HAPI FHIR, onde ficam os módulos responsáveis pela implementação das funcionalidades do FHIR. Ele é desenvolvido principalmente em Java e serve como base para todo o ecossistema do HAPI. Se quiséssemos montar um servidor FHIR completamente do zero utilizando essa implementação, precisaríamos conhecer bastante de Java/Spring Boot e também entender toda a parte de persistência, endpoints, configuração e infraestrutura envolvida. É uma alternativa interessante para quem precisa de uma solução extremamente customizada, mas não era exatamente o nosso caso.

O segundo já nos entrega uma aplicação de servidor baseada no HAPI FHIR JPA, permitindo subir o serviço de forma muito mais rápida sem precisar desenvolver toda essa estrutura do zero. Ele também possui suporte para execução em containers, o que facilita bastante quando estamos trabalhando com Docker.

É importante lembrar que o HAPI não é a única implementação disponível. Existem várias outras implementações open-source do FHIR em diferentes linguagens e frameworks, e elas podem ser consultadas na lista de implementações open-source do HL7.

No nosso caso, escolhemos a implementação em Java principalmente pela comunidade e pela quantidade de material disponível. Quando começamos a mexer com uma tecnologia dessas, inevitavelmente aparecem problemas de configuração, arquitetura e infraestrutura, então ter bastante documentação e gente que já passou pelos mesmos problemas ajuda muito. O próprio projeto já traz bastante material relacionado a Docker, PostgreSQL, CORS, proxy reverso, load balancers e outras coisas que provavelmente vamos precisar conforme o projeto crescer.

Mas existe uma peculiaridade importante: o HAPI FHIR está adequado ao padrão FHIR, mas ele não vem com os perfis específicos do Brasil instalados por padrão. É aí que entra o BR-Core.

Desenho da arquitetura

A arquitetura inicial que estamos pensando ficou mais ou menos assim:

Diagrama Mermaid

flowchart TD

    %% Estilos dos nós

    classDef frontend fill:#3b82f6,stroke:#2563eb,stroke-width:2px,color:#fff

    classDef laravel fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff

    classDef db fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff

    classDef fhir fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff

    classDef rnds fill:#8b5cf6,stroke:#7c3aed,stroke-width:2px,color:#fff


    subgraph Cliente ["Camada de Apresentação (Frontend)"]

        UI["Interface do Utilizador\n(React, Vue, ou Blade)"]:::frontend

    end


    subgraph Sistema_Principal ["Backend Principal"]

        API["Laravel App (API)\n- Autenticação\n- Orquestração\n- Assinatura Digital"]:::laravel

        LocalDB[("Banco Local\n(PostgreSQL/MySQL)\n- Utilizadores\n- Permissões\n- IDs de Mapeamento")]:::db

        API <-->|SQL| LocalDB

    end


    subgraph CasaOS ["Ambiente FHIR Local (Docker no CasaOS)"]

        HAPI["Servidor HAPI FHIR\n(BR-Core Ativo)"]:::fhir

        FHIRDB[("Banco FHIR\n(PostgreSQL 15)")]:::db

        HAPI <-->|JDBC/Hibernate| FHIRDB

    end


    subgraph Externo ["Ministério da Saúde"]

        RNDS["RNDS\n(Rede Nacional de Dados em Saúde)"]:::rnds

    end


    %% Fluxo de comunicação

    UI <-->|HTTP / REST JSON| API

    API <-->|FHIR REST JSON| HAPI

    API <-->|Integração segura| RNDS

Papel de cada componente na arquitetura

1. Interface do Utilizador (Frontend)

O frontend não precisa conversar diretamente com o servidor FHIR. A ideia é que ele envie os formulários normais e consuma os endpoints REST expostos pelo Laravel. Dessa forma, a camada visual não precisa saber o que é FHIR ou como um Patient deve ser estruturado segundo o BR-Core. Essas responsabilidades ficam concentradas no backend.

2. Laravel App — o orquestrador

O Laravel é o cérebro da operação. Ele recebe a requisição do frontend e divide as águas entre o que pertence à aplicação e o que pertence ao servidor FHIR.

Dados como passwords, tokens de sessão, permissões de acesso e a interface administrativa do projeto PET-Saúde ficam no banco da própria aplicação. Já os dados clínicos seguem para o HAPI FHIR.

O Laravel atua como uma espécie de tradutor. Ele pega os dados recebidos do formulário, aplica as regras necessárias e constrói o JSON do recurso FHIR de acordo com os perfis do BR-Core, enviando posteriormente esse recurso para o HAPI.

No banco de dados do Laravel, uma tabela de pacientes, por exemplo, poderia guardar apenas informações necessárias para o controle da aplicação, como:

id_local
nome
cpf
fhir_id
Enter fullscreen mode Exit fullscreen mode

O fhir_id seria a referência para o recurso correspondente dentro do servidor FHIR. Quando fosse necessário consultar o histórico clínico, o Laravel utilizaria esse identificador para buscar os dados no HAPI.

3. HAPI FHIR Server & Base de Dados

Esse é o nosso servidor FHIR. Como estamos adicionando o BR-Core, ele passa a ter acesso aos perfis brasileiros que vamos utilizar para estruturar e validar nossos recursos.

O banco de dados utilizado pelo HAPI é o PostgreSQL. A aplicação não deve manipular esse banco diretamente para criar ou alterar recursos FHIR. Quem deve controlar essa persistência é o próprio HAPI, através da API FHIR.

4. RNDS

Quando um atendimento estiver finalizado e precisar ser enviado para a rede nacional, o Laravel entra em ação novamente. A ideia é buscar no HAPI os recursos necessários, montar o conteúdo esperado para a integração e realizar a comunicação com a RNDS seguindo os requisitos definidos pelo Ministério da Saúde, incluindo os mecanismos de autenticação, transporte seguro e assinatura digital necessários.

Docker Compose e Envs

Com a arquitetura definida, podemos começar pela parte prática.

Primeiro, baixei a imagem do HAPI FHIR:

sudo docker pull hapiproject/hapi:v8.12.0-2
Enter fullscreen mode Exit fullscreen mode

Preferi utilizar uma versão específica em vez de latest, principalmente para evitar que uma atualização futura da imagem altere o comportamento do ambiente sem que tenhamos planejado isso.

Depois criei os arquivos docker-compose.yml e application.yaml.

application.yaml

O application.yaml ficou responsável principalmente pela conexão com o PostgreSQL, configuração do FHIR R4, CORS e algumas configurações do HAPI:

spring:
  datasource:
    # Conecta o HAPI ao container do PostgreSQL criado no docker-compose
    url: 'jdbc:postgresql://fhir-db:5432/hapi'
    username: postgres
    password: admin_pet_saude
    driverClassName: org.postgresql.Driver

  jpa:
    properties:
      # O dialeto obriga o Hibernate a traduzir as queries para o padrão PostgreSQL
      hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect

      # Desativa a busca full-text avançada inicialmente
      hibernate.search.enabled: false

hapi:
  fhir:
    # Habilita a versão R4 do FHIR, utilizada pelo BR-Core
    fhir_version: R4

    # Habilita o upload dinâmico de Implementation Guides
    ig_runtime_upload_enabled: true

    cors:
      allow_Credentials: false
      allowed_origin:
        - "*"

      # Em produção, substitua "*" pelos domínios reais
      allowed_headers:
        - Origin
        - Accept
        - Content-Type
        - Authorization
        - Cache-Control
        - If-Match
        - If-None-Match

      exposed_headers:
        - Location
        - Content-Location
        - ETag

    tester:
      home:
        name: PET-Saúde Local FHIR
        server_address: 'http://localhost:8080/fhir'
        refuse_to_fetch_third_party_urls: false
        fhir_version: R4
Enter fullscreen mode Exit fullscreen mode

Algumas funcionalidades mais avançadas ficaram de fora neste primeiro momento porque não eram necessárias para o MVP. A ideia é não adicionar complexidade antes de realmente precisar dela.

docker-compose.yml

O docker-compose.yml ficou responsável por subir tanto o servidor FHIR quanto o PostgreSQL:

services:
  fhir-server:
    container_name: pet_fhir_api
    image: "hapiproject/hapi:v8.12.0-2"

    ports:
      - "8080:8080"

    configs:
      - source: hapi_config
        target: /app/config/application.yaml

    depends_on:
      - fhir-db

    networks:
      - pet_saude_net

    restart: unless-stopped

  fhir-db:
    container_name: pet_fhir_db
    image: postgres:15-alpine

    restart: unless-stopped

    environment:
      POSTGRES_PASSWORD: admin_pet_saude
      POSTGRES_USER: postgres
      POSTGRES_DB: hapi

    ports:
      - "5432:5432"

    volumes:
      - fhir_pg_data:/var/lib/postgresql/data

    networks:
      - pet_saude_net

configs:
  hapi_config:
    file: ./application.yaml

volumes:
  fhir_pg_data:

networks:
  pet_saude_net:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Depois de subir o ambiente, alguns endpoints interessantes para testar são:

http://localhost:8080
http://localhost:8080/fhir/swagger-ui
http://localhost:8080/fhir/metadata
Enter fullscreen mode Exit fullscreen mode

O /fhir/metadata, em especial, retorna o CapabilityStatement do servidor e ajuda bastante a entender o que aquela instância do FHIR suporta.

HAPI FHIR CLI

Até aqui temos um servidor FHIR funcionando, mas ainda falta uma parte importante: precisamos colocar as definições do BR-Core dentro dele.

Foi nessa etapa que encontrei o HAPI FHIR CLI.

A documentação oficial pode ser encontrada aqui:

O CLI é uma ferramenta de linha de comando do HAPI que permite realizar algumas operações relacionadas ao ecossistema FHIR. No nosso caso, ele seria útil principalmente para trabalhar com pacotes e Implementation Guides.

Minha ideia inicial era utilizar o próprio CLI para pegar as definições do BR-Core e gerar o .tgz que posteriormente seria instalado no servidor.

Foi aí que comecei a procurar onde exatamente estava o pacote que precisava.

O BR-Core

Para entender e baixar o material do BR-Core, utilizei a documentação disponibilizada pelo projeto:

Uma coisa legal que descobri nesse processo foi o Forge, uma ferramenta que permite editar e criar perfis FHIR.

Download:

O Forge é interessante principalmente quando começarmos a criar ou adaptar perfis próprios, já que trabalhar manualmente com todos aqueles JSONs de StructureDefinition pode ficar meio doloroso depois de um tempo.

Mas, para o que eu precisava agora, o mais importante era conseguir o pacote oficial do BR-Core.

E aqui vem, novamente, uma das partes mais chatas desse projeto: encontrar o bendito **package.tgz**.

Eu já tinha falado que essa parte foi meio sofrida, mas vale reforçar porque provavelmente alguém vai passar pela mesma coisa. Na documentação oficial do BR-Core existe bastante coisa para baixar, mas não fica muito claro que, para utilizar o pacote no HAPI, você precisa baixar o guia de implementação completo em **.zip** e procurar o pacote lá dentro. Não existe aquele momento mágico de “clique aqui para baixar o package.tgz”. Você baixa o ZIP, abre um monte de arquivo e começa a procurar por package, na esperança de encontrar o que precisa.

Enquanto eu não encontrava o arquivo, comecei até a estudar o HAPI FHIR CLI, porque achei que provavelmente teria que gerar o .tgz por conta própria. Felizmente, não precisei inventar moda.

Depois de abrir o ZIP e pesquisar por package, encontrei os pacotes das implementações já prontos, no formato .tgz. No meu caso, como o servidor está configurado para trabalhar com FHIR R4, o arquivo que nos interessa é:

package.r4.tgz
Enter fullscreen mode Exit fullscreen mode

E uma comparação que achei curiosa é que, no outro guia disponibilizado junto à documentação, o Guia de Implementação da SES GO - CORE, da Secretaria de Estado da Saúde de Goiás (SES-GO), essa parte é bem mais tranquila. Na página de downloads existe um tópico específico chamado “NPM Package”, justamente para explicar e disponibilizar o package.tgz. Você entra, encontra o tópico, baixa o pacote e segue a vida.

Já no material do BR-Core que eu estava utilizando, precisei fazer essa pequena caça ao tesouro para descobrir que o pacote já estava dentro do guia completo. 😅

POR FAVOR LEIA: BAIXE O GUIA COMPLETO EM **.ZIP**, ABRA O ARQUIVO E PESQUISE **PACKAGE**.

É sério. 😭 Os pacotes .tgz das implementações já estão lá, prontos para uso. Depois é só escolher o que corresponde à versão do FHIR utilizada pelo seu servidor.

Com os arquivos finalmente encontrados, podemos partir para a configuração do HAPI e para a instalação do pacote no nosso servidor.

Organização do projeto

Depois que encontrei o pacote, organizei os arquivos em uma pasta própria para deixar tudo relacionado ao servidor FHIR no mesmo lugar:

pet-compose/
├── docker-compose.yml
├── application.yaml
└── fhir-packages/
    └── package.r4.tgz
Enter fullscreen mode Exit fullscreen mode

A ideia é que essa pasta fhir-packages concentre os pacotes que forem necessários futuramente.

Como o pacote está na máquina host, precisamos fazer com que o container também consiga enxergá-lo. Para isso, adicionei um bind mount ao serviço do FHIR:

volumes:
  - ./fhir-packages:/fhir-packages
Enter fullscreen mode Exit fullscreen mode

Esse volume faz uma ligação entre uma pasta da máquina que está executando o Docker e uma pasta dentro do container.

Então temos:

Máquina host
./fhir-packages
       │
       │ bind mount
       ▼
Container
/fhir-packages
Enter fullscreen mode Exit fullscreen mode

Assim, o arquivo que está no nosso projeto também fica disponível para o ambiente do HAPI.

Instalando o BR-Core

Com o pacote em mãos, chegou a hora de instalá-lo.

Aqui aconteceu uma das partes mais interessantes do processo, porque a documentação nem sempre deixa claro qual é exatamente o formato que determinada operação do HAPI espera.

A operação utilizada pelo servidor é:

POST /fhir/ImplementationGuide/$install
Enter fullscreen mode Exit fullscreen mode

Na primeira tentativa, tentei enviar diretamente o .tgz para o endpoint utilizando application/gzip.

O HAPI respondeu que a operação precisava receber um Content-Type FHIR. Ou seja, não bastava simplesmente mandar o arquivo compactado.

Depois disso, transformei o .tgz em Base64:

base64 -w 0 /home/debugz/pet-compose/fhir-packages/package.r4.tgz > /tmp/brcore.b64
Enter fullscreen mode Exit fullscreen mode

Agora o conteúdo do pacote estava salvo em:

/tmp/brcore.b64
Enter fullscreen mode Exit fullscreen mode

Em seguida, criei um Parameters FHIR contendo esse conteúdo:

python3 - <<'PY'
import json

with open("/tmp/brcore.b64") as f:
    # Remove a quebra de linha final gerada pelo base64
    data = f.read().strip()

params = {
    "resourceType": "Parameters",
    "parameter": [
        {
            "name": "npmContent",
            "valueBase64Binary": data
        }
    ]
}

with open("/tmp/brcore-install.json", "w") as f:
    json.dump(params, f)

print("Arquivo criado: /tmp/brcore-install.json")
PY
Enter fullscreen mode Exit fullscreen mode

E finalmente fiz o POST:

curl -X POST \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  --data-binary @/tmp/brcore-install.json \
  "http://localhost:8080/fhir/ImplementationGuide/\$install"
Enter fullscreen mode Exit fullscreen mode

O retorno foi:

{
  "resourceType": "Parameters"
}
Enter fullscreen mode Exit fullscreen mode

Nesse momento eu já tinha uma boa indicação de que a instalação tinha sido aceita, mas ainda faltava confirmar se os recursos realmente estavam disponíveis.

Nem tudo funcionou de primeira

Como quase sempre acontece quando estamos configurando alguma coisa nova, o caminho não foi exatamente reto.

A primeira tentativa de chamar o $install utilizando diretamente o .tgz falhou porque o endpoint esperava uma requisição FHIR.

Depois, na primeira versão do Parameters, utilizei um nome de parâmetro incorreto e o HAPI acabou retornando um NullPointerException. Depois de entender melhor o formato esperado pela operação, alterei o parâmetro para npmContent e a instalação passou a ser processada.

Também apareceu um warning durante a instalação: o StructureDefinition/br-core-careteam estava com status = draft, e a configuração atual do HAPI não permitiu que esse recurso fosse instalado.

Mesmo assim, o restante do pacote continuou sendo processado normalmente.

No final, o HAPI informou que havia criado:

46 resources of type StructureDefinition
Enter fullscreen mode Exit fullscreen mode

e ignorado:

1 resource of type StructureDefinition
Enter fullscreen mode Exit fullscreen mode

Então, apesar dos percalços, a instalação do pacote funcionou.

Confirmando o BR-Core no servidor

Agora vinha a parte que eu considero mais importante: não basta o servidor responder que instalou alguma coisa. Precisamos confirmar que os recursos realmente estão disponíveis.

Para isso, fiz uma busca pelo StructureDefinition do paciente:

curl -s \
  -H "Accept: application/fhir+json" \
  "http://localhost:8080/fhir/StructureDefinition?url=https://br-core.saude.gov.br/fhir/StructureDefinition/br-core-patient" \
  | jq '.entry[]?.resource | {resourceType,id,url,version,status}'
Enter fullscreen mode Exit fullscreen mode

O resultado foi:

{
  "resourceType": "StructureDefinition",
  "id": "1080",
  "url": "https://br-core.saude.gov.br/fhir/StructureDefinition/br-core-patient",
  "version": "1.3.0",
  "status": "active"
}
Enter fullscreen mode Exit fullscreen mode

Ou seja, o br-core-patient realmente está disponível no servidor, com a URL canônica correta, versão 1.3.0 e status active.

Foi aqui que finalmente deu para dizer: beleza, o BR-Core está no nosso HAPI.

E o ImplementationGuide?

Durante a instalação, o HAPI também encontrou o ImplementationGuide dentro do pacote:

ImplementationGuide-br.gov.saude.br-core.fhir.json
Enter fullscreen mode Exit fullscreen mode

com a URL:

https://br-core.saude.gov.br/fhir/ImplementationGuide/br.gov.saude.br-core.fhir
Enter fullscreen mode Exit fullscreen mode

Porém, quando fiz a busca:

curl -s -G \
  -H "Accept: application/fhir+json" \
  --data-urlencode \
  "url=https://br-core.saude.gov.br/fhir/ImplementationGuide/br.gov.saude.br-core.fhir" \
  "http://localhost:8080/fhir/ImplementationGuide"
Enter fullscreen mode Exit fullscreen mode

o servidor retornou:

{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 0
}
Enter fullscreen mode Exit fullscreen mode

Isso foi uma situação interessante porque os StructureDefinitions foram instalados e podem ser consultados normalmente, enquanto o ImplementationGuide não aparece no endpoint de recursos.

Ainda não vou entrar muito nessa investigação aqui porque ela merece um artigo próprio se eu for realmente aprofundar no comportamento interno do PackageInstallerSvc e na diferença entre o gerenciamento de packages e a persistência dos recursos no repositório FHIR.

Para o objetivo atual, o que importa é que os perfis necessários do BR-Core já estão disponíveis no servidor.

Estado atual

Então, neste momento, temos:

HAPI FHIR
  versão: v8.12.0-2
  FHIR: R4
  container: pet_fhir_api

PostgreSQL
  versão: 15
  container: pet_fhir_db

BR-Core
  package: br.gov.saude.br-core.fhir.r4
  versão: 1.3.0

StructureDefinitions
  criados: 46
  ignorados: 1

br-core-patient
  status: active
  versão: 1.3.0
  disponível pela API: sim

ImplementationGuide
  encontrado no pacote: sim
  disponível em /ImplementationGuide: não
Enter fullscreen mode Exit fullscreen mode

O br-core-careteam foi o único StructureDefinition ignorado porque estava como draft.

Próximos passos

Agora que temos a base funcionando, os próximos passos são começar a utilizar os perfis do BR-Core de verdade dentro da aplicação.

Isso significa definir quais recursos realmente serão utilizados pelo projeto, criar os Patient, Practitioner, Encounter e outros recursos necessários, validar esses recursos contra os perfis corretos e começar a estruturar a comunicação entre o Laravel e o HAPI FHIR.

Depois disso entra uma parte ainda maior do projeto, que é trabalhar a integração com a RNDS.

A ideia não é tentar resolver tudo de uma vez. Primeiro colocamos o servidor FHIR de pé, depois adicionamos os perfis necessários, validamos os recursos e só então começamos a construir as integrações em cima disso.

Conclusão

No começo, parecia que o problema era simplesmente:

"Precisamos colocar o BR-Core no nosso HAPI FHIR."

Na prática, acabou envolvendo Docker, PostgreSQL, HAPI FHIR, HAPI CLI, pacotes FHIR, .tgz, Base64, scripts em Python e alguns erros no caminho.

E foi justamente isso que tornou o processo interessante.

No final, conseguimos instalar o pacote:

br.gov.saude.br-core.fhir.r4#1.3.0
Enter fullscreen mode Exit fullscreen mode

e confirmar que os StructureDefinitions do BR-Core estão disponíveis no nosso servidor HAPI FHIR.

Ainda existem alguns pontos que quero investigar e melhorar, mas pelo menos agora temos uma base funcional para continuar desenvolvendo a infraestrutura de saúde digital do projeto.

E acho que essa é uma das partes mais legais de trabalhar com tecnologia: você começa pensando que vai "só instalar um pacote" e termina estudando como um servidor FHIR resolve packages por dentro. 😂

E se você trabalha com FHIR, BR-Core, RNDS ou qualquer coisa relacionada e tiver alguma sugestão, correção ou uma forma melhor de fazer alguma das coisas que mostrei aqui, pode falar comigo! Ainda estou aprendendo bastante sobre esse ecossistema, então toda troca com quem já tem mais experiência é muito bem-vinda. A ideia aqui é justamente compartilhar o que fui descobrindo no caminho e também aprender com quem já passou por problemas parecidos. 😄

Referências

Top comments (0)