EDS Blocks and External API Integration
Integrating external APIs with AEM Edge Delivery Services blocks, including direct browser calls, CORS, credentials, AEM Edge Functions, API contracts, loading and failure states, caching, timeouts, and production architecture.
Content Objective
- Separate authored content from runtime business data, and decide who owns each piece.
- Know when a block can call an external API directly from the browser and when it cannot.
- Understand the browser security boundary: public JavaScript, credentials, CORS, and why environment variables are not automatically secret.
- Use a trusted server-side layer (an AEM Edge Function, or the serverless proxy pattern from our POC) to hold secrets, transform responses, and aggregate APIs.
- Handle the four states a dynamic block moves through — loading, success, empty, and error — plus timeouts, caching, and local-vs-deployed routing.
- Debug API-backed blocks from the Network panel outward, and see what actually broke when we wired the ADC form block to a real enterprise API on the POC.
Introduction
Everything we have built so far can work primarily from authored content.
That changes when a block needs data that belongs to another system.
Consider a Product Availability block.
The author may control:
- heading
- supporting text
- CTA label
But availability belongs to another system.
The block may need information such as:
- product identifier
- availability
- delivery estimate
- market-specific data
That data should not be copied into AEM just to make the block work.
Now the page has two data sources:
Authored content
and
Runtime application data
This introduces several new questions:
- Should the browser call the external API directly?
- Does the API require credentials?
- Is CORS configured?
- Should an AEM Edge Function sit between the browser and the backend?
- What happens when the API is slow?
- What happens when it fails?
- Can the response be cached?
- Who owns the API contract?
This is where a content block starts participating in application architecture.
1. Start With Data Ownership
Before writing:
fetch(...)
I first decide who owns the data.
For a Product Availability block, a simple ownership split could be:
| Data | Owner |
|---|---|
| Heading | Author |
| Description | Author |
| Product ID | Content/configuration |
| CTA label | Author |
| Availability | Backend system |
| Delivery estimate | Backend system |
| Inventory | Backend system |
This matters because I do not want authors maintaining values that already have a system of record.
AEM should not become a second inventory database just because the frontend needs inventory information.
2. Static Content and Runtime Data Have Different Lifecycles
The authored part may change once a week.
Availability may change every few minutes.
Trying to manage both through the same content lifecycle creates unnecessary coupling.
Instead, the page can deliver stable content immediately.
The block then retrieves runtime information when needed.
Conceptually:
Delivered page
contains the authored experience.
Runtime API
provides the current business state.
The block combines them in the browser.
3. The Simplest Option: Browser → API
The simplest integration is:
const response = await fetch('https://api.example.com/products/123');
const data = await response.json();
There is nothing inherently wrong with a browser calling an API directly.
For some APIs, that is exactly the right design.
But before doing it, I ask:
- Is the API intended for browser access?
- Does it allow requests from our site?
- Does it require a private credential?
- Is its response safe to expose directly?
- Does the browser need to call several backend APIs?
- Is the backend contract suitable for frontend use?
Those answers determine whether direct browser integration is appropriate.
4. When Direct Browser Calls Work Well
A direct browser request can be reasonable when the API is:
- public or intentionally browser-accessible
- protected using an authentication model suitable for the browser
- configured for the required origin
- already shaped for frontend consumption
- not exposing internal-only fields
- stable enough for the frontend to depend on directly
In that case, adding another server-side layer may not provide much value.
Architecture should solve an actual problem.
5. Browser JavaScript Is Public
This is the boundary that cannot be ignored.
Anything shipped in:
blocks/product-availability/product-availability.js
can be inspected by the user.
That means this is not a secret:
const API_KEY = 'my-private-api-key';
Neither is a value hidden in another frontend JavaScript file.
Neither is a build-time variable if its value ends up in the browser bundle or generated JavaScript.
If the browser needs the value to make the request, the user can ultimately inspect it.
Private credentials belong on a trusted server-side boundary.
6. Environment Variables Do Not Automatically Create Secrets
A common mistake is assuming:
It came from an environment variable, so it is secure.
That depends entirely on where the value is used.
An environment variable used inside server-side code can remain private.
An environment variable substituted into browser JavaScript becomes browser-visible.
The security question is not:
Where did this value originate?
It is:
Where does this value execute?
If it executes in the browser, treat it as public.
7. CORS Is a Browser Policy
Suppose the site runs on:
https://www.example.com
and the API runs on:
https://api.example.net
The browser is making a cross-origin request.
The API needs to allow the appropriate origin for browser access.
Otherwise the browser may block access to the response.
That is where CORS enters the picture.
A CORS error does not automatically mean the API itself is down.
The backend may have returned a response, but the browser refuses to expose it to the frontend because the cross-origin policy does not permit it.
8. CORS Is Not Authentication
Another distinction matters here.
CORS answers a browser question:
Is JavaScript from this origin allowed to access this response?
It does not answer:
Is this caller authorized to perform this business operation?
Those are different concerns.
An API still needs appropriate:
- authentication
- authorization
- validation
- abuse protection
where required.
I would never treat:
Access-Control-Allow-Origin
as an authorization mechanism.
9. When an Intermediate Server-Side Layer Is Needed
Now consider an API that requires a private token.
The browser cannot safely hold that token.
We need a trusted layer between the block and the backend.
Current AEM provides AEM Edge Functions, which can run JavaScript at the CDN layer and can act as middleware, aggregate or transform third-party responses, and keep server-side credentials out of browser code.
The architecture becomes:
EDS Block → AEM Edge Function → Backend API
The browser calls an endpoint it is allowed to call.
The Edge Function performs the trusted backend interaction.
10. What an AEM Edge Function Solves
An AEM Edge Function can be useful when we need to:
- keep credentials server-side
- transform a backend response
- combine multiple backend APIs
- hide unnecessary backend fields
- provide a frontend-specific API contract
- run lightweight server-side logic close to the delivery layer
Adobe's current EDS tutorial uses this exact pattern for a dynamic block: the block calls an AEM Edge Function, the function calls upstream APIs, and only the required response is returned to the browser.
That makes it a useful option for EDS dynamic blocks.
11. What an Edge Function Should Not Become
I would not automatically move every backend responsibility into an Edge Function.
For example, an existing enterprise platform may already provide:
- API gateway
- OAuth handling
- rate limiting
- auditing
- backend orchestration
- complex authorization
- transactional logic
- centralized monitoring
Rebuilding all of that at the edge would create another application platform to maintain.
An Edge Function is useful for lightweight edge-side logic.
It is not a reason to duplicate mature backend architecture.
12. A Product Availability Example
For this chapter, imagine the page contains:
Product Availability
Check availability for this product.
Product ID: ABC-123
The author controls the explanatory content.
The runtime system owns availability.
The block needs to request something conceptually like:
/api/product-availability?productId=ABC-123
The response might be:
{
"productId": "ABC-123",
"status": "available",
"message": "Available"
}
I intentionally keep the frontend contract small.
The browser should receive what the UI needs, not an entire backend domain object.
13. Keep the API Contract Small
Suppose the backend response contains 80 fields.
The block only needs:
{
"status": "available",
"message": "Available"
}
Returning all 80 fields creates unnecessary coupling.
The frontend may eventually start depending on internal fields simply because they are available.
A transformation layer can give the block a stable frontend-oriented contract.
That also gives the backend more freedom to evolve independently.
14. A Simple Block Request
A basic block might start with:
async function fetchAvailability(productId) {
const response = await fetch(
`/api/product-availability?productId=${encodeURIComponent(productId)}`,
);
if (!response.ok) {
throw new Error(`Availability request failed: ${response.status}`);
}
return response.json();
}
Then:
export default async function decorate(block) {
const productId = block.dataset.productId;
if (!productId) {
return;
}
const data = await fetchAvailability(productId);
// update the block
}
The real implementation will depend on how the Product ID is represented in the delivered block.
As always, inspect the actual DOM/content contract first.
15. Validate Before Making the Request
Do not make a network request with data you already know is invalid.
For example:
if (!productId) {
block.classList.add('is-invalid');
return;
}
If the user provides input:
if (!postcode.trim()) {
showMessage('Postcode required');
return;
}
Client-side validation does not replace server-side validation.
It simply avoids unnecessary requests and gives faster feedback.
The server-side endpoint must still validate its own inputs.
16. Dynamic Blocks Need Explicit States
A static Hero may have one main rendered state.
An API-backed block has more.
I normally think in at least four:
Loading
The request is in progress.
Success
Valid data was returned.
Empty
The request succeeded but there is no relevant result.
Error
The request could not produce a usable result.
These are different user experiences.
Do not collapse all non-success cases into:
Something went wrong
unless that is genuinely the only distinction the user needs.
17. Loading State
Before starting the request:
block.classList.add('is-loading');
The UI may show:
Checking availability…
When the request completes:
block.classList.remove('is-loading');
A loading state is particularly useful when the request can take noticeable time.
But it should not hide stable authored content unnecessarily.
The heading and description can remain visible while runtime data loads.
18. Success State
On success:
function renderAvailability(container, data) {
const status = document.createElement('p');
status.classList.add('availability-status');
status.textContent = data.message;
container.replaceChildren(status);
}
Using:
textContent
for API values avoids treating external data as trusted HTML.
The backend contract should return data.
The frontend decides how that data becomes UI.
19. Empty State
An API can succeed technically but have no result.
For example:
{
"status": "unknown",
"message": null
}
That is not necessarily a server error.
The UI may show:
Availability information is not currently available.
This is different from a failed request.
Modeling the empty state explicitly makes the block behavior clearer.
20. Error State
A basic request wrapper might look like:
try {
const data = await fetchAvailability(productId);
renderAvailability(result, data);
} catch (error) {
console.error('Availability request failed', error);
renderError(result);
}
The browser console can contain technical debugging information.
The user-facing message should be appropriate for the experience.
I would not expose:
HTTP 502 from upstream inventory-service-v2
to the user unless that information is genuinely useful to them.
21. Timeouts Matter
A request that never completes is also a failure mode.
Modern browser code can use AbortController.
For example:
async function fetchAvailability(productId) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(
`/api/product-availability?productId=${encodeURIComponent(productId)}`,
{ signal: controller.signal },
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} finally {
clearTimeout(timeout);
}
}
The exact timeout should come from the application's requirements.
The point is that dynamic blocks need a defined behavior for slow dependencies.
22. Separate API Logic From DOM Logic
As a block grows, I prefer not to mix everything into one function.
Instead of:
export default async function decorate(block) {
// find DOM
// build URL
// fetch
// parse response
// transform API data
// render loading
// render success
// render error
}
I separate responsibilities.
For example:
async function fetchAvailability(productId) {
// request logic
}
function renderLoading(container) {
// loading UI
}
function renderAvailability(container, data) {
// success UI
}
function renderError(container) {
// failure UI
}
export default async function decorate(block) {
// orchestration
}
This makes both debugging and testing easier.
23. Move Shared API Logic Only When It Is Actually Shared
If several blocks call the same API, a shared API module may make sense.
For example:
scripts/
└── api/
└── product-api.js
Then:
import { getAvailability } from '../../scripts/api/product-api.js';
But I would not create a generic enterprise API framework after the first fetch().
Start local.
Extract when the repetition and ownership are clear.
24. Local Development Changes the Request Origin
This is an important practical detail for AEM Edge Functions.
Current Adobe guidance uses separate local servers:
- EDS site through
aem up - AEM Edge Function through its local development server
Adobe's current tutorial uses localhost:3000 for the EDS site and 127.0.0.1:7676 for the local Edge Function. Because those are different origins, local calls require the Edge Function to return appropriate CORS headers.
That means a request can work after deployment and still fail locally because the local architecture crosses origins.
25. Deployed Routing Can Be Same-Origin
The deployed architecture is different.
Adobe's current EDS Edge Function guidance uses a relative endpoint from the block, for example:
/api/product-availability
with CDN routing sending that request to the deployed Edge Function.
The browser sees the request on the site's own domain.
Adobe documents this same-origin pattern for deployed EDS Edge Function integrations.
This removes the cross-origin condition that exists between the two local development servers.
26. Keep Environment Logic Small
I do not want environment checks scattered throughout the block.
A small resolver is easier to manage:
const API_PATH = '/api/product-availability';
const LOCAL_EDGE_ORIGIN = 'http://127.0.0.1:7676';
function getApiUrl() {
const { hostname } = window.location;
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
return isLocal
? `${LOCAL_EDGE_ORIGIN}${API_PATH}`
: API_PATH;
}
Then the rest of the block uses:
fetch(getApiUrl());
The environment difference stays at one boundary.
27. Secrets Belong in the Trusted Layer
If the upstream API requires a token, the Edge Function can retrieve the secret server-side.
Adobe's current Edge Functions guidance uses SecretStoreManager.getSecret(...) and explicitly recommends keeping secrets inside the Edge Function rather than returning or logging them.
Conceptually:
const token = await SecretStoreManager.getSecret('API_TOKEN');
Then the server-side request can use it:
const response = await fetch(upstreamUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
});
The token never needs to become part of the block JavaScript.
28. Do Not Return Secrets to the Browser
This sounds obvious, but it is worth stating.
The Edge Function should not solve:
How do I hide the token from the frontend?
and then respond with:
{
"token": "..."
}
The function should use the credential internally.
The browser should receive only the application data it is authorized to see.
The trusted boundary exists to keep trusted operations out of the browser.
29. Edge Function as a Response Adapter
Suppose the upstream response is:
{
"sku": "ABC-123",
"inventoryStatusCode": "I01",
"warehouse": {
"id": "W001",
"region": "..."
},
"internalMetadata": {
"...": "..."
}
}
The block may only need:
{
"status": "available"
}
The Edge Function can translate the backend contract into the frontend contract.
That is often more valuable than simply proxying the entire upstream response unchanged.
30. Aggregating Multiple APIs
A dynamic block may need data from more than one service.
For example:
Catalog API
provides product details.
Inventory API
provides availability.
The browser could call both.
But that creates:
- multiple network requests
- multiple failure paths
- backend contract knowledge in the frontend
- possibly multiple authentication mechanisms
An Edge Function can call both and return:
{
"product": {
"id": "ABC-123",
"name": "Product A"
},
"availability": {
"status": "available"
}
}
Adobe lists response aggregation and transformation among current AEM Edge Function use cases.
31. Validate Inputs Again at the Server Boundary
Even if the block validates:
if (!productId) return;
the Edge Function must validate independently.
The endpoint can be called without the block.
Someone can directly request:
/api/product-availability?productId=...
Server-side validation is therefore required for server-side trust decisions.
Client-side validation is UX.
Server-side validation is part of the security boundary.
32. Caching Needs a Data Decision
Caching dynamic API responses can improve:
- response time
- backend load
- resilience
But the correct strategy depends on the data.
For example:
Product description
may tolerate longer caching.
Availability
may require shorter caching.
User-specific account information
may not be appropriate for shared caching at all.
Do not add caching only because EDS is heavily optimized around delivery.
Dynamic data has its own freshness and privacy requirements.
33. Personalized Data Needs Extra Care
Suppose an API response depends on:
- authenticated user
- location
- account
- contract
- customer segment
A shared cache could return one user's data to another user if the cache key and response policy are wrong.
That is a serious architecture issue.
Before caching personalized responses, understand:
- identity
- cache key
- headers
- CDN behavior
- response privacy
- authorization
When uncertain, do not cache sensitive user-specific data in a shared layer.
34. Performance: Avoid Blocking the Whole Page
An API-backed block should not make unrelated content wait unnecessarily.
The page can render:
- navigation
- Hero
- static content
- footer
while Product Availability retrieves its runtime state.
This keeps the dynamic dependency local.
If the inventory system is slow, the entire page should not become slow because one block needs inventory.
That isolation is one of the benefits of keeping the dynamic behavior inside the block boundary.
35. Load Runtime Data Only When Needed
Not every dynamic block needs an immediate request.
Suppose Product Availability appears far below the fold.
Depending on the requirement, we may decide to load it when it approaches the viewport or after more critical content has loaded.
But I do not add lazy behavior automatically.
I first ask:
- Is the data important immediately?
- Is it above the fold?
- How expensive is the request?
- How often do users reach the block?
- Does delaying it create visible waiting later?
Performance strategy should follow the user experience.
36. Error Handling Should Preserve the Page
If the API fails, the block may show:
Availability is temporarily unavailable.
The rest of the page should continue normally.
I would avoid throwing an uncaught exception that interrupts other initialization.
The API is a dependency of the block.
It should not become a dependency of the entire page unless the application truly requires that.
37. Debug With the Network Panel
For an API-backed block, the Network panel becomes one of the main debugging tools.
I inspect:
- Was the request made?
- What URL was used?
- Which HTTP method?
- Which query parameters?
- What status code?
- What response body?
- How long did it take?
- Was there a CORS error?
- Was the request aborted?
- Was the response cached?
That often tells me more than starting with the block JavaScript.
38. No Network Request Means Start Earlier
Suppose the block shows no availability and Network contains no API request.
The backend is probably not the first place to investigate.
Check:
- did
decorate(block)run? - did the event listener attach?
- did validation return early?
- is the Product ID present?
- did an earlier JavaScript error stop execution?
Again, find the first stage where expected behavior disappears.
39. Request Exists but URL Is Wrong
Suppose Network shows:
/api/product-availability?productId=undefined
The API is not the first problem.
Trace productId backward.
Where should it come from?
- authored content?
- data attribute?
- URL?
- another DOM element?
Inspect that source.
A wrong request usually means something was wrong before fetch().
40. Understanding Common Status Codes
A few HTTP statuses are useful during debugging.
400
The request may be invalid.
Check parameters and validation.
401
Authentication is missing or invalid.
403
The caller is understood but not allowed, or another policy layer is rejecting the request.
404
Check the route and environment.
500-range
The server-side layer or an upstream dependency failed.
These are starting points.
The actual response and server-side logs determine the real cause.
41. Local Works, Deployed Fails
This is an important category.
If the API integration works locally but fails after deployment, compare:
- request URL
- CDN route
- Edge Function deployment
- environment configuration
- secret availability
- site binding
- backend accessibility
Adobe's current deployment guidance specifically recommends checking the CDN origin selector and Edge Function deployment when a deployed request fails after working locally.
Do not assume the block code is wrong simply because production behaves differently.
42. Deployed Works, Local Fails
The reverse can happen too.
The deployed request may be same-origin through CDN routing.
Local development uses:
localhost:3000
and:
127.0.0.1:7676
which are different origins.
If the browser reports a CORS failure locally, check the Edge Function's local CORS response before changing the production architecture. Adobe's current tutorial explicitly documents this local-vs-deployed difference.
43. Keep Logs Useful and Safe
Server-side logging can help answer:
- Was the function invoked?
- Which route?
- Which upstream dependency failed?
- How long did the call take?
- Which response category occurred?
But logs should not contain:
- passwords
- access tokens
- private API keys
- unnecessary personal information
A debugging system should not create a new security problem.
44. API Integration Architecture
There are two integration paths worth holding in your head as one picture.

