← Back to list

Milestone 01 — Backend: parte 01: SDUI e configurações

Contract-first no backend do Pix — fronteiras que escalam

Josiel Manzonni in Construindo o Pix JSR · 2026-07-31 10:12 · 0 claps · 5.4 min read
#pixjsrbackend #open-finance #backend-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Milestone 01 — Backend: parte 01: SDUI e configurações

Contract-first no backend do Pix — fronteiras que escalam

1. Contexto: quatro serviços, quatro vocabulários

1. Contexto: quatro serviços, quatro vocabulários

Um contrato compartilhado pode parecer conveniente hoje e se tornar a forma mais silenciosa de acoplar serviços amanhã.

Leitura sugerida antes desse artigo:

[embed]O que é Server-Driven UI ? Entenda o que o servidor realmente controla em uma arquitetura SDUI, como o cliente transforma documentos declarativos…medium.com

1. Contexto e sequência do fluxo

Foram definidos dois endpoints para o SDUI porque configuração e tela respondem perguntas diferentes:

GET /mobile/v1/configuration: Quais features este cliente deve apresentar?
GET /mobile/v1/screens/{screenId}: Como uma tela específica deve ser composta?
""" arquivo utils.py"""

RESOURCES_DIRECTORY = Path(__file__).resolve().parent / "resources"
CONFIGURATION_FILE = RESOURCES_DIRECTORY / "configuration.json"
SCREENS_FILE = RESOURCES_DIRECTORY / "screens.json"

def read_configuration() -> dict[str, Any]:
    """Return the published configuration document."""
    return read_json_file(CONFIGURATION_FILE)

def read_screens() -> dict[str, Any]:
    """Return the published screens collection."""
    return read_json_file(SCREENS_FILE)

