Building a Config-Driven API Facade on Azure Functions
The first time a partner changes a request field from orderId to externalOrderId, you learn whether you have an edge or a tangle. If five projects each map that field in their own controllers, you get five hotfixes and five slightly different error shapes. If you run a config-driven API facade pattern on a single Azure Function, you change a route profile, reload configuration, and keep the internal event contract stable.
This deep dive follows my microservices integration patterns catalog. The short version: a single catch-all .NET isolated Azure Function turns config-defined HTTP routes into validated, authenticated, and idempotent CloudEvents, then publishes them to Event Grid and/or Service Bus with a configured HTTP response. Engineers get the pipeline and config contract. Managers get one scoreboard: one external contract per channel, one shared function package, project variance only in configuration.
I am not rewriting the Azure Architecture Center. I am describing how I treat the facade as a reusable Lego block—not as a one-off “API project” per partner.
Implementation reference: the facade connector in github.com/fransiscuss/integration-azure (functions/facade/, docs in docs/FACADE.md, sample routes in functions/facade/config/facade.sample.yaml). Snippets below are from that tree (around commit a24bc48)—copy paths, not folklore.
What is the API facade pattern?
An API facade pattern (sometimes called a facade microservice) is a thin edge that owns the external or channel-facing HTTP contract. It validates input, authenticates per route policy, maps the request into a stable internal event, and returns a consistent response shape. Downstream systems speak your internal vocabulary. Partners and channels speak theirs.
That sits next to patterns Microsoft documents for the edge—especially Backends for Frontends (BFF), gateway routing, and gateway aggregation. Azure API Management is still the public gate. The facade is useful when you need real application logic at the edge: JSON Schema validation, route-level auth modes, idempotency, CloudEvent construction, and multi-sink publish behaviour that policies alone get awkward to own and unit test.
| Edge piece | Primary job | What I keep out of it |
|---|---|---|
| API Management | TLS edge, products/subscriptions, throttling, coarse JWT, observability at the gate | Deep domain rules; long-running orchestration |
| API facade (Azure Function) | Channel contract, per-route auth, JSON Schema, idempotency, map → CloudEvent, multi-sink publish | Pricing, eligibility, “fix the business outcome” |
| BFF (per UI) | Shape APIs for one frontend experience (web vs mobile) | Partner B2B integration catalog concerns |
| Domain service | Business decisions and state | Partner-specific field aliases and transport quirks |
If everything lives only in APIM policies, you eventually encode application behaviour in fragments that no one wants to unit test. If everything lives only in domain services, partner quirks leak inward forever. The facade is the deliberate seam.
API Management vs API facade vs BFF
| Situation | Prefer | Why |
|---|---|---|
| Throttle, API key, simple header inject, pass-through to one backend | APIM only | No extra hop; policies are enough |
| Partner HTTP API with versioned schemas, CloudEvents, multi-sink publish | APIM + API facade on Functions | Testable app logic + shared package across projects |
| Mobile vs web need different payloads from same domain | BFF per client | UI-driven shaping, not partner ingress |
| Fan-in multiple internal reads for one screen | BFF or gateway aggregation | Chattiness is UI-side, not partner ingress |
| Heavy field mapping after accept | Facade emits CloudEvent → Transformer on the bus | Keeps edge fast; mapping versioned as its own block |
Mild opinion: teams that “just use APIM for everything” eventually rebuild a half-tested programming language in policies. Teams that “just expose domain controllers to the internet” eventually rebuild partner management in every service. The facade block is the boring middle that scales with partner count.
Reference architecture on Azure Functions


