Application Router Setup
Set up the SAP Application Router (approuter) as the single entry-point for your Cloud Foundry application.
Purpose
The SAP Application Router is a key infrastructure component for web-facing applications running in Cloud Foundry on SAP BTP. It is used to:
- Serve static content: Deliver HTML, CSS, and JavaScript files directly to the client.
- Authenticate users: Trigger authentication processes via XSUAA to ensure secure access.
- Rewrite URLs: Modify URLs of requests to match routing rules.
- Forward or proxy requests: Redirect client requests to the appropriate backend services.
- Propagate user information: Include user data (user ID, roles) when forwarding requests to backend services.
In the Neo environment, the platform handled routing and authentication implicitly. In Cloud Foundry, you must explicitly set up the approuter as a separate Node.js module.
Detection
Do NOT invoke this skill based on the presence of
web.xmlalone. Almost every Java servlet application has aweb.xml. An approuter is only needed when the application requires user-facing authentication or serves static web content. Headless backend apps (REST APIs, mail, persistence, document management, keystore) must NOT have an approuter added — it creates an unbacked module and breaks deploy.
This skill applies only if at least one of these explicit auth/UI signals is present:
Explicit authentication signal in web.xml
<!-- auth-method is FORM or XSUAA -->
<auth-method>FORM</auth-method>
<!-- OR security-constraint definitions -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
</security-constraint>
# Detect auth signals
grep -l "auth-method\|security-constraint" src/main/webapp/WEB-INF/web.xml
UserProvider usage in Java source
grep -rl "UserProvider\|getUserPrincipal\|isUserInRole" src/main/java/
Static web content served to users
# HTML files in webapp/ — indicates a UI that needs an entry point
find . -name "*.html" -path "*/webapp/*" | head -5
User explicitly requests approuter setup
- "Set up approuter"
- "Add application router"
- "Configure approuter for my app"
When this skill does NOT apply — do not add an approuter
🛑 The presence of
web.xmlalone does NOT mean you need an approuter. An approuter's job is serving static UI content and/or fronting XSUAA authentication. A standalone, API-only backend with no authentication does not need one — and adding it is actively harmful: the standard approuter'sxs-app.jsonbinds XSUAA, so the module fails to boot withCannot find service uaa in environment: No service matches uaa(andias), taking down an otherwise-working app.Skip this skill (no approuter) when ALL of these hold:
- No authentication:
web.xmlhas no<auth-method>and no<security-constraint>, and there is no XSUAA<resource-ref>.- No served UI: no
.html/.css/.jsunderwebapp/(a servlet that only returns JSON/text is API-only).- The app is reached directly (e.g. a mail-sender, a REST/JDBC backend).
In that case the migrated app is a single
java.tomcatmodule bound directly to whatever services it needs (destination, connectivity, credstore, …) — noapprouter/module, noxs-app.json, no uaa/ias resources. Thepersistence, anddocument-managementscenarios are exactly this shape (JSON/REST backends with aweb.xmlbut no auth and no served UI).⚠️ If you add an approuter anyway, you own the route mapping. The standard
xs-app.jsonrewrites the public path onto the backend destination. If the WAR's context root is non-root (e.g.document-management.warserves at/document-management, not/) and thexs-app.jsonroute target does not account for it, every request 404s — the approuter forwards/ecmto the backend as/document-management/ecm, which does not exist. This is silent: the app is healthy, the approuter is healthy, and every call returns 404. Skipping the approuter entirely (the app is reached directly) avoids the whole class of mismatch.
Backend-only apps — XSUAA without approuter
If the application is a headless REST/servlet app (no user-facing UI, no FORM auth) but still needs XSUAA for technical token validation (e.g. machine-to-machine calls), do not add an approuter. Bind XSUAA directly to the Java module:
# mtad.yaml — XSUAA-only, no approuter module
modules:
- name: <app-name>
type: java.tomcat
path: target/<artifactId>.war
parameters:
buildpack: sap_java_buildpack_jakarta
memory: 1024M
disk-quota: 1024M
properties:
ENABLE_SECURITY_JAVA_API_V2: true
JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']"
JBP_CONFIG_SAP_MACHINE_JRE: "{ version: 25.+ }"
TARGET_RUNTIME: tomcat
SET_LOGGING_LEVEL: 'ROOT: INFO'
requires:
- name: <app-name>-xsuaa
resources:
- name: <app-name>-xsuaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: xs-security.json
The
authentication-xsuaaskill handles thexs-security.jsonand XSUAA service resource. An approuter is only needed when you want the platform to enforce authentication before requests reach the backend.
Prerequisites
Before invoking this skill, ensure you have invoked:
- sdk-replacement -
Use the sdk-replacement skill- Sets up SAP Cloud SDK
- REQUIRED before this skill
Decision: Standard vs Extended Approuter
Default to Standard. The extended approuter is rarely needed. XSUAA handles SAML, OIDC, and Basic auth challenges natively. Only choose Extended if you have confirmed that your backend application explicitly sends
WWW-Authenticateorcom.sap.cloud.security.logoutheaders in HTTP responses that require client-side redirect interception.
Before proceeding, determine which approuter variant your application needs:
| Scenario | Variant | Start Command |
|---|---|---|
| All endpoints require XSUAA auth | Standard | node node_modules/@sap/approuter/approuter.js |
| Mix of authenticated + public endpoints | Standard | node node_modules/@sap/approuter/approuter.js |
| Neo app used SAML2 + Basic Auth (common) | Standard | node node_modules/@sap/approuter/approuter.js |
Backend explicitly sends WWW-Authenticate headers that need client-side redirect |
Extended | node server.js |
Backend explicitly sends com.sap.cloud.security.logout headers |
Extended | node server.js |
Rule of thumb: Use Standard in almost all cases. The fact that the Neo application used SAML2 or Basic Auth does not mean you need the extended approuter — XSUAA handles these authentication methods natively. Only choose Extended if the user explicitly confirms the backend triggers
WWW-Authenticatechallenge headers that must be intercepted and redirected by middleware (this is very rare in CF).
Transformation Steps
Step 1: Create the approuter Directory
Create a new approuter/ directory in the project root:
mkdir -p approuter
Step 2: Create package.json
Standard Approuter
Create approuter/package.json — see assets/package.json:
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^16.0.0"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
Extended Approuter
If you need the extended approuter, use a custom start script instead:
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^16.0.0"
},
"scripts": {
"start": "node server.js"
}
}
Note: The version of
@sap/approutermay be updated over time. Check the NPM package page for the latest version during the actual migration.
Step 3: Create xs-app.json (Routing Configuration)
The xs-app.json file defines how the approuter routes requests to your backend application.
Standard xs-app.json
Create approuter/xs-app.json — see assets/xs-app.json:
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/(.*)$",
"target": "/$1",
"destination": "<destination-name>",
"authenticationType": "xsuaa",
"csrfProtection": false
}
]
}
Note: Replace
<destination-name>with the name of the destination you will wire inmtad.yaml(e.g.,backend-app-destination). This destination proxies requests from the approuter to your Java backend.
Context path:
sap_java_buildpack_jakartaalways serves the app at/(ROOT context). It additionally registers the app at/<warName>/only ifweb.xmlcontains a servlet mapped tourl-pattern: /. In most migrated apps there is no such mapping, so only/works — approuter targets should use/$1without any/<artifactId>/prefix.
xs-app.json with Protected and Public Routes
If your application has both protected and public endpoints:
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/protected(/.*)?$",
"target": "/protected$1",
"destination": "<destination-name>",
"authenticationType": "xsuaa",
"csrfProtection": false
},
{
"source": "^/(.*)$",
"target": "/$1",
"destination": "<destination-name>",
"authenticationType": "none",
"csrfProtection": false
}
],
"logout": {
"logoutEndpoint": "/logout",
"logoutPage": "/"
}
}
Important: Routes are matched top-to-bottom. Place more specific routes (e.g.,
/protected) above the catch-all route (^(/.*)).
Preserving Servlet Path Prefixes
If the backend's web.xml maps the JAX-RS/Jersey servlet to a specific prefix (e.g., /webapi/*), the approuter route must preserve that prefix in the target. A common mistake is writing a route that strips the prefix:
// WRONG — strips /webapi, backend servlet mapping won't match
{ "source": "^/webapi(/.*)?$", "target": "$1" }
// CORRECT — preserves /webapi, backend servlet mapping matches
{ "source": "^/webapi(/.*)?$", "target": "/webapi$1" }
On Neo there was no approuter — the browser called the backend directly and the servlet mapping matched naturally. On CF, the approuter sits in front, so its target must forward the path the backend expects. If the backend also serves static content (HTML, CSS, JS) from the WAR's webapp/ directory, do NOT change the servlet mapping to /* as this causes Jersey to intercept static resource requests, returning "Not Found".
Extended xs-app.json (Rare — Backend-Initiated Auth Challenges Only)
Warning: Only use this template if you have explicitly verified the need for extended approuter (see decision table above). In most migrations, the standard xs-app.json is sufficient. Using the extended configuration unnecessarily introduces routing complexity and potential crashes.
For applications where the backend explicitly sends WWW-Authenticate or com.sap.cloud.security.logout headers — see assets/xs-app-extended.json:
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/protected(/.*)?$",
"target": "/protected$1",
"destination": "<destination-name>",
"authenticationType": "xsuaa",
"csrfProtection": false
},
{
"source": "^/authentication/endpoint(/.*)",
"target": "$1",
"destination": "<destination-name>",
"authenticationType": "xsuaa",
"csrfProtection": false
},
{
"source": "^/basic/authentication/endpoint(/.*)",
"target": "$1",
"destination": "<destination-name>",
"authenticationType": "basic",
"csrfProtection": false
},
{
"source": "^/logout/callback",
"target": "/logout",
"destination": "<destination-name>",
"authenticationType": "none",
"csrfProtection": false
},
{
"source": "^/(.*)$",
"target": "/$1",
"destination": "<destination-name>",
"authenticationType": "none",
"csrfProtection": false
}
],
"logout": {
"logoutEndpoint": "/logout",
"logoutPage": "/logout/callback"
}
}
Step 4: Create Extended Approuter Files (if needed)
Skip this step if using the standard approuter.
approuter/server.js
Create approuter/server.js — see assets/server.js:
var approuter = require('@sap/approuter');
var ar = approuter();
ar.start({
extensions: [
require('./authentication-challenge-handler.js')
]
});
approuter/authentication-challenge-handler.js
Create approuter/authentication-challenge-handler.js — see assets/authentication-challenge-handler.js.
This custom middleware handles:
- SAML2 authentication challenges: Intercepts
WWW-Authenticate: SAML2 realm="Identity Authentication Service"headers from the backend and redirects the user to the XSUAA-authenticated endpoint. - Basic authentication challenges: Intercepts
WWW-Authenticate: Basic realm="SAP HANA Cloud Platform"headers and redirects to a Basic-authenticated endpoint. - Logout handling: Intercepts
com.sap.cloud.security.logoutheaders from the backend and processes logout redirect flows.
function redirect(response, locationURL) {
console.log('Redirecting to: [' + locationURL + ']');
response.setHeader('Location', locationURL);
response.statusCode = 303;
response.end();
}
function handleAuthenticationChallenge(context, authenticateHeader) {
let incomingRequest = context.incomingRequest;
let incomingResponse = context.incomingResponse;
if (authenticateHeader.includes('SAML2 realm="Identity Authentication Service"')) {
console.log('Handling SAML2 (OIDC) authentication challenge.');
incomingResponse.setHeader('Location', '/authentication/endpoint' + incomingRequest.url);
incomingResponse.statusCode = 303;
} else if (authenticateHeader.includes('Basic realm="SAP HANA Cloud Platform"')) {
console.log('Handling BASIC (OIDC) authentication challenge.');
incomingResponse.setHeader('Location', '/basic/authentication/endpoint' + incomingRequest.url);
incomingResponse.statusCode = 303;
}
}
function handleLogout(context, logoutRequest) {
let incomingRequest = context.incomingRequest;
let incomingResponse = context.incomingResponse;
if (logoutRequest.includes('logout-request')) {
console.log('Triggering logout.');
incomingResponse.setHeader('Location', '/logout/endpoint?originalURL=' + incomingRequest.url);
incomingResponse.statusCode = 303;
}
}
module.exports = {
insertMiddleware: {
beforeRequestHandler: [
{
handler: function authenticationChallengeHandler(request, response, callNextHandler) {
console.log('Handling request with path: [' + request.url + ']');
if (request.url.startsWith('/authentication/endpoint')) {
let locationURL = request.url.substring('/authentication/endpoint'.length);
redirect(response, locationURL);
} else if (request.url.startsWith('/basic/authentication/endpoint')) {
let locationURL = request.url.substring('/basic/authentication/endpoint'.length);
redirect(response, locationURL);
} else if (request.url.startsWith('/logout/callback?originalURL=')) {
let locationURL = request.url.substring('/logout/callback?originalURL='.length);
redirect(response, locationURL);
} else {
request.afterRequestHandler = function (context, done) {
let outgoingResponse = context.outgoingResponse;
let authenticateHeader = outgoingResponse.headers['www-authenticate'];
let logoutRequest = outgoingResponse.headers['com.sap.cloud.security.logout'];
if (authenticateHeader !== undefined) {
handleAuthenticationChallenge(context, authenticateHeader);
}
if (logoutRequest !== undefined) {
handleLogout(context, logoutRequest);
}
done(null, context.incomingResponse);
};
callNextHandler();
}
}
}
]
}
};
Step 5: Wire Approuter into mtad.yaml
Add the approuter module to your mtad.yaml. The Java backend must provide its URL, and the approuter requires it to create a destination:
modules:
# Java Backend Application
- name: <app-name>
type: java.tomcat
path: <path-to-war-file>
parameters:
buildpack: sap_java_buildpack_jakarta
disk-quota: 1024M
memory: 1024M
properties:
ENABLE_SECURITY_JAVA_API_V2: true
SET_LOGGING_LEVEL: 'ROOT: INFO'
requires:
- name: <app-name>-xsuaa
provides:
- name: <app-name>-java-app
properties:
java_app_url: '${default-url}'
# Application Router
- name: <app-name>-approuter
type: nodejs
path: approuter
parameters:
disk-quota: 256M
memory: 256M
routes:
- route: '${protocol}://<app-name>.${default-domain}'
protocol: http1
properties:
XS_APP_LOG_LEVEL: debug
requires:
- name: <app-name>-xsuaa
- name: <app-name>-java-app
group: destinations
properties:
name: <destination-name>
url: '~{java_app_url}'
forwardAuthToken: true
Note: Replace
<app-name>with your application name and<destination-name>with the destination name used inxs-app.json(e.g.,backend-app-destination).
Note: The
group: destinationsproperty creates a destination environment variable for the approuter at deployment time — no need to create a separate destination service instance for this internal routing.
Step 6: Verify Approuter Directory Structure
After completing all steps, verify the directory structure:
Standard Approuter
approuter/
├── package.json
└── xs-app.json
Extended Approuter
approuter/
├── authentication-challenge-handler.js
├── package.json
├── server.js
└── xs-app.json
Configuration Files
| File | Location | Purpose |
|---|---|---|
package.json |
approuter/ | Node.js dependencies and start script |
xs-app.json |
approuter/ | Routing configuration (routes, auth types) |
server.js |
approuter/ | Extended approuter entry point (optional) |
authentication-challenge-handler.js |
approuter/ | Custom middleware for auth challenges (optional) |
CF Services
| Service | Plan | Purpose |
|---|---|---|
xsuaa |
application | OAuth 2.0 authorization (bound to approuter) |
Note: The approuter itself requires an XSUAA service binding. The
authentication-xsuaaskill handles creating thexs-security.jsonand XSUAA service resource.
Verification
1. Check File Structure
ls -la approuter/
# Should show package.json, xs-app.json, and optionally server.js + handler
2. Validate xs-app.json
cat approuter/xs-app.json | python3 -m json.tool
# Should parse without errors
3. Build and Deploy
mvn clean package
cf deploy . -f
4. Test Routing
- Open the approuter URL:
https://<app-name>.<cf-domain> - For authenticated routes: should redirect to identity provider login
- For unauthenticated routes: should proxy directly to backend
- Check approuter logs:
cf logs <app-name>-approuter --recent
5. Verify Destination Wiring
cf env <app-name>-approuter
# Should show destinations environment variable with backend URL
Common Issues
Issue: 502 Bad Gateway
Cause: Backend application not reachable from the approuter.
Solution: Verify the provides/requires URL wiring in mtad.yaml. Check that the backend app is started: cf apps
Issue: "No destination found" error
Cause: Destination name in xs-app.json does not match the destination name in mtad.yaml.
Solution: Ensure <destination-name> is identical in both files.
Issue: CORS errors in browser
Solution: Add CORS headers configuration to xs-app.json:
{
"cors": [
{
"uriPattern": "^/api/(.*)$",
"allowedOrigin": [{ "host": "*" }],
"allowedMethods": ["GET", "POST", "PUT", "DELETE"],
"allowedHeaders": ["Authorization", "Content-Type"]
}
]
}
Issue: Infinite redirect loop with extended approuter
Cause: Route patterns in xs-app.json conflict with middleware redirect paths.
Solution: Ensure the /authentication/endpoint and /basic/authentication/endpoint routes in xs-app.json match the paths handled by authentication-challenge-handler.js.
Issue: TENANT_HOST_PATTERN errors (multi-tenant apps)
Solution: Add the pattern as a property in the approuter module in mtad.yaml:
properties:
TENANT_HOST_PATTERN: '(.*).cfapps.sap.hana.ondemand.com'
Issue: Blank/empty page — approuter routes to backend but page renders nothing
Cause: The approuter forwards the request correctly, and the HTML page loads (200 OK), but the page appears blank. This is typically caused by SAPUI5 not loading:
- In Neo,
com.sap.ui5.resource.ResourceServletserved SAPUI5 at/resources/*. In CF, that servlet doesn't exist, so<script src="resources/sap-ui-core.js">returns 404 silently. - The old CDN domain
sapui5.hana.ondemand.comno longer serves older SAPUI5 versions (pre-1.71) — pinned versions like1.38.xreturn 404. - The
sap_bluecrystaltheme was removed in SAPUI5 1.40+ and won't load.
Diagnosis: Open the browser developer tools (F12 → Network tab). Look for a 404 on sap-ui-core.js or theme CSS files.
Solution: Update all HTML files to load SAPUI5 from the current CDN domain, use a supported version, and replace deprecated themes:
<script src="https://ui5.sap.com/1.120.0/resources/sap-ui-core.js" ...
data-sap-ui-theme="sap_fiori_3" ...>
See the sdk-replacement skill, Step 6: Migrate SAPUI5 ResourceServlet for full instructions and bulk-fix commands.
Issue: REST API calls from the UI return 404 after migration
Cause: The backend's JAX-RS/CXF servlet is declared in web.xml but has no <servlet-mapping>. The Neo web.xml may have had a mapping that was accidentally omitted during the copy to the CF project. Without the mapping, API endpoints like /rest/* are unreachable.
Diagnosis: Check src/main/webapp/WEB-INF/web.xml — if there is a <servlet> for CXFServlet but no <servlet-mapping>, that's the problem.
Solution: Add the missing servlet mapping:
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
See the sdk-replacement skill, Step 7: Verify Servlet Mappings for details.
Issue: REST API calls return 404 — approuter strips servlet path prefix
Cause: The approuter's xs-app.json route uses a target pattern like "target": "$1" that strips a path prefix (e.g., /webapi). On Neo there was no approuter, so the browser called the backend directly at /webapi/cnbrest/v1/... and the servlet mapping /webapi/* matched. On CF, the approuter strips the prefix and the request arrives at the backend as /cnbrest/v1/..., which doesn't match the old servlet mapping.
Diagnosis:
# Check if xs-app.json strips a prefix
cat approuter/xs-app.json
# A route like: "source": "^/webapi(/.*)?$", "target": "$1"
# strips /webapi — request arrives at backend without it
# Check servlet mapping
grep -A2 "servlet-mapping" src/main/webapp/WEB-INF/web.xml
Solution: Fix the approuter route to preserve the path prefix by including it in the target:
{
"source": "^/webapi(/.*)?$",
"target": "/webapi$1",
"destination": "backend-app-destination"
}
This way the request path arrives at the backend unchanged and the existing servlet mapping /webapi/* continues to work.
Warning: Do NOT fix this by changing the servlet mapping from
/webapi/*to/*. While that makes API calls work, it causes Jersey to intercept ALL requests including static content (HTML, CSS, JS), returning "Not Found" for non-API paths. The backend typically serves both static content (via Tomcat's default servlet) and REST APIs (via the Jersey servlet). Mapping Jersey to/*breaks static content serving.
Next Steps
After completing this skill, proceed to:
- ../authentication-xsuaa/SKILL.md - Set up XSUAA security configuration (xs-security.json, auth-method, user access code)
- ../mta-descriptor/SKILL.md - Generate the full MTA deployment descriptor
Related Information
- Application Router | SAP Help Portal
- Extending the Application Router | SAP Help Portal
- Routing Configuration File | SAP Help Portal
- @sap/approuter NPM Package | npmjs.com
- Custom Middleware Injection | SAP Help Portal
- For advanced routing patterns, Docker deployment, and multi-tenancy, see references/advanced-routing.md