API-led connectivity transforms enterprise integration through layered, reusable APIs. This guide covers architectural patterns, implementation strategies, and governance for successful API-led adoption.
API-Led Architecture Layers
Understanding the three-tier architecture:
┌─────────────────────────────────────────────────────────────────┐
│ EXPERIENCE LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Mobile │ │ Web │ │ Partner │ │ IoT │ │
│ │ API │ │ API │ │ API │ │ API │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
├───────┼─────────────┼─────────────┼─────────────┼──────────────┤
│ │ PROCESS LAYER │ │ │
│ ┌────▼─────────────▼─────────────▼─────────────▼────┐ │
│ │ Order Process API │ │
│ └────┬─────────────┬─────────────┬─────────────┬────┘ │
│ │ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │Customer │ │Inventory│ │ Payment │ │Shipping │ │
│ │ Process │ │ Process │ │ Process │ │ Process │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
├───────┼─────────────┼─────────────┼─────────────┼──────────────┤
│ │ SYSTEM LAYER │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │Salesforce│ │ SAP │ │ Stripe │ │ FedEx │ │
│ │ sAPI │ │ sAPI │ │ sAPI │ │ sAPI │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼─────────────┼─────────────┼─────────────┼──────────────┘
│ │ │ │
[Salesforce] [SAP ERP] [Stripe] [FedEx API]
System API Implementation
System APIs expose backend systems with standardized interfaces:
Salesforce System API
<!-- salesforce-sapi.xml -->
<mule xmlns="http://www.mulesoft.org/schema/mule/core"
xmlns:http="http://www.mulesoft.org/schema/mule/http"
xmlns:salesforce="http://www.mulesoft.org/schema/mule/salesforce"
xmlns:apikit="http://www.mulesoft.org/schema/mule/mule-apikit">
<!-- API Router -->
<flow name="salesforce-sapi-main">
<http:listener config-ref="HTTPS_Listener_Config" path="/api/*">
<http:response statusCode="#[vars.httpStatus default 200]">
<http:headers>#[vars.responseHeaders default {}]</http:headers>
</http:response>
</http:listener>
<!-- Client ID enforcement -->
<apikit:router config-ref="salesforce-sapi-config"/>
</flow>
<!-- GET /customers -->
<flow name="get:\customers:salesforce-sapi-config">
<ee:transform doc:name="Build Query">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output text/plain
var searchTerm = attributes.queryParams.search default ""
var limit = attributes.queryParams.limit default 100
var offset = attributes.queryParams.offset default 0
---
"SELECT Id, Name, Email, Phone, Industry, BillingCity, BillingCountry,
CreatedDate, LastModifiedDate
FROM Account
WHERE IsDeleted = false" ++
(if (searchTerm != "") " AND Name LIKE '%" ++ searchTerm ++ "%'" else "") ++
" ORDER BY Name
LIMIT " ++ (limit as String) ++
" OFFSET " ++ (offset as String)]]></ee:set-payload>
</ee:message>
</ee:transform>
<salesforce:query config-ref="Salesforce_Config" doc:name="Query Accounts"/>
<ee:transform doc:name="Transform to Canonical">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
metadata: {
totalRecords: sizeOf(payload),
offset: attributes.queryParams.offset default 0,
limit: attributes.queryParams.limit default 100,
source: "salesforce"
},
customers: payload map {
id: $.Id,
externalId: $.Id,
name: $.Name,
email: $.Email,
phone: $.Phone,
industry: $.Industry,
address: {
city: $.BillingCity,
country: $.BillingCountry
},
createdAt: $.CreatedDate,
updatedAt: $.LastModifiedDate
}
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>
<!-- GET /customers/{customerId} -->
<flow name="get:\customers\(customerId):salesforce-sapi-config">
<salesforce:query-single config-ref="Salesforce_Config" doc:name="Get Account">
<salesforce:salesforce-query><![CDATA[
SELECT Id, Name, Email, Phone, Industry, Website, Description,
BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry,
ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry,
NumberOfEmployees, AnnualRevenue, CreatedDate, LastModifiedDate
FROM Account
WHERE Id = ':customerId'
]]></salesforce:salesforce-query>
<salesforce:parameters><![CDATA[#[{
customerId: attributes.uriParams.customerId
}]]]></salesforce:parameters>
</salesforce:query-single>
<choice doc:name="Check Result">
<when expression="#[payload != null]">
<ee:transform doc:name="Transform Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
id: payload.Id,
externalId: payload.Id,
name: payload.Name,
email: payload.Email,
phone: payload.Phone,
website: payload.Website,
industry: payload.Industry,
description: payload.Description,
employees: payload.NumberOfEmployees,
revenue: payload.AnnualRevenue,
billingAddress: {
street: payload.BillingStreet,
city: payload.BillingCity,
state: payload.BillingState,
postalCode: payload.BillingPostalCode,
country: payload.BillingCountry
},
shippingAddress: {
street: payload.ShippingStreet,
city: payload.ShippingCity,
state: payload.ShippingState,
postalCode: payload.ShippingPostalCode,
country: payload.ShippingCountry
},
createdAt: payload.CreatedDate,
updatedAt: payload.LastModifiedDate,
_links: {
self: "/customers/" ++ payload.Id,
contacts: "/customers/" ++ payload.Id ++ "/contacts",
orders: "/customers/" ++ payload.Id ++ "/orders"
}
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</when>
<otherwise>
<raise-error type="APP:NOT_FOUND" description="Customer not found"/>
</otherwise>
</choice>
</flow>
<!-- POST /customers -->
<flow name="post:\customers:application\json:salesforce-sapi-config">
<ee:transform doc:name="Map to Salesforce">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/java
---
[{
Name: payload.name,
Email: payload.email,
Phone: payload.phone,
Website: payload.website,
Industry: payload.industry,
Description: payload.description,
NumberOfEmployees: payload.employees,
BillingStreet: payload.billingAddress.street,
BillingCity: payload.billingAddress.city,
BillingState: payload.billingAddress.state,
BillingPostalCode: payload.billingAddress.postalCode,
BillingCountry: payload.billingAddress.country
}]]]></ee:set-payload>
</ee:message>
</ee:transform>
<salesforce:create config-ref="Salesforce_Config" type="Account" doc:name="Create Account"/>
<ee:transform doc:name="Build Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
id: payload[0].id,
success: payload[0].success,
message: if (payload[0].success) "Customer created successfully"
else payload[0].errors[0].message
}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">
#[if (payload[0].success) 201 else 400]
</ee:set-variable>
</ee:variables>
</ee:transform>
</flow>
</mule>SAP System API
<!-- sap-sapi.xml -->
<flow name="get:\products:sap-sapi-config">
<ee:transform doc:name="Build RFC Parameters">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/java
---
{
I_MATERIAL_TYPE: attributes.queryParams.type default "*",
I_PLANT: attributes.queryParams.plant default "1000",
I_MAX_ROWS: attributes.queryParams.limit default 100
}]]></ee:set-payload>
</ee:message>
</ee:transform>
<!-- Call SAP RFC/BAPI -->
<sap:execute-synchronous-remote-function-call
config-ref="SAP_Config"
functionName="BAPI_MATERIAL_GETLIST"
doc:name="Get Materials">
<sap:content>#[payload]</sap:content>
</sap:execute-synchronous-remote-function-call>
<ee:transform doc:name="Transform to Canonical">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
var materials = payload.MATNRLIST default []
var returnMessages = payload.RETURN default []
---
{
metadata: {
totalRecords: sizeOf(materials),
source: "sap",
plant: attributes.queryParams.plant default "1000"
},
products: materials map {
id: $.MATERIAL,
sku: $.MATERIAL,
name: $.MATL_DESC,
type: $.MATL_TYPE,
group: $.MATL_GROUP,
unit: $.BASE_UOM,
status: $.MATL_STATUS default "active",
plant: $.PLANT
},
(warnings: returnMessages filter ($.TYPE == "W") map $.MESSAGE)
if (sizeOf(returnMessages filter ($.TYPE == "W")) > 0)
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>
<flow name="get:\inventory\{productId}:sap-sapi-config">
<sap:execute-synchronous-remote-function-call
config-ref="SAP_Config"
functionName="BAPI_MATERIAL_STOCK_REQ_LIST"
doc:name="Get Stock">
<sap:content><![CDATA[#[{
MATERIAL: attributes.uriParams.productId,
PLANT: attributes.queryParams.plant default "1000"
}]]]></sap:content>
</sap:execute-synchronous-remote-function-call>
<ee:transform doc:name="Transform Stock Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
var stockData = payload.STOCK_REQ_LIST default []
---
{
productId: attributes.uriParams.productId,
plant: attributes.queryParams.plant default "1000",
inventory: {
available: sum(stockData.AVAIL_QTY default [0]),
reserved: sum(stockData.RES_QTY default [0]),
blocked: sum(stockData.BLOCK_QTY default [0]),
inTransit: sum(stockData.TRANSIT_QTY default [0])
},
locations: stockData map {
storageLocation: $.STGE_LOC,
available: $.AVAIL_QTY,
reserved: $.RES_QTY,
unit: $.BASE_UOM
},
lastUpdated: now()
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>Process API Implementation
Process APIs orchestrate business processes across system APIs:
<!-- order-process-api.xml -->
<flow name="post:\orders:application\json:order-papi-config">
<set-variable variableName="orderRequest" value="#[payload]" doc:name="Store Order Request"/>
<set-variable variableName="orderId" value="#[uuid()]" doc:name="Generate Order ID"/>
<!-- Step 1: Validate Customer -->
<flow-ref name="validate-customer" doc:name="Validate Customer"/>
<!-- Step 2: Check Inventory -->
<flow-ref name="check-inventory" doc:name="Check Inventory"/>
<!-- Step 3: Calculate Pricing -->
<flow-ref name="calculate-pricing" doc:name="Calculate Pricing"/>
<!-- Step 4: Process Payment -->
<flow-ref name="process-payment" doc:name="Process Payment"/>
<!-- Step 5: Create Order Records -->
<flow-ref name="create-order-records" doc:name="Create Order Records"/>
<!-- Step 6: Reserve Inventory -->
<flow-ref name="reserve-inventory" doc:name="Reserve Inventory"/>
<!-- Step 7: Initiate Fulfillment -->
<flow-ref name="initiate-fulfillment" doc:name="Initiate Fulfillment"/>
<!-- Build Response -->
<ee:transform doc:name="Build Order Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
orderId: vars.orderId,
status: "CONFIRMED",
customer: vars.customerDetails,
items: vars.pricedItems,
totals: vars.orderTotals,
payment: {
transactionId: vars.paymentResult.transactionId,
status: vars.paymentResult.status
},
fulfillment: {
estimatedDelivery: vars.fulfillmentDetails.estimatedDelivery,
trackingAvailable: vars.fulfillmentDetails.trackingAvailable
},
createdAt: now()
}]]></ee:set-payload>
</ee:message>
</ee:transform>
<!-- Error Handler with Compensation -->
<error-handler>
<on-error-propagate type="ANY">
<flow-ref name="compensate-order-failure" doc:name="Compensation"/>
</on-error-propagate>
</error-handler>
</flow>
<sub-flow name="validate-customer">
<http:request
method="GET"
config-ref="Salesforce_SAPI_Config"
path="/customers/{customerId}"
doc:name="Get Customer">
<http:uri-params><![CDATA[#[{
customerId: vars.orderRequest.customerId
}]]]></http:uri-params>
</http:request>
<choice doc:name="Check Customer Status">
<when expression="#[payload.status == 'SUSPENDED']">
<raise-error type="APP:CUSTOMER_SUSPENDED" description="Customer account is suspended"/>
</when>
</choice>
<set-variable variableName="customerDetails" value="#[payload]" doc:name="Store Customer"/>
</sub-flow>
<sub-flow name="check-inventory">
<scatter-gather doc:name="Check All Items">
<route>
<foreach collection="#[vars.orderRequest.items]" doc:name="For Each Item">
<http:request
method="GET"
config-ref="SAP_SAPI_Config"
path="/inventory/{productId}"
doc:name="Check Stock">
<http:uri-params><![CDATA[#[{
productId: payload.productId
}]]]></http:uri-params>
</http:request>
<choice doc:name="Validate Stock">
<when expression="#[payload.inventory.available < vars.rootMessage.payload.quantity]">
<raise-error type="APP:INSUFFICIENT_STOCK"
description='Insufficient stock for $(payload.productId)'/>
</when>
</choice>
</foreach>
</route>
</scatter-gather>
<set-variable variableName="inventoryChecked" value="#[true]" doc:name="Mark Inventory Checked"/>
</sub-flow>
<sub-flow name="process-payment">
<http:request
method="POST"
config-ref="Stripe_SAPI_Config"
path="/payments"
doc:name="Create Payment">
<http:body><![CDATA[#[{
amount: vars.orderTotals.grandTotal,
currency: vars.orderRequest.currency default "USD",
customerId: vars.customerDetails.stripeCustomerId,
paymentMethod: vars.orderRequest.payment.methodId,
description: "Order " ++ vars.orderId,
metadata: {
orderId: vars.orderId,
customerId: vars.orderRequest.customerId
}
}]]]></http:body>
</http:request>
<choice doc:name="Check Payment Result">
<when expression="#[payload.status != 'succeeded']">
<raise-error type="APP:PAYMENT_FAILED" description="#[payload.failureMessage]"/>
</when>
</choice>
<set-variable variableName="paymentResult" value="#[payload]" doc:name="Store Payment Result"/>
</sub-flow>
<sub-flow name="compensate-order-failure">
<!-- Reverse payment if processed -->
<choice doc:name="Check Payment to Reverse">
<when expression="#[vars.paymentResult != null]">
<http:request
method="POST"
config-ref="Stripe_SAPI_Config"
path="/payments/{paymentId}/refund"
doc:name="Refund Payment">
<http:uri-params><![CDATA[#[{
paymentId: vars.paymentResult.transactionId
}]]]></http:uri-params>
</http:request>
<logger level="INFO" message="Payment reversed: #[vars.paymentResult.transactionId]"/>
</when>
</choice>
<!-- Release inventory if reserved -->
<choice doc:name="Check Inventory to Release">
<when expression="#[vars.inventoryReserved == true]">
<foreach collection="#[vars.orderRequest.items]" doc:name="Release Each Item">
<http:request
method="DELETE"
config-ref="SAP_SAPI_Config"
path="/inventory/reservations/{reservationId}"
doc:name="Release Reservation"/>
</foreach>
<logger level="INFO" message="Inventory reservations released"/>
</when>
</choice>
<!-- Build error response -->
<ee:transform doc:name="Build Error Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
orderId: vars.orderId,
status: "FAILED",
error: {
code: error.errorType.identifier,
message: error.description,
compensationApplied: true
}
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</sub-flow>Experience API Implementation
Experience APIs tailor data for specific consumers:
<!-- mobile-experience-api.xml -->
<flow name="get:\dashboard:mobile-eapi-config">
<!-- Parallel calls to gather dashboard data -->
<scatter-gather doc:name="Gather Dashboard Data">
<!-- Recent Orders -->
<route>
<http:request
method="GET"
config-ref="Order_PAPI_Config"
path="/orders"
doc:name="Get Recent Orders">
<http:query-params><![CDATA[#[{
customerId: vars.userId,
limit: 5,
status: "RECENT"
}]]]></http:query-params>
</http:request>
<set-variable variableName="recentOrders" value="#[payload]"/>
</route>
<!-- Account Summary -->
<route>
<http:request
method="GET"
config-ref="Customer_PAPI_Config"
path="/customers/{customerId}/summary"
doc:name="Get Account Summary">
<http:uri-params><![CDATA[#[{
customerId: vars.userId
}]]]></http:uri-params>
</http:request>
<set-variable variableName="accountSummary" value="#[payload]"/>
</route>
<!-- Notifications -->
<route>
<http:request
method="GET"
config-ref="Notification_PAPI_Config"
path="/notifications"
doc:name="Get Notifications">
<http:query-params><![CDATA[#[{
userId: vars.userId,
unreadOnly: true,
limit: 10
}]]]></http:query-params>
</http:request>
<set-variable variableName="notifications" value="#[payload]"/>
</route>
<!-- Recommendations -->
<route>
<http:request
method="GET"
config-ref="Recommendation_PAPI_Config"
path="/recommendations"
doc:name="Get Recommendations">
<http:query-params><![CDATA[#[{
userId: vars.userId,
limit: 6
}]]]></http:query-params>
</http:request>
<set-variable variableName="recommendations" value="#[payload]"/>
</route>
</scatter-gather>
<!-- Build mobile-optimized response -->
<ee:transform doc:name="Build Mobile Dashboard">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
// Helper to format currency for mobile display
fun formatPrice(amount, currency) =
currency ++ " " ++ (amount as String {format: "#,##0.00"})
---
{
user: {
name: vars.accountSummary.name,
avatar: vars.accountSummary.avatarUrl,
memberSince: vars.accountSummary.memberSince as Date as String {format: "MMM yyyy"}
},
// Compact order cards for mobile
recentOrders: vars.recentOrders.orders[0 to 2] map {
id: $.orderId,
date: $.createdAt as Date as String {format: "MMM d"},
total: formatPrice($.totals.grandTotal, $.currency),
status: $.status,
statusColor: $.status match {
case "DELIVERED" -> "#4CAF50"
case "SHIPPED" -> "#2196F3"
case "PROCESSING" -> "#FF9800"
else -> "#9E9E9E"
},
itemCount: sizeOf($.items),
thumbnail: $.items[0].imageUrl
},
// Notification badges
notifications: {
unreadCount: sizeOf(vars.notifications.items),
items: vars.notifications.items[0 to 4] map {
id: $.id,
title: $.title,
preview: if (sizeOf($.message) > 50)
$.message[0 to 47] ++ "..."
else
$.message,
type: $.type,
icon: $.type match {
case "ORDER" -> "shopping_cart"
case "PROMO" -> "local_offer"
case "ALERT" -> "warning"
else -> "notifications"
},
timestamp: $.createdAt
}
},
// Product cards optimized for mobile grid
recommendations: vars.recommendations.products map {
id: $.productId,
name: if (sizeOf($.name) > 25) $.name[0 to 22] ++ "..." else $.name,
price: formatPrice($.price, "USD"),
originalPrice: if ($.onSale) formatPrice($.originalPrice, "USD") else null,
discount: if ($.onSale) round((1 - $.price / $.originalPrice) * 100) ++ "%" else null,
image: $.images[0].thumbnailUrl,
rating: $.rating,
reviewCount: $.reviewCount
},
// Quick action buttons
quickActions: [
{id: "reorder", label: "Reorder", icon: "replay"},
{id: "track", label: "Track Order", icon: "local_shipping"},
{id: "support", label: "Support", icon: "help_outline"},
{id: "deals", label: "Deals", icon: "local_offer"}
],
_meta: {
generatedAt: now(),
cacheControl: "max-age=300"
}
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>API Governance
Establish governance across API layers:
# api-governance-policy.yaml
governance:
naming_conventions:
system_api:
pattern: "{system-name}-sapi"
examples: ["salesforce-sapi", "sap-sapi", "stripe-sapi"]
process_api:
pattern: "{business-process}-papi"
examples: ["order-papi", "customer-papi", "fulfillment-papi"]
experience_api:
pattern: "{channel}-{domain}-eapi"
examples: ["mobile-commerce-eapi", "web-account-eapi", "partner-orders-eapi"]
versioning:
strategy: "URI path versioning"
format: "/v{major}"
examples:
- "/v1/customers"
- "/v2/customers"
deprecation:
notice_period_days: 90
sunset_header: true
security:
authentication:
system_api: "Client Credentials OAuth 2.0"
process_api: "Client Credentials OAuth 2.0"
experience_api: "Authorization Code OAuth 2.0 or JWT"
policies:
required:
- "Client ID enforcement"
- "Rate limiting"
- "Spike control"
recommended:
- "IP allowlist (system APIs)"
- "JWT validation (experience APIs)"
documentation:
required_elements:
- "API description and purpose"
- "Authentication requirements"
- "Request/response examples"
- "Error codes and handling"
- "Rate limits and quotas"
format: "OAS 3.0"
hosting: "Anypoint Exchange"
sla:
system_api:
availability: "99.5%"
latency_p95: "500ms"
process_api:
availability: "99.5%"
latency_p95: "2000ms"
experience_api:
availability: "99.9%"
latency_p95: "1000ms"Conclusion
API-led connectivity provides a scalable, reusable approach to enterprise integration. System APIs abstract backend complexity, Process APIs orchestrate business logic, and Experience APIs optimize for consumer needs. Strong governance ensures consistency and quality across the API landscape. This layered architecture enables agility while maintaining enterprise standards.