Every Fiori or CAP app running on SAP BTP has a small JSON file sitting at its root that most developers copy from a template and never really read. That file is xs-app.json, and it controls something fundamental: how the approuter decides where each incoming request goes, who is allowed in, and which backend system answers.
Once you understand it, a whole class of "my app works locally but not on BTP" problems starts making sense.
What the approuter does
The approuter is a Node.js reverse proxy that sits in front of your app. Every request from the browser hits the approuter first. It handles authentication (redirecting to XSUAA when needed), then forwards the request — either to your app's own static resources, or to a backend destination like a CAP service or an S/4HANA system.
xs-app.json is the approuter's rulebook. Without it, the approuter does not know your routes exist.
The anatomy of a route
Here is a typical xs-app.json for a CAP-backed Fiori app, with annotations:
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/do/logout"
},
"routes": [
{
"source": "^/myapp/(.*)$",
"target": "/$1",
"localDir": "app/myapp/webapp",
"authenticationType": "xsuaa"
},
{
"source": "^/odata/v4/catalog/(.*)$",
"destination": "srv-api",
"authenticationType": "xsuaa"
},
{
"source": "^/api/s4/(.*)$",
"destination": "s4hana-backend",
"authenticationType": "xsuaa",
"csrfProtection": false
}
]
}
Let us break down what each part means.
Sources, targets, and destinations
Each route has a source — a regular expression matched against the incoming URL path. The first matching route wins, so order matters: put specific routes before generic ones.
Then there are two ways to say where the request goes:
localDir— serve static files from a folder inside the approuter app itself. This is how your Fiori UI5 resources get served. Thetargetrewrites the path (here, stripping the/myappprefix).destination— forward to a named destination. The name (likesrv-apiors4hana-backend) must match a destination configured in the BTP destination service or in thedefault-env.jsonfor local testing.
A frequent gotcha: the destination name in xs-app.json must exactly match the destination instance name — including case. A mismatch gives you a cryptic 502 and an afternoon of debugging.
Routes are evaluated top to bottom, first match wins. Put your most specific routes first and any catch-all route last — otherwise the catch-all swallows everything below it.
Authentication per route
The authenticationType field controls whether the approuter demands a login:
xsuaa— the user must be authenticated via XSUAA. Unauthenticated requests get redirected to the identity provider. This is what you want for anything behind a login.none— public route, no authentication. Useful for health checks or genuinely public content — but think twice before using it on API routes.
The top-level authenticationMethod field has two options: route (each route declares its own auth, as above) or all (everything requires authentication). For most apps, route gives you the flexibility you need.
A few fields people overlook
welcomeFile — what gets served when someone hits the approuter root (/). Usually your Fiori launchpad page or app index.
csrfProtection — the approuter validates CSRF tokens on state-changing requests by default. If you are proxying to a backend that handles CSRF itself (or does not use it), you may need to disable it per route, as in the S/4HANA example above. Do not disable it globally unless you understand the implications.
httpMethods — restrict a route to specific HTTP verbs. Handy when you want GETs to go to one destination and POSTs somewhere else, though that is rare in practice.
How it fits together at runtime
Picture the request flow for a Fiori app backed by CAP:
- Browser requests
https://myapp.cfapps.../myapp/index.html. - Approuter matches the first route, sees
authenticationType: xsuaa, and — if no session exists — redirects to XSUAA for login. - After login, the approuter serves
index.htmlfromlocalDir. - The UI5 app then calls
/odata/v4/catalog/Books. The approuter matches the second route and proxies to thesrv-apidestination (your CAP service), attaching the user's JWT so the backend can authorize the request.
That JWT forwarding is the quiet superpower of the approuter: your backend receives the authenticated user's identity without any extra code, which is exactly what XSUAA-based authorization in CAP relies on.
If your app works with cds watch locally but fails on BTP, check xs-app.json first: destination names, route order, and authentication types are the cause more often than not.
Local testing tip
You can run the approuter locally with npm start (the @sap/approuter package) and a default-env.json that defines your destinations. This catches route mistakes before you deploy. The destination names in default-env.json must match the ones in xs-app.json — same rule as on BTP, same 502 if you get it wrong.