Both exist, both work — and yet the same question comes up in almost every Power Pages project at some point: Liquid FetchXML or Web API? Answering with "it depends" is technically correct, but helpful to no one. This article gives you a concrete decision tree, explains the technical differences, and shows when the hybrid approach is the cleanest solution.
Liquid FetchXML: Server-Side Rendering
Liquid FetchXML is the older of the two approaches — and often the better one. The mechanism is straightforward: when the page renders, Power Pages executes the FetchXML query server-side and writes the result directly into the HTML output. By the time the browser receives the page, the data is already in the DOM.
{% fetchxml accounts_query %}
<fetch top="50">
<entity name="account">
<attribute name="name" />
<attribute name="accountid" />
<filter>
<condition attribute="statecode" operator="eq" value="0" />
</filter>
<link-entity name="contact" from="accountid"
to="accountid" link-type="inner">
<filter>
<condition attribute="contactid" operator="eq"
value="{{ user.id }}" />
</filter>
</link-entity>
</entity>
</fetch>
{% endfetchxml %}
{% for account in accounts_query.results.entities %}
<div>{{ account.name }}</div>
{% endfor %}
Advantages
No CORS, no CSRF
Since everything happens server-side, there are no cross-origin issues and no anti-forgery token is required. The request runs from the portal server itself, not from the browser.
N:M Relationships Work Cleanly
Both approaches use the same Table Permissions model. The difference: FetchXML traverses N:M relationships directly in the query via link-entity — the permission scope doesn't interfere. The Web API translates scope conditions into OData filters, which causes CDS errors for N:M traversal with Parental, Contact, or Account scope (workaround: FetchXML parameter in OData query).
Faster Initial Load
Data arrives with the first HTML response. No extra requests, no flickering, no spinners. Time to Interactive is measurably lower — especially with larger datasets.
SEO-Friendly
Search engine crawlers see the data directly in the HTML. Content loaded via JavaScript can be ignored by crawlers — especially critical for product pages or public directories.
Disadvantages
No Dynamic Loading
What isn't there at page load stays absent — without a full page reload.
Read-Only — No Create, Update, Delete
With Liquid you can read data (including across complex relationships), but not write, update, or delete. For mutations you always need the Web API.
Template Complexity
Nested relationships, conditional logic, and transformations in Liquid quickly become hard to maintain.
Web API: Client-Side Data Access
The Power Pages Web API is an OData-compatible REST endpoint at /_api/. Requests run from the browser — after page load, triggered by user interactions or timers. That makes it the right choice for everything dynamic.
webapi.safeAjax({
type: "GET",
url: "/_api/accounts?$select=name,accountid&$filter=statecode eq 0",
contentType: "application/json"
}).done(function(data) {
data.value.forEach(function(account) {
console.log(account.name);
});
}).fail(function(jqXHR) {
console.error("Error:", jqXHR.status);
});
Important: webapi.safeAjax() is Power Pages' own wrapper function. It automatically injects the anti-forgery token (__RequestVerificationToken), which is mandatory for all write operations (POST, PATCH, DELETE). Direct $.ajax() calls without this token will fail with a 403.
Advantages
Full CRUD Support
GET, POST, PATCH, DELETE — all operations are available. Plus Associate and Disassociate for N:M relationships via the $ref endpoint.
Dynamic Without Reload
Content updates in place, filters apply instantly, forms save without page navigation. Modern UX patterns like infinite scroll or live updates are only possible this way.
Familiar Technology
OData is an established standard. Developers familiar with Dataverse know the query syntax. Debugging in browser DevTools is easier than tracking down Liquid issues.
Selective Data Loading
You can load only the data that's actually needed at any given moment — rather than rendering everything at page load.
Disadvantages
Table Permissions + Known Issue with N:M
Table Permissions and the site setting (Webapi/<entity>/enabled = true) are mandatory. For N:M read access with Parental, Contact, or Account scope there is a known CDS error issue — workaround: FetchXML parameter in OData query or disableodatafilter = true.
N+1 Request Problem
Every API call is a separate HTTP request. When many data points are needed at page load, this adds up to noticeable latency — and users see spinners instead of content.
The Decision Tree
In practice, four questions are enough to choose the right approach. Work through them in order:
Special case — N:M reads: According to the official Microsoft documentation, the Web API has a known issue: GET requests on tables with N:M table permissions fail when the scope is Parental, Contact, or Account — the portal server throws a CDS error. Microsoft's recommended solution is to use FetchXML as a parameter in the OData query (/_api/entity?fetchXml=...) instead of OData filtering. An alternative is the site setting Webapi/<table>/disableodatafilter = true, which comes with a performance penalty. Liquid FetchXML avoids this problem entirely — no workaround needed.
The Hybrid Pattern: Best of Both Worlds
In practice, most sophisticated Power Pages pages are hybrids. The pattern is simple and proven: Liquid loads data at page load into a JavaScript variable — the Web API handles mutations only.
A concrete example: a page displays all products associated with an incident (N:M). The user can add and remove products. Without the hybrid approach, every click would trigger a full reload, or you'd fight with Global permissions. With the hybrid pattern it's clean:
{% fetchxml products_query %}
<fetch>
<entity name="product">
<attribute name="name" />
<attribute name="productid" />
<attribute name="productnumber" />
<filter>
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</entity>
</fetch>
{% endfetchxml %}
<script>
// All available products – one request, no spinner
var AVAILABLE_PRODUCTS = [
{% for p in products_query.results.entities %}
{ id: "{{ p.productid }}", name: "{{ p.name | escape }}", number: "{{ p.productnumber | escape }}" }{% unless forloop.last %},{% endunless %}
{% endfor %}
];
</script>
// Associate product with an Incident (N:M Associate)
function associateProduct(incidentId, productId) {
return webapi.safeAjax({
type: "POST",
url: "/_api/incidents(" + incidentId + ")/product_incidents/$ref",
contentType: "application/json",
data: JSON.stringify({
"@odata.id": "/_api/products(" + productId + ")"
})
});
}
// Remove association (N:M Disassociate)
function disassociateProduct(incidentId, productId) {
return webapi.safeAjax({
type: "DELETE",
url: "/_api/incidents(" + incidentId + ")/product_incidents(" + productId + ")/$ref"
});
}
// JavaScript filters preloaded data — no extra request
function renderFilteredProducts(searchTerm) {
return AVAILABLE_PRODUCTS.filter(function(p) {
return p.name.toLowerCase().includes(searchTerm.toLowerCase());
});
}
The result: the initial page load is fast (a single server request via Liquid), client-side filtering and search require no network calls, and only the actual changes (Associate/Disassociate) go through the Web API. Table Permissions only need to be configured for the mutation — not for reading the N:M data.
Performance Comparison
The performance implications are more direct than most developers expect. The key metric is Time to Interactive — when can the user actually work with the page?
| Scenario | Liquid FetchXML | Web API |
|---|---|---|
| 100 records at page load | 1 request (HTML) | 2 requests (HTML + API) |
| Data visible from | Immediately (in HTML) | After API response |
| Client-side filtering / search | Instant (no network) | Extra request per search |
| Update data after user action | Page reload required | No reload needed |
| Large dataset (1,000+ rows) | Slower page load | Pagination possible |
Rule of thumb: Up to around 200 records, Liquid has a clear advantage for the initial load. Beyond that, or when pagination is required, the Web API is worth considering — ideally combined with a Liquid preload of the first page.
Security Comparison
Both approaches — Liquid FetchXML and Web API — use the same Table Permissions model. There is no separate permission system per method. The difference lies in how the permission check is technically implemented and what consequences that has for certain scenarios.
Liquid FetchXML
Table Permissions — server-side query filtering
- + Permission check: is the user allowed to read the target table?
- + Relationship filtering is handled by FetchXML itself — scope boundaries do not interfere with N:M traversal
- + All scopes (Global, Account, Contact, Self, Parental) work without workarounds
- ~ Configured in Power Pages Studio under "Security"
Web API
Table Permissions — translated as OData filters
- + Automatic CSRF token protection via safeAjax
- + Simple to configure for standard CRUD
- - Scope conditions are injected as OData filters — N:M with Parental/Contact/Account scope causes a known CDS error
- - Site settings required per table (
Webapi/entity/enabled)
Security warning: Global Table Permissions are often the "quick fix" for the N:M problem with the Web API — and a serious security risk. They grant authenticated users read access to all records in the table, regardless of relationships. Microsoft's recommended approach is to use FetchXML as a parameter in the OData query, or Liquid FetchXML as the simplest option requiring no workaround at all. More on the security layers in the Power Pages security architecture article.
Quick Reference: When to Use What
| Use Case | Recommendation | Reason |
|---|---|---|
| Initial data load | Liquid | 1 request, immediately in DOM, SEO-compatible |
| Read N:M relationship | Liquid recommended | Web API possible, but known issue with narrow scopes — FetchXML parameter required as workaround |
| Write N:M relationship | Web API | $ref endpoint for Associate / Disassociate |
| CRUD after user interaction | Web API | No page reload, modern UX pattern |
| Populate dropdown / lookup | Liquid | Reference data rarely changes, preload is more efficient |
| Infinite scroll / pagination | Web API | $top / $skip for page-by-page loading |
| SEO-relevant data | Liquid | Crawlers see server-rendered HTML content |
| Live updates (polling) | Web API | Interval-based API calls without reload |
| Client-side filtering / search | Hybrid | Liquid preloads all data, JS filters without network |
The cheat sheets for Power Pages Web API and Liquid Templates are available in the downloads section.
Power Pages Architecture Questions?
Liquid vs. Web API is one of many architectural decisions that determine success or frustration in Power Pages projects. In a free consultation, I'll look at your specific situation.
Book Free Consultation