Skip to main content
Build the React frontend for a JTL Cloud App using Vite, TypeScript, TanStack Router, and the JTL AppBridge SDK. By the end, the app runs locally with three working routes and a clear state for connecting to your backend. Stack: Vite, React, TypeScript, TanStack Router (file-based routing), JTL AppBridge, JTL Platform UI.

Prerequisites

You need:
  • A JTL ID (your login to the JTL ecosystem)
  • Access to an organization (tenant) in the Partner Portal (created automatically on first login)
  • Node.js v24.16.0 or higher (current LTS, includes npm). Verify with node --version and npm --version
If you don’t have your JTL ID and Partner Portal access yet, follow Create a Developer Account first.

What you’re Building

Before writing code, here’s how a JTL Cloud App works: Your frontend runs inside JTL’s App Shell (in an iframe). It communicates with the shell through AppBridge, a small SDK that exposes shell capabilities to the iframe. The most important one is getSessionToken, a short-lived signed token that proves which merchant (tenant) is currently using your app. Your frontend sends that token to your backend, which verifies it and uses it to make tenant-scoped calls to the JTL API. Not every page in your app runs inside the shell. The frontend has three routes, and they fall into two groups:
  • /setup and /erp run inside the App Shell iframe. AppBridge is available, and the app knows which tenant it’s serving.
  • /hub opens in a standalone browser tab when a merchant clicks the app card. There’s no iframe, no AppBridge, and no tenant context.
These two contexts require slightly different setup. You’ll handle that when building the frontend.

1. Create the Project

When prompted:
  • Ok to proceed? (y): y
  • Install with npm and start now?: Yes
Once Vite finishes installing, press Ctrl + C to stop the dev server.

2. Install Packages

PackagePurpose
@tanstack/react-routerType-safe router with file-based routing
@tanstack/router-pluginVite plugin that auto-generates the route tree from your file structure
@tanstack/react-router-devtoolsDevtools panel for inspecting routes (dev only)
@jtl-software/cloud-apps-coreAppBridge SDK: communication between your app and the JTL App Shell
@jtl-software/platform-ui-reactJTL’s UI component library (Button, Card, Text, etc.)
tailwindcss + @tailwindcss/viteUtility-first CSS, required by the JTL UI library

3. Configure Vite

Replace frontend/vite.config.ts:
This does three things:
  • TanStack Router plugin watches src/routes/ and regenerates src/routeTree.gen.ts whenever you add or rename a route file. The plugin must be listed before react(). See the installation docs for details.
  • Tailwind plugin enables Tailwind utilities, which the JTL UI library depends on.
  • Dev proxy forwards /api/* requests to a backend on localhost:5273. Your frontend can call fetch('/api/...') and Vite will route the request without CORS issues during development.

4. Set up Styles

Replace frontend/src/index.css:

5. Create the Route Files

TanStack Router uses file-based routing, so your folder structure under src/routes/ is your route configuration. Delete the default src/App.tsx (the router replaces it), then create the following structure:
Two conventions are in play here:
  • __root.tsx is the top-level layout, always rendered.
  • _shell.tsx is a pathless layout route. The leading underscore means it doesn’t add a URL segment, so _shell.setup.tsx becomes /setup, not /_shell/setup. Any route file prefixed with _shell. shares the layout’s wrapper. hub.tsx doesn’t, so it bypasses the AppBridge initialization entirely.

Root Route

Add the following to your frontend/src/routes/__root.tsx:
The <Outlet /> is where child routes render. During development, a devtools panel appears in the bottom corner. It won’t be included in your production build.

Shell Layout (AppBridge Provider)

This layout wraps every iframe route. frontend/src/routes/_shell.tsx:
Then create a frontend/src/appBridge.tsx file that initializes AppBridge once, exposes the bridge and tenant ID through React Context, and shows a loading state until the connection completes.
Two things to note:
  • Dynamic import of createAppBridge keeps the SDK out of any pre-render path. AppBridge requires window, so it can only run client-side.
  • React Context exposes appBridge, tenantId, and connection state through the useAppBridge() hook. Any route that is a child of the _shell route can call it with no prop drilling and a single shared connection state.

Setup Page

Add the following to your frontend/src/routes/_shell.setup.tsx:
Uses the useAppBridge() hook to connect to the tenant. The error branch renders whenever /api/connect-tenant is unreachable or returns a non-2xx response. The success branch renders once a backend verifies the session token and returns a tenant ID.

ERP Page

Add the following to your frontend/src/routes/_shell.erp.tsx:
This is the page merchants see when they open your app from the ERP menu.

Hub Page

This route sits outside the shell layout and doesn’t inherit AppBridge initialization. When a merchant clicks the app card in the JTL Hub, this page opens in a full browser tab with no iframe and no session token. frontend/src/routes/hub.tsx:

6. Wire up the Router and Add AppBridgeProvider

Update frontend/src/main.tsx to initialize the router and wrap your shell layout with AppBridgeProvider.

7. Run the Frontend

Start the dev server:
The first time you run this, the TanStack Router plugin generates src/routeTree.gen.ts automatically. Visit each route to confirm the app works:
1

Visit http://localhost:5173/setup

You should see a spinner and then a message: “Waiting for backend”. This shows the routing works and the shell layout mounts, AppBridge attempts to initialize, and the fetch to /api/connect-tenant fails (no backend yet).
2

Visit http://localhost:5173/erp

Same waiting message as before. Confirms the second iframe route loads and reuses the shell layout.
3

Visit http://localhost:5173/hub

You should see the “Launched from JTL Cloud” card. No spinner, no error, since this route bypasses the shell layout entirely.

Common Issues

This error occurs when AppBridge is loaded outside a browser environment. It relies on the window object, which is not available during server-side rendering or in Node.In the shell layout, the SDK should be loaded using a dynamic await import('@jtl-software/cloud-apps-core') inside useEffect. If it is moved to a top-level import, it runs too early and triggers this error.Also make sure you are running npm run dev. This app is a client-only Vite app and is not designed to run in a server environment.
This means the route tree file has not been generated yet. It is created automatically by the TanStack Router Vite plugin when it detects your route files.Start the dev server with npm run dev, then save any file inside src/routes/. The plugin watches this folder and will generate src/routeTree.gen.ts as soon as it detects a change.If the file still does not appear, check vite.config.ts. The tanstackRouter() plugin must be listed before react() in the plugins array.
This usually means the route is not being treated as a pathless layout.In TanStack Router, the leading underscore (_) marks a route as pathless. If the file is named shell.setup.tsx (without the underscore), _shell becomes part of the URL.Rename the file to _shell.setup.tsx to restore the expected behavior. Also make sure you are using the flat-file convention (_shell.setup.tsx), not a folder structure like _shell/setup.tsx, since this guide assumes the flat-file format.
This usually happens when AppBridgeContext and useAppBridge are defined in the same file as a TanStack Router route (for example, _shell.tsx).During development, the router’s Vite integration can create multiple module instances under hot module replacement (HMR). This causes the provider and consumer to reference different context objects, so useAppBridge() returns null.Fix: Move the context, hook, and provider into a separate file (for example, src/appBridge.tsx) and import useAppBridge from there in all components.

Next: Build the Backend

Pick a language to build the backend that will verify session tokens and proxy requests to the JTL API:

Node.js

Express and TypeScript.

C# (.NET)

ASP.NET Core 8.

PHP

Slim 4 and PHP.