flexway
FRENTalk to an expert

Serving your first API

TutorialOperationsSince version 0.1.0

Markdown

Operations guide: configure, deploy and observe the gateway. What the capability does, product side: product guide; calling it: developer guide.

Goal: route a declared operation, with no identity and no quota, to check the chain end to end.

Command

cat > flexway.yaml <<'YAML'
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://host.docker.internal:8081   # your API; in a container, localhost is the gateway itself
    protocol: http
    timeout: 3s
YAML

flexway validate                     # offline, no cluster needed
docker run --rm --name flexway -p 8080:8080 -p 9090:9090 \
  --add-host=host.docker.internal:host-gateway \
  -v "$PWD:/etc/flexway:ro" \
  ghcr.io/flexwiz/flexway:<version>

An image tag carries the release's v (ghcr.io/flexwiz/flexway:v1.0.0). The gateway serves traffic on 8080 and its admin surface on 9090; the openapi.yaml sits next to flexway.yaml, and every operation in it carries an operationId - an operation without one is refused at load, since quotas, keys and audit name operations by it.

Check: an operation declared in the OpenAPI answers. An undeclared route answers 404 in application/problem+json - not because it is forbidden, because it does not exist.

Any operation without x-flexway-upstream uses the default upstream. That is what lets you start with four lines.

Path templates. A segment can mix parameters and text, as OpenAPI allows: /reports/{year}.pdf, /api/v{n}/status, /tiles/{z}-{x}-{y}.png, /tasks/{id}:cancel. Each parameter takes at least one character and stops at the first text that follows it (/files/{name}.{ext} on archive.tar.gz: name = archive, ext = tar.gz). Precedence: a literal segment (/reports/latest.pdf), then the mixed segment with the most text, then the bare parameter (/reports/{id}). Refused at load, naming the operation: two parameters side by side ({x}{y}), a lone brace, and two mixed segments of the same rank that can read the same text (a{x} and {x}a).

Before starting, the same check runs offline - no cluster, no network:

flexway validate
# or, without a checkout - the validator ships in every published image (signed, same digest):
docker run --rm -v "$PWD:/etc/flexway:ro" ghcr.io/flexwiz/flexway:<version> validate -v
docker run --rm -v "$PWD:/w:ro" ghcr.io/flexwiz/flexway:<version> validate --config /w/flexway.yaml --contract /w/next.yaml

Exit code 0 when the profile is accepted, 1 with the field at fault when it is refused, 2 on a misused command line. --contract validates another OpenAPI document against the same configuration, in place of contracts.openapi.

It replays exactly what the service does at start - same loader, same refusals. A validator that reimplemented the checks would eventually disagree with the thing it validates. One deliberate exception: a secret reference ("${VAR}", file:/...) is checked for its form, not resolved - a pipeline does not hold the production secrets. An unset variable is found at start, which refuses to serve and names it.

Tuning timeouts. Three budgets, with defaults that suit an ordinary API - write them only to depart from them:

server:
  timeouts:
    connect: 5s     # establishing the upstream connection
    read: 60s       # silence tolerated between two bytes - this, and only this, bounds a stream
    total: 30s      # the whole exchange, retries included
  shutdown:
    drain: 20s      # on SIGTERM, time left to what is in flight

The most specific wins: server.timeouts.total, then timeout on the upstream, then x-flexway-timeout on the operation in the contract. A search at 2s and an export at 120s live side by side without raising everyone's budget.

Check: an operation that runs past its budget answers 504 upstream_timeout at that budget, not at the client's. An SSE stream that emits at least once per read is never cut, however long it runs.

Say who answers for an operation, and when it is withdrawn. Two contract extensions, read for the console's catalogue - never on the request path:

info:
  title: Orders
  version: "1.0.0"
  x-flexway-owner: { team: orders-team, contact: "#orders" }   # every operation inherits it
paths:
  /v1/orders/{id}:
    get:
      operationId: getOrderV1
      deprecated: true
      x-flexway-sunset: "2027-06-30"      # withdrawal date: requires deprecated: true
      x-flexway-owner: { team: legacy-team }   # the operation's own wins

team is required, contact optional; 128 characters each. The contact is a team's channel (a list, a room, a page), never a person: every reader of the console sees it. The date is written YYYY-MM-DD. In gRPC, in the method's option:

rpc ListPayouts(Req) returns (Resp) {
  option deprecated = true;                 // Protobuf's native option, read as deprecated
  option (flexway.v1.operation) = { owner: { team: "payments-team" } sunset: "2027-03-01" };
}

Check: flexway validate refuses a date without deprecation ("is set on an operation that is not deprecated"), a malformed date, an owner without team or with an unknown key; the admin /config publishes owner and sunset in catalog. An operation without an owner loads: the catalogue's compliance reports it. The Sunset and Deprecation headers towards callers come with a later change.

Available since 1.2.0.

Next steps