Direct API path — the EDS block calls a public, browser-safe API directly. No private credentials are involved.
Trusted integration path — the EDS block calls a relative endpoint (/api/product-availability), which is routed to an AEM Edge Function. Inside that trusted boundary the function holds the secret and performs validation, transformation, and aggregation before calling the external API.
The two environments route that trusted path differently:
- Local — the browser calls the local Edge Function origin (a different origin from the local EDS site), so CORS applies.
- Deployed — the browser calls a same-origin
/api/...path that CDN routing forwards to the Edge Function, so there is no cross-origin condition.
45. When an Enterprise API Gateway May Be Better
An organization may already have an API management layer.
For example, the enterprise architecture may require:
- centralized authentication
- quotas
- rate limiting
- API analytics
- threat protection
- lifecycle governance
- reusable backend APIs
In that architecture, the EDS block may call:
EDS Block → Enterprise API Layer
or:
EDS Block → Edge Function → Enterprise API Layer
depending on what transformation or edge behavior is needed.
I would not introduce an Edge Function merely to bypass established API governance.
The correct boundary depends on the wider platform architecture.
46. Avoid Backend Logic in the Block
I would not put logic such as this in browser code:
if (inventoryCode === 'A1' && warehouseType === 'PRIMARY') {
// available
}
if those codes are internal backend rules.
Prefer the API contract to return something meaningful:
{
"status": "available"
}
The browser should implement presentation logic.
It should not become the owner of backend business rules.
47. API Versioning and Frontend Compatibility
Suppose the API currently returns:
{
"status": "available"
}
and later changes to:
{
"inventoryStatus": {
"code": "AVAILABLE"
}
}
That can break the block.
A frontend-oriented integration layer can shield the block from some backend changes by preserving the frontend contract.
This is another reason transformation can be valuable.
It reduces direct coupling between browser code and backend implementation details.
48. Product Availability Block Contract
By this point, our dynamic block has two contracts.
Content contract
Heading
Description
Product ID
CTA
Runtime contract
{
"status": "available",
"message": "Available"
}
The block combines them.
This separation makes ownership clear.
Authors manage the experience.
The backend manages the business state.
The block manages presentation.
49. Developer Perspective
The biggest change with API-backed blocks is that debugging is no longer limited to the DOM.
I now need to understand:
- authored content
- block execution
- request construction
- browser security
- API responses
- server-side integration
- runtime states
That sounds like more complexity, and it is.
The way to keep it manageable is to preserve boundaries.
Don't mix content, network, transformation, and rendering logic unnecessarily.
50. AEM Developer Perspective
For an AEM developer, it can be tempting to solve every integration through AEM backend code because that is familiar.
EDS gives us another architecture option.
Some integrations can happen directly from the browser.
Some need AEM Edge Functions.
Some should continue to use established enterprise APIs and gateways.
The important question is not:
How do I make EDS call this API?
It is:
Where should this integration responsibility live?
That is an architecture decision.
51. Architect Perspective
For each dynamic block, I would review five boundaries.
Data ownership
Who owns the data?
Trust
Can the browser access it directly?
Contract
What is the smallest stable response the frontend needs?
Failure
What happens when the dependency is unavailable?
Performance
When should the request happen, and what can safely be cached?
If those five questions have clear answers, the implementation usually becomes much simpler.
52. What I Learned From Dynamic Blocks
A dynamic EDS block is still a block.
It starts with delivered content.
It still has a DOM boundary.
It still uses block-scoped JavaScript and CSS.
But runtime data introduces another system into the lifecycle.
That means the frontend should not casually absorb responsibilities that belong elsewhere.
Private credentials stay server-side.
Business rules stay with the appropriate backend.
The block owns presentation and interaction.
A trusted integration layer can adapt backend data when necessary.
Once those boundaries are clear, API integration becomes much easier to reason about.
53. What Actually Happened When We Wired the ADC Form to a Real API on the POC
The block we actually built on the POC was not Product Availability — it was the ADC form (covered in full in the next chapter). But wiring it to a real enterprise API forced every boundary in this chapter into the open. A few things only became obvious once we ran it.
The secret could not live anywhere in the block. EDS is static hosting, so the browser can never hold the enterprise API key — exactly the "browser JavaScript is public" boundary from section 5. In AEM this is what FormSubmitServlet and APILookupService plus OSGi config solve on the server. In EDS we rebuilt that as a small serverless proxy — the EDS equivalent of the servlet. The block POSTs to the proxy; the proxy attaches X-Origin-Secret from an environment variable server-side and forwards to the enterprise API. We verified the browser request carried no secret and the proxy log showed secret= present.
The proxy is the trusted layer from section 9 — nothing more. It resolves a logical formType key to a real URL (mirroring APILookupService), adds the secret and domain, forwards, and returns JSON. It does not reimplement the enterprise API gateway, and per section 11 we deliberately kept it that way.
Dynamic dropdowns are just another runtime contract. A select whose options are lookup:<key> triggers a runtime fetch to the proxy's lookup endpoint — the same "runtime data has a different lifecycle than authored content" idea from section 2, applied to form field options.
Local development crossed origins exactly as section 24 warns. Universal Editor runs over HTTPS, so a plain http://localhost proxy is blocked as mixed content. We ran the proxy locally and exposed it over HTTPS with a cloudflared tunnel, then pointed the block's proxy-endpoint field at the tunnel URL. That is the local-vs-deployed origin difference made concrete.
Environment variable is not the same as a secret — verified. The key was safe only because it was read inside the proxy (server side), never substituted into the block JavaScript. That is section 6 in practice.
The full form-specific implementation — the AEM Form Container, its submit servlet and OSGi-held secret, and the serverless proxy rebuilt as an EDS block — is the subject of the next chapter.
Key Takeaways
- Start API integration by defining data ownership.
- Keep authored content separate from runtime business data.
- Direct browser API calls are appropriate when the API is intentionally browser-safe.
- Browser JavaScript cannot safely contain private credentials.
- Environment variables are not secret if their values are shipped to the browser.
- CORS is a browser cross-origin policy, not an authorization mechanism.
- Use a trusted server-side boundary when private credentials or backend-only operations are required.
- AEM Edge Functions can provide server-side JavaScript at the CDN layer for EDS integrations.
- Edge Functions can hold secrets, transform responses, and aggregate upstream APIs.
- Do not turn Edge Functions into a replacement for mature enterprise backend architecture without a reason.
- Keep frontend API contracts small and stable.
- Validate input before sending requests, but validate again at the server boundary.
- Dynamic blocks should explicitly handle loading, success, empty, and error states.
- Define timeout behavior for slow dependencies.
- Separate network logic from DOM rendering as complexity grows.
- Extract shared API modules only when the responsibility is genuinely shared.
- Local EDS and local Edge Function servers can be cross-origin and require CORS handling.
- Deployed EDS Edge Function calls can use relative same-origin paths routed through the CDN.
- Keep environment-specific URL logic at one boundary.
- Secrets should stay inside the trusted Edge Function and should not be returned or logged.
- Use an integration layer to remove unnecessary backend fields and translate internal contracts.
- Be careful caching personalized or sensitive responses.
- A failing dynamic block should not break unrelated page content.
- Use the Network panel to debug requests before guessing at backend problems.
- If no request exists, debug the block before the API.
- If the request is malformed, trace its inputs backward.
- Compare routing, secrets, deployment, and environment configuration when local and deployed behavior differ.
- Keep internal backend business rules out of browser code.
- A dynamic block has both a content contract and a runtime data contract.
- The right integration boundary is an architecture decision, not simply a frontend implementation choice.
- On our POC the trusted layer was a small serverless proxy — the EDS equivalent of AEM's
FormSubmitServletplusAPILookupService— and the secret stayed in an environment variable read only server-side. - Universal Editor runs over HTTPS, so a local proxy must be exposed over HTTPS (we used a
cloudflaredtunnel) or the browser blocks it as mixed content.
Next Steps
This chapter covered the general shape of external API integration — data ownership, the trust boundary, Edge Functions, the states a dynamic block moves through, and debugging from the Network panel.
The next chapter, Forms and API Integration in EDS, applies all of it to the concrete case we ported on the POC: the AEM Form Container, its submit servlet and OSGi-held secret, rebuilt as an EDS block plus a serverless proxy — with config via spreadsheets, per-country headers, client-side validation, and reCAPTCHA.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.