O primeiro carrega [resources/configuration.json](https://github.com/JosielManzonni/pix-jsr-backend/blob/main/sdui-service/resources/configuration.json). Esse documento contém as features avaliadas para o aplicativo, como pix_qr e pix_nfc, indicando visibilidade, habilitação e metadados de apresentação.

py@app.get(
    "/mobile/v1/configuration",
    response_model=AppConfiguration,
    responses={
        400: {"model": Problem},
        404: {"model": Problem},
        406: {"model": Problem},
        426: {"model": AppUpdateProblem},
        500: {"model": Problem},
    },
    tags=["Mobile configuration"],
)
async def get_mobile_configuration(
    context: MobileContext,
    response: Response,
) -> AppConfiguration | Response:
    """Return the static configuration as its generated contract model."""
    configuration = _load_configuration()
    serialized = configuration.model_dump(mode="json", exclude_none=True)
    etag = build_etag(serialized, "configuration")
    headers = _representation_headers(
        context,
        contract_version=configuration.contractVersion,
        etag=etag,
        revision_header="X-Configuration-Revision",
        revision=configuration.revision or 1,
    )
    if context.if_none_match == etag:
        return Response(status_code=304, headers=headers)

    _apply_headers(response, headers)
    return configuration

Depois, o cliente pode solicitar uma tela. O segundo endpoint lê [resources/screens.json](https://github.com/JosielManzonni/pix-jsr-backend/blob/main/sdui-service/resources/screens.json), procura o screenId e devolve a árvore de componentes nativos, como text, spacer e feature.

@app.get(
    "/mobile/v1/screens/{screenId}",
    response_model=Screen,
    responses={
        400: {"model": Problem},
        404: {"model": Problem},
        406: {"model": Problem},
        426: {"model": AppUpdateProblem},
        500: {"model": Problem},
    },
    tags=["Mobile screens"],
)
async def get_mobile_screen(
    screen_id: Annotated[ResourceId, Path(alias="screenId")],
    context: SduiContext,
    response: Response,
) -> Screen | Response:
    """Return one static screen as its generated contract model."""
    screen = _load_screen(screen_id)
    if context.contract_version < screen.contractVersion:
        raise ApiProblemError(
            406,
            "Unsupported SDUI contract",
            "The client cannot safely interpret this screen contract.",
            "SDUI_UNSUPPORTED_CONTRACT",
        )
    _validate_component_compatibility(screen, context)

    serialized = screen.model_dump(mode="json", exclude_none=True)
    etag = build_etag(serialized, f"screen-{screen_id}")
    headers = _representation_headers(
        context,
        contract_version=screen.contractVersion,
        etag=etag,
        revision_header="X-Screen-Revision",
        revision=screen.revision or 1,
    )
    if context.if_none_match == etag:
        return Response(status_code=304, headers=headers)

    _apply_headers(response, headers)
    return screen

Essa ordem forma um fluxo fácil de observar: configuração informa quais entradas existem; screen descreve a experiência escolhida. Ela não é uma obrigação protocolar — o cliente pode abrir uma tela conhecida diretamente — , mas representa o uso esperado na home.

2. Fronteiras contract-first

Cada domínio mantém seu próprio contrato:

As alternativas estão detalhadas na

A escolha de contratos locais foi formalizada na

Um OpenAPI único facilitaria descoberta, mas permitiria que componentes visuais referenciassem modelos internos de pagamento. Contratos locais aceitam alguma repetição para preservar ownership e releases independentes.

3. Arquitetura e escala

SDUI possui carga predominantemente de leitura. Para milhões de usuários, a evolução natural inclui cache local, ETag com revalidação 304, revisões imutáveis, cache de representação e instâncias stateless. Isso pode escalar sem aumentar também instâncias do lifecycle de pagamentos.

A fonte editável está em docs/diagrams/01-backend-contract-boundaries.mmd.

4. Headers e segurança

Os dois endpoints não usam autenticação neste milestone, mas exigem contexto mobile. Esses valores são declarações do cliente: ajudam compatibilidade, localização e cache, porém nunca autorizam uma operação de negócio.

5. Executando localmente com Uvicorn

No terminal, a partir da raiz do repositório:

cd sdui-service
source .venv-sdui/bin/activate
uvicorn main:app --reload --host 127.0.0.1 --port 8000

O serviço fica em http://127.0.0.1:8000. A documentação runtime pode ser consultada em /docs.

No Postman (ou usando curls), requests GET: informe URL e headers.

Se algum header obrigatório estiver faltando, é esperado esse comportamento

request errada:

curl --location 'http://127.0.0.1:8000/mobile/v1/configuration'

response:

{
    "type": "https://openpix.dev/problems/context-invalid-request",
    "title": "Invalid request context",
    "status": 400,
    "detail": "Invalid or missing request value: X-App-Platform, X-App-Version, X-App-Build, X-App-Package, X-Country-Code, Accept-Language.",
    "instance": "/mobile/v1/configuration",
    "code": "CONTEXT_INVALID_REQUEST",
    "correlationId": "7c535dca-c7f2-4ec1-9fa3-c26f754db234"
}

request correta:

curl --location --request GET 'http://127.0.0.1:8000/mobile/v1/configuration' \
--header 'X-App-Platform:  android' \
--header 'X-App-Version:  1.4.2' \
--header 'X-App-Build:  10402' \
--header 'X-App-Package:  com.openpix.jsr' \
--header 'X-Country-Code:  BR' \
--header 'Accept-Language:  pt-BR' \
--header 'X-SDUI-Contract-Version:  1' \
--header 'X-SDUI-Component-Versions:  text=1,spacer=1,feature=1' \
--header 'Content-Type: application/json' \
--data ''

response:

{
    "contractVersion": 1,
    "revision": null,
    "compatibility": null,
    "features": [
        {
            "id": "pix_qr",
            "visible": true,
            "enabled": false,
            "disabledMessage": null,
            "presentation": {
                "title": "QR Code",
                "subtitle": "Escaneie um código Pix",
                "icon": "qr_code",
                "style": "card"
            }
        },
        {
            "id": "pix_nfc",
            "visible": true,
            "enabled": false,
            "disabledMessage": "Disponível em breve",
            "presentation": {
                "title": "Pix por aproximação",
                "subtitle": "Aproxime o celular para pagar",
                "icon": "pix_nfc",
                "style": "card"
            }
        }
    ]
}

Endpoint GET screens/{screenId}

Definição e motivo

GET /mobile/v1/screens/{screenId}

Alterar a revisão de pix_home não exige inventar outro ID (existente em screen.json).

Parâmetros esperados

O único path parameter é screenId, validado no formato

^[a-z][a-z0–9_]{1,63}$. Não há query parameter ou body. Além dos headers mobile, a chamada exige a versão do contrato e o mapa de componentes.

Como testar no Postman

GET http://127.0.0.1:8000/mobile/v1/screens/pix_home
X-App-Platform: android
X-App-Version: 1.4.2
X-App-Build: 10402
X-App-Package: com.openpix.jsr
X-Country-Code: BR
Accept-Language: pt-BR
X-App-Capabilities: PIX_QR,PIX_NFC,PAYMENT
X-SDUI-Contract-Version: 1
X-SDUI-Component-Versions: text=1,spacer=1,feature=1

Uma resposta 200 começa assim:

{
  "contractVersion": 1,
  "id": "pix_home",
  "title": "Pix",
    "components": [
      {
        "id": "home_title",
        "type": "text",
        "compatibilityVersion": ["1"],
        "text": "Como deseja pagar?",
        "style": "title"
      }
    ]
}

Use um screenId inexistente para validar o 404. Troque feature=1 por uma versão não suportada para observar o 406. Reenvie o ETag recebido em If-None-Match para validar o 304.

6. Implementação e fonte de dados

FastAPI usa dependências tipadas para normalizar headers uma única vez. Os arquivos são lidos por *utils.py* e validados nos modelos Pydantic gerados:

configuration = AppConfiguration.model_validate(read_configuration())
screen = Screen.model_validate(read_screen(screen_id))

O filesystem é temporário e não faz parte do contrato HTTP. A decisão e os critérios para substituí-lo estão na ADR-SDUI-002 — Static JSON runtime source.

7. Trade-offs e conclusão

Separar os endpoints evita misturar disponibilidade de features com composição visual e permite cache e evolução independentes. O custo é manter dois recursos e garantir coerência entre feature IDs e telas publicadas.

O fluxo local agora pode ser reproduzido do início ao fim: iniciar Uvicorn, consultar configuração, escolher screenId, carregar a tela e validar 200, 304, 404 e 406. O próximo artigo aprofunda por que a compatibilidade é verificada tanto no backend quanto no renderer.

8. Materiais de apoio

Próximo capítulo:

[embed]Milestone 01 — Backend: parte 02: Compatibilidade SDUI: prevenção no backend, reação no cliente Como negociar versões sem transferir a responsabilidade final do renderer para o servidormedium.com

Contato

Quer conversar sobre Pix, Open Finance, Android, arquitetura de pagamentos ou Server-Driven UI?


메타데이터
post_id
e85f27ea4734
slug
milestone-01-backend-parte-01-sdui-e-configurações-e85f27ea4734
url
https://medium.com/construindo-o-pix-jsr/milestone-01-backend-parte-01-sdui-e-configura%C3%A7%C3%B5es-e85f27ea4734
canonical_url
https://medium.com/construindo-o-pix-jsr/milestone-01-backend-parte-01-sdui-e-configura%C3%A7%C3%B5es-e85f27ea4734
author_url
https://medium.com/@josielmanzonni
status
ok
fetched_at
2026-08-18 11:45:58