The reference host is a .NET isolated Azure Function deployed as a Linux Function App—not a general Container Apps “API host” with partner controllers. The shared artifact is a versioned Function package, not a forked image per customer. App settings hold endpoints and Key Vault references. Route tables and JSON Schemas live in private Blob Storage and are periodically reloaded. That is the template model from the catalog: CI/CD pulls shared code; projects differ by config.
The Facade translates each partner-specific HTTP request into a canonical CloudEvent. Route configuration controls the event type, source, ID, extensions, payload, and destination without introducing partner-specific controllers.
How the request pipeline works
One catch-all Function handles every configured route. Adding an endpoint changes configuration, not compiled Functions or controllers. That is the most important implementation detail of this design.
The pipeline, in order:
- Match HTTP method and route template.
- Enforce body-size and content-type limits.
- Authenticate according to route configuration.
- Parse JSON.
- Validate against the configured JSON Schema.
- Check the idempotency key.
- Map the request into a CloudEvent.
- Publish to configured sinks.
- Render the configured success or failure response.
The Function trigger is deliberately Anonymous at the host level because runtime route matching and authentication are configuration-driven. That makes route configuration part of the security boundary—more on that below.
Config-driven route example
Behaviour is represented by cloudEvent, sinks, onPartialFailure, and configurable response cases—not by invented fields like mode: asyncAccept or a separate error catalog object. App settings supply endpoints; the YAML/JSON route file supplies the contract:
routes:
- id: order-created
route: "orders/{orgId:guid}/created"
methods: [POST]
auth:
mode: aadJwt
issuer: "https://login.microsoftonline.com/${tenantId}/v2.0"
audience: "api://integration-facade"
requiredScopes: [events.publish]
validation:
schemaRef: payloads/order-created.v1.json
rejectUnknownProperties: true
cloudEvent:
type: com.acme.order.created.v1
source: "/acme/orders/${$route['orgId']}"
id: "${$.orderId ?? $newGuid}"
data: "${$}"
sinks:
- kind: eventgrid
endpoint: "${$env['EVENTGRID_TOPIC_ENDPOINT']}"
- kind: servicebus-queue
queueOrTopic: "${$env['ORDERS_QUEUE']}"
messageId: "${$ce.id}"
onPartialFailure: fail
response:
onSuccess:
status: 202
body:
status: accepted
eventId: "${$ce.id}"
Secrets stay out of the route file. Connection material resolves at runtime through managed identity and Key Vault references. There is no route-level enabled kill switch in the current model; if you need one, treat it as a recommendation (feature flag or config omission) rather than a field the runtime already understands. OpenAPI synchronization with the live route table is a useful future capability—the implemented contract today is JSON Schema references.
What the catch-all Function actually looks like
One HTTP trigger owns every configured route. Azure Functions binds a catch-all path at compile time; runtime matching, auth, validation, CloudEvent mapping, and sinks all come from config. From functions/facade/Functions/FacadeFunction.cs:
[Function("FacadeApi")]
public async Task<IResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route = "{*path}")]
HttpRequest request,
CancellationToken ct)
{
var path = request.Path.Value ?? "/";
var match = routeMatcher.Match(request.Method, path);
if (match is null)
return Results.NotFound();
var route = match.Route;
// then: body limits → RouteAuthenticator → JSON Schema →
// idempotency → CloudEventMapper → FacadeSinkPublisher → ResponseBuilder
}
Anonymous at the host trigger is intentional: each route’s auth block decides aadJwt, API key, HMAC, function key, or explicit none. Pair that with APIM/Front Door and private Function access so the open trigger is not a second public door.
Route behaviour is data. A trimmed shape from functions/facade/config/facade.sample.yaml:
routes:
- id: order-created
route: "orders/{orgId:guid}/created"
methods: [POST]
auth:
mode: aadJwt
issuer: "https://login.microsoftonline.com/${tenantId}/v2.0"
audience: "api://integration-facade"
requiredScopes: [events.publish]
validation:
schemaRef: payloads/order-created.v1.json
rejectUnknownProperties: true
cloudEvent:
type: "com.acme.order.created.v1"
source: "/acme/orders/${$route['orgId']}"
id: "${$.orderId ?? $newGuid}"
data: "${$}"
sinks:
- kind: eventgrid
endpoint: "${$env['EVENTGRID_TOPIC_ENDPOINT']}"
- kind: servicebus-queue
queueOrTopic: "${$env['ORDERS_QUEUE']}"
messageId: "${$ce.id}"
Full attribute set, expression language ($header, $route, pipes, ??), response cases, and idempotency TTL are documented in the repo’s docs/FACADE.md—keep this post as the practitioner cut, not a second full manual.
CloudEvent mapping
CloudEvents are the strongest differentiator of this facade. The edge does not “map to an internal command” in the abstract. It builds a CloudEvent 1.0 envelope:
id,type,source,subject,timedatacontenttypeanddataschema- Custom extension attributes
- Request body (or a projected subset) into
data - Values drawn from body, headers, query parameters, route values, environment settings, timestamps, and W3C trace context
The same event can then use:
- Structured binding: the complete CloudEvent is the message body.
- Binary binding:
databecomes the body and CloudEvent attributes become Service Bus application properties. - Native CloudEvent publishing to Event Grid.
Downstream consumers subscribe to a stable event type and source pattern. Partner field aliases stay at the edge. That is why the facade microservice earns its place in the catalog instead of being “just APIM plus a controller.”
Authentication and authorization
APIM can still apply coarse JWT or certificate policy at the public edge. Independently, each route can authenticate using configuration:
- AAD JWT (issuer, audience, required scopes)
- Function key
- API key
- HMAC
- Explicit anonymous access
Principles I treat as non-negotiable:
- Production routes should be default-deny.
auth.mode: nonemust be an explicit exception, not an accident of missing config.- When APIM is mandatory, restrict direct Function access (private endpoints / access restrictions) so the Anonymous trigger is not a second public internet door.
- Validate route configuration at startup before accepting traffic.
- Keep APIM and facade authentication responsibilities explicit to prevent policy drift (“the gateway checked it” vs “the function checked it”).
- Return sanitized public errors while logging internal authentication, mapping, and sink details.
- HMAC authentication should include a timestamp/nonce and replay window, not only a signature over the body.
- Maintain W3C trace context separately from partner-facing correlation identifiers.
Claims and scopes after a successful JWT validation still belong at the application layer when the decision is route-specific. Do not let “it passed the gateway” become the whole security story.
JSON Schema validation
Reject bad payloads before they become poison messages. Each route points at a schema reference (for example payloads/order-created.v1.json) with options such as rejecting unknown properties. Fail closed: never invent business data at the edge to “make the map work.”
Schema files live next to the route tables in Blob and reload with the same cadence as routes. Version the schemas the same way you version events—silent field renames are how consumers break in production without a deploy.
Idempotency and replay semantics
Mutating routes should require an idempotency key. The pipeline records the key before CloudEvent mapping and sink delivery so retries do not double-create. Scope keys by route plus tenant/caller, not only route plus the caller-provided key, and reject reuse of the same key with a different request body (store a request hash).
That design has a real distributed-systems risk when you publish to multiple sinks sequentially:
- Event Grid accepts the event.
- Service Bus fails.
- Facade returns 503.
- Client retries with the same idempotency key.
- The retry is treated as a duplicate and does not repair the missing Service Bus delivery.
Similar pain appears if mapping fails after the key has already been recorded. If the idempotency record stores only the key—not the original event ID, request hash, response, processing state, or per-sink delivery status—you cannot safely resume.
Production patterns I recommend choosing deliberately:
- Publish once to one authoritative durable sink, then fan out downstream.
- Use a transactional outbox and an asynchronous dispatcher.
- Store per-sink delivery state and retry only missing sinks.
- Persist original event ID, request hash, response, and processing status in the idempotency record (for example in Cosmos DB).
Document the chosen semantics in the partner contract. “We are idempotent” without multi-sink behaviour is how support invents folklore.
Event Grid vs Service Bus sinks
A route can publish to Event Grid, a Service Bus queue, a Service Bus topic, or multiple sinks. That is intentional: Event Grid is a natural fit for fan-out of CloudEvents; Service Bus is a natural fit for competing consumers, sessions, and peek-lock workers. Peek-lock and dead-letter policy still matter for everything after the edge—I covered that seam in the Service Bus peek-lock guide.
The connector publishes and returns a configured response (often 202 with the CloudEvent id). It does not currently provide an operation-status API or a general synchronous downstream request/reply. Prefer honest async acceptance over pretending the facade is a distributed transaction coordinator.
Partial failure and delivery guarantees
onPartialFailure controls what happens when one sink succeeds and another fails. A fail-fast setting protects you from silent split-brain only if idempotency and retry semantics can repair the gap. Otherwise you trade a clear 5xx for a permanent one-sided delivery.
Failure modes I design for:
- Validation theatre: schema says required, mapper defaults missing fields. Fail closed.
- Dual source of truth: APIM OpenAPI drifts from facade JSON Schemas. CI-check one story; treat drift as a pipeline failure. OpenAPI sync is a future hardening step, not today’s source of truth.
- Sync creep: stakeholders want the facade to “just call billing quickly.” This design returns configured responses after publish; do not grow a hidden request/reply mesh inside the edge.
- God route config: business rules embedded in mapping expressions. Push rules to domain; keep facade maps structural and event-centric.
- Forked facades per customer: the anti-pattern this series exists to kill. Add a config profile—not
Facade.Contoso. - Multi-sink + thin idempotency: as above—pick an authoritative sink or store per-sink state.
Security and observability
Because the host trigger is Anonymous and routes are data, treat config reload and Blob ACLs as security controls equal to code review. Startup validation should refuse to serve if routes are missing auth, schemas fail to load, or sink endpoints cannot resolve.
Observability defaults I keep:
- W3C
traceparent/ trace context through the Function and into CloudEvent extensions where useful. - Partner-facing correlation or
eventIdin the HTTP response body when configured. - Structured logs:
routeId, auth mode outcome, schema version, CloudEventid/type, sink results, idempotency hit/miss. - Metrics on validation failures, auth failures, partial publish failures, and duplicate keys.
Security reviews care about the same boundaries I use on AI edges: clear trust zones, least privilege identities, and auditability. Different verbs than an enterprise MCP gateway, same discipline—do not let every caller invent its own path to privileged systems.
When not to use a facade
- Simple pass-through with throttling only → APIM is enough.
- UI-specific aggregation for one app → BFF, not a partner integration facade.
- Multi-step business sagas (“check credit, reserve stock, book courier”) → domain or orchestrator, not the edge Lego block.
- Very low partner count and no shared catalog → a smaller shared library may win until partner pressure grows.
What “done” looks like
| Audience | Healthy signal | Smell |
|---|---|---|
| Business / product | New partner HTTP onboarding is mostly contract + config + UAT | Every partner is a multi-sprint greenfield API |
| Engineering manager | One Function package CVE patch rolls to all projects; route auth is default-deny | N repos named after customers; no shared SLOs |
| Engineers | Can add a route with schema + CloudEvent map + sinks in config without new controllers | Copy-paste controller + tribal deploy notes |
FAQ
What is the API facade pattern on Azure?
A thin, config-driven edge—here a single Azure Function—that exposes stable HTTP routes for partners or channels, validates and authenticates at the application layer, maps requests into CloudEvents, and publishes to Event Grid or Service Bus while keeping partner-specific quirks out of domain services.
Is this the same as a BFF?
Related, not identical. BFF optimizes for a specific frontend experience. An API facade in an integration catalog optimizes for partner/channel ingress and internal event stability. You can run both in one estate for different problems.
Why not put all of this in Azure API Management?
APIM is excellent for gateway concerns—products, throttling, certificates, basic transforms. Application-level JSON Schema validation, route-level auth modes, idempotency, CloudEvent construction, and multi-sink publish usually age better in a real Function with a normal CI pipeline. I use both: APIM in front, facade behind.
Does the facade call domain APIs synchronously?
In this reference design the connector publishes and returns a configured response. It does not provide a general operation-status API or synchronous downstream request/reply. Prefer that honesty over growing hidden sync coupling at the edge.
Why CloudEvents instead of a custom command DTO?
CloudEvents give you a standard envelope (type, source, id, time, extensions) and first-class bindings for Event Grid and Service Bus. Downstream processors share one event language even when partners send different HTTP shapes.
How does this relate to the rest of the catalog?
Facade is the HTTP front door. SFTP Processor is the file front door. Transformer, Encryptor/Decryptor, and API Sender handle shape, crypto boundary, and egress. The overview catalog is in the microservices integration patterns post; later posts cover the file and crypto blocks.
Can I read the real facade source?
Yes — functions/facade in integration-azure, plus docs/FACADE.md. Treat blog posts as design narrative; treat the repo as the contract when they disagree.
Closing
A strong API facade pattern on Azure Functions is boring on purpose. One catch-all Function, routes and schemas in Blob, CloudEvents out to Event Grid or Service Bus, idempotency with eyes open about multi-sink failure, and APIM still owning the public gate. It wins when the fifteenth partner does not force a sixteenth codebase, when validation failures are structured, and when internal services never learn that one customer still sends dates as DD/MM/YYYY strings.
Put APIM at the managed edge. Put shared facade code on one Function release train. Drive routes, schemas, auth, and sinks through versioned config. Stamp correlation and event IDs early. Keep business decisions out of the edge. That is the Lego block—next in this series, the SFTP Sender/Processor pair where the front door is a file drop instead of HTTP.