Install @sap/approuter, write an xs-app.json that routes requests to your CAP backend, and run the full AppRouter + CAP stack locally before deploying.
AppRouter is an SAP-provided Node.js reverse proxy. It has three jobs:
Authorization: Bearer <token> to every request it proxies to your backendWithout AppRouter, your SPA would need to implement OAuth2 PKCE itself and handle token refresh — AppRouter does all of that for free.
mkdir -p app/router
cd app/router
npm init -y
npm install @sap/approuter
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/catalog/(.*)$",
"target": "/catalog/$1",
"destination": "srv-api",
"authenticationType": "xsuaa",
"csrfProtection": false
},
{
"source": "^/(.*)$",
"target": "$1",
"localDir": "webapp",
"authenticationType": "xsuaa"
}
]
}
| Field | What it does |
|---|---|
source |
Regex matching incoming request path |
target |
Path to rewrite to (capture groups work) |
destination |
Named destination (defined in env vars or mta.yaml) |
localDir |
Serve static files from this folder (relative to xs-app.json) |
authenticationType |
xsuaa = require login, none = public route |
csrfProtection |
Set to false for pure API routes (OData handles it internally) |
{
"name": "approuter",
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
},
"dependencies": {
"@sap/approuter": "^16.x.x"
}
}
AppRouter needs to know where XSUAA and your backend are.
Create app/router/default-env.json (never commit this — add to .gitignore):
{
"destinations": [
{
"name": "srv-api",
"url": "http://localhost:4004",
"forwardAuthToken": true
}
],
"VCAP_SERVICES": {
"xsuaa": [
{
"name": "bookshop-xsuaa",
"label": "xsuaa",
"plan": "application",
"credentials": {
"clientid": "sb-bookshop...",
"clientsecret": "...",
"url": "https://yourtenant.authentication.eu10.hana.ondemand.com",
"xsappname": "bookshop"
}
}
]
}
}
Copy credentials from your XSUAA service key (from BTP Cockpit).
Terminal 1 — CAP backend:
cd bookshop
cds watch --profile hybrid
Terminal 2 — AppRouter:
cd app/router
npm start
AppRouter listens on port 5000 by default. Open http://localhost:5000 — you are redirected to XSUAA login, then back to your app.
PORT=5001 npm start
Or add to default-env.json:
{
"PORT": 5001
}
Setting "authenticationMethod": "none" globally in xs-app.json for local testing, then forgetting to change it before deploying. You end up with a publicly accessible app with no login. Always use "authenticationMethod": "route" and set individual routes to "authenticationType": "none" for genuinely public paths.