Liking cljdoc? Tell your friends :D

Spec reference

The BFF spec is a single YAML file (or multiple merged files) that describes every GraphQL endpoint the engine will serve.

Environment variables

Any string value can reference an environment variable:

url: "${USER_SERVICE_URL}/api/v1/users/{userId}"            # required — throws at startup if unset
url: "${USER_SERVICE_URL:-http://localhost:8081}/api/v1/users/{userId}"  # with fallback

Substitution runs at load time. Missing variables with no default fail at startup.

Top level

forward_headers:       # optional — headers forwarded to every backend step
  - authorization      # defaults to [authorization, x-request-id, x-correlation-id]
  - x-request-id

scalars:               # optional — custom GraphQL scalar types
  - name: DateTime     # implementation supplied in the handler's :scalars config
    description: "ISO-8601 timestamp"

input_types:           # optional — shared input object types for mutation args
  - name: MyInput
    fields:
      field1: String!
      field2: Int

endpoints:             # list of query and mutation definitions
  - ...

Every name in the scalars: block must have a matching implementation supplied to bff.core/create-handler under the :scalars config key (or via BffConfig.Builder#scalar in Java). See extensions.md — Custom scalar for the interface.

Endpoint

- name: myEndpoint          # GraphQL query/mutation name
  type: query               # query | mutation
  description: "..."

  args:
    simpleArg: { type: String!, default: "val" }

    # args can include per-field validation rules:
    userId:
      type: String!
      validate:
        pattern: "^[a-f0-9-]{36}$"   # regex — strings only
        message: "userId must be a UUID"
    amount:
      type: Float!
      validate:
        min: 0                        # inclusive lower bound — numbers only
        max: 10000                    # inclusive upper bound — numbers only
        message: "amount must be between 0 and 10000"

  output_type:
    name: MyType
    fields:
      field1: String!
      field2: "[Int]"
      nested:
        name: NestedType
        fields:
          value: String

  backend_chain:
    - ...

  output_mapping:
    field1:
      source: step
      step_id: some_step
      jq: ".nested.value"

  # optional — custom validator (runs before backend_chain; short-circuits on failure)
  validator:
    ns: my.project.validators     # resolve a Clojure var
    fn: my-validator-fn
  # or:
  validator:
    key: my-validator             # look up in the handler's :validators config

  # optional — transformer (runs after output_mapping is applied)
  transformer:
    ns: my.project.transformers
    fn: my-fn
  # or:
  transformer:
    key: my-transform

  # optional — custom resolver (replaces backend_chain entirely)
  resolver:
    key: my-resolver

Backend step

- id: my_step
  url: "https://svc/path/{argName}"   # {placeholders} filled from args or prior step data
  method: GET                          # GET POST PUT PATCH DELETE
  deps: [other_step]                   # explicit dependency — determines execution wave
  critical: true                       # if true, failure aborts the entire chain
  condition:                           # skip this step if the value resolves to false
    source: args
    key: someBoolean
  extra_headers:                       # step-specific headers merged with forwarded headers
    x-service-key: "secret"

  # Query params (GET/DELETE)
  input_mapping:
    param_name:
      source: args | step | value | ctx
      key: argName           # for args/ctx sources
      step_id: step_name     # for step source
      jq: ".nested.field"    # jq path (step source only)
      value: "literal"       # for value source

  # Request body (POST/PUT/PATCH)
  body_mapping:
    body_field:
      source: step
      step_id: prev_step
      jq: ".id"

  # Response caching
  cache:
    key: "prefix:{argName}"  # cache key template — same placeholder syntax as url
    ttl: 60000               # TTL in milliseconds (default 60000)

  # Retry on selected failure codes
  retry:
    max: 2                             # max additional attempts (3 calls total)
    on_code: [unauthorized, timeout]   # semantic codes from http_client — see below
    before_retry:                      # optional — run a hook between attempts
      key: cmap-token-refresh          # or {ns: my.ns, fn: my-hook}

  # Remap step failures to domain-specific error codes
  errors:
    400: DUPLICATE_CUSTOMER            # keyed by upstream HTTP status
    unauthorized: TOKEN_EXPIRED        # or by semantic code from http_client

Error code mapping

Each step failure carries both a raw HTTP status (when the failure came from an HTTP response) and a semantic code (:unauthorized, :timeout, …). By default the semantic code appears under extensions.code on the surfaced GraphQL error. Declaring an errors: map on the step remaps that to a domain-specific string of your choice:

- id: create_customer
  url: "{cmap_base}/portal/customers"
  method: POST
  errors:
    400: DUPLICATE_CUSTOMER
    422: VALIDATION_FAILED
    unauthorized: TOKEN_EXPIRED

Lookup order: HTTP status → semantic code (keyword) → semantic code (string). The mapping runs after the retry loop, so retry.on_code still matches against the raw semantic codes.

Retryable error codes

The semantic codes come from bff.http-client and match one-to-one with the kind of failure that occurred. Use these in retry.on_code:

CodeOrigin
no-responseUnderlying HTTP call returned no status
bad-requestUpstream returned 400
unauthorizedUpstream returned 401
forbiddenUpstream returned 403
not-foundUpstream returned 404
unprocessableUpstream returned 422
backend-errorUpstream returned 5xx
unexpected-statusAny other non-2xx status
connection-refusedTCP/connect failure
timeoutConnect or socket read timeout
unexpectedAny other client-side exception

The before_retry hook — see extensions.md — is called between attempts and can return a rewritten request-ctx (typically with a refreshed Authorization header).

Mapping sources

sourceDescription
argsGraphQL input argument (key: argName)
stepPrior step's response data — use jq for paths
valueHardcoded literal (value: "foo")
ctxRequest context — forwarded headers and remote-addr

jq expressions

All standard jq is supported via jackson-jq. Expressions are compiled at startup.

jq: ".id"                                      # top-level field
jq: ".profile.display_name"                    # nested
jq: ".contact.emails[0].address"               # array index
jq: "[.items[].title]"                         # collect from array
jq: ".price * .quantity"                       # arithmetic
jq: "if .verified then \"Verified\" else null end"  # conditional
jq: ".name // \"Anonymous\""                   # default value
jq: "[.items[] | select(.active)] | length"    # filter + count

Multiple specs

Multiple YAML files are merged into a single spec at startup. Endpoints from all files are combined; forward_headers and input_types are deduplicated.

(bff/create-handler ["base.yaml" "orders.yaml" "users.yaml"])

Can you improve this documentation? These fine people already did:
Rohit Thadani & rthadani
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close