Package a React SPA for HTML5 App Repository, configure AppRouter routing, and propagate user JWT to CAP.
/api/* to your CAP service and /* to the SPAWhen you deploy a full-stack BTP app you need more than just a backend. Users must authenticate before reaching the UI, the UI bundle must be served efficiently, and the backend must trust the caller's identity. The HTML5 App Repository stores the static frontend assets and the AppRouter acts as the single entry point: it handles XSUAA login, serves the SPA, and forwards API calls to your CAP service with the user's JWT attached.
Browser
│
▼
AppRouter (your-app.cfapps.eu10.hana.ondemand.com)
│ ├─ GET / → HTML5 App Repository (serves index.html + JS/CSS)
│ └─ /api/* → CAP service (forwards with Authorization: Bearer <JWT>)
│
XSUAA (login redirect, token exchange)
The AppRouter is a pre-built Node.js app (@sap/approuter) that you configure, not code.
# Build your Vite app
cd frontend && npm run build # outputs dist/
# Create the xs-app.json inside dist/
frontend/dist/xs-app.json:
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/api/(.*)$",
"target": "/api/$1",
"destination": "cap-backend",
"authenticationType": "xsuaa"
},
{
"source": "^(.*)$",
"target": "$1",
"service": "html5-apps-repo-rt",
"authenticationType": "xsuaa"
}
]
}
xs-app.json at root)The root xs-app.json (in the AppRouter package, not the SPA) defines top-level routing:
{
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/do/logout",
"logoutPage": "/"
},
"routes": [
{
"source": "^/api/(.*)$",
"target": "/api/$1",
"destination": "cap-backend",
"csrfProtection": true,
"authenticationType": "xsuaa"
},
{
"source": "^(.*)$",
"target": "$1",
"service": "html5-apps-repo-rt",
"authenticationType": "xsuaa"
}
]
}
The cap-backend destination is defined in the BTP Destination Service:
URL: https://my-cap-app.cfapps.eu10.hana.ondemand.com
ProxyType: Internet
Authentication: OAuth2UserTokenExchange
TokenServiceInstanceName: my-xsuaa
OAuth2UserTokenExchange exchanges the AppRouter user token for a token scoped to the CAP service — this is how user identity is propagated.
package.json{
"name": "approuter",
"version": "1.0.0",
"dependencies": {
"@sap/approuter": "^14"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
modules:
- name: my-app-approuter
type: approuter.nodejs
path: approuter
requires:
- name: my-xsuaa
- name: my-html5-runtime
- name: my-destination-service
parameters:
disk-quota: 256M
memory: 256M
- name: my-app-ui-deployer
type: com.sap.application.content
path: frontend/dist
requires:
- name: my-html5-repo-host
parameters:
content-target: true
build-parameters:
build-result: dist
requires:
- name: frontend
artifacts:
- dist.zip
target-path: resources/
resources:
- name: my-xsuaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
- name: my-html5-repo-host
type: org.cloudfoundry.managed-service
parameters:
service: html5-apps-repo
service-plan: app-host
- name: my-html5-runtime
type: org.cloudfoundry.managed-service
parameters:
service: html5-apps-repo
service-plan: app-runtime
- name: my-destination-service
type: org.cloudfoundry.managed-service
parameters:
service: destination
service-plan: lite
When AppRouter forwards a request with OAuth2UserTokenExchange, CAP receives a JWT where req.user.id is the authenticated user's XSUAA subject. No extra code needed — CAP's @sap/cds-security verifies the token automatically.
this.before('*', req => {
console.log('Authenticated user:', req.user.id); // P0012345
console.log('Tenant:', req.tenant); // my-subaccount
});
During local development, the React dev server (:5173) talks directly to the CAP server (:4004) without AppRouter. Configure CORS in CAP:
// srv/server.js
cds.on('bootstrap', app => {
if (process.env.NODE_ENV !== 'production') {
app.use(require('cors')({ origin: 'http://localhost:5173', credentials: true }));
}
});
In production, AppRouter handles same-origin routing — CORS headers are not needed.
| Mistake | Fix |
|---|---|
Forgetting xs-app.json in the SPA bundle |
The HTML5 App Repository requires this file at the bundle root |
Using NoAuthentication on API routes |
Always use xsuaa for API routes — otherwise JWT is not forwarded |
| Hardcoding backend URL in React | Use relative paths (/api/...) — AppRouter routes them to CAP |
| Not enabling CSRF protection | Set "csrfProtection": true on mutation routes |
| Exposing AppRouter credentials to the browser | Never — AppRouter handles auth server-side |
You can package a React SPA for the HTML5 App Repository, configure AppRouter to serve the SPA and proxy API calls to CAP with the user JWT, wire the MTA descriptor to bind XSUAA, HTML5 Repo, and Destination Service, and propagate user identity end-to-end from browser to CAP handler.
Sign in for progress tracking & Q&A →