68 · HTML5 App Repository & AppRouter

Package a React SPA for HTML5 App Repository, configure AppRouter routing, and propagate user JWT to CAP.

Episode 68 — HTML5 App Repository & App Router

What you'll learn

Why this matters

When 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.

Architecture overview

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.

Packaging the frontend for HTML5 App Repository

# 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"
    }
  ]
}

AppRouter configuration (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.

AppRouter package.json

{
  "name": "approuter",
  "version": "1.0.0",
  "dependencies": {
    "@sap/approuter": "^14"
  },
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  }
}

MTA descriptor wiring

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

Verifying identity in CAP

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
});

CORS for local development

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.

Common mistakes

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

Checkpoint ✓

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 →