> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js

> Build the Express backend for your JTL Cloud App. Verify session tokens, fetch access tokens, and proxy requests to the JTL Cloud API.

Build the Express backend for a JTL Cloud App using Node.js, TypeScript, and the `jose` library for JWT verification. By the end, the backend runs locally and the frontend's "Waiting for backend" placeholder turns into a real connection.

**Stack:** Node.js 24.16.0+, Express, TypeScript, `jose` for JWT verification.

## Prerequisites

You need:

* ✅ **A finished frontend** from the [Build the Frontend](/get-started/quick-start/from-scratch/frontend) page, running locally on `http://localhost:5173`
* ✅ **[Node.js](https://nodejs.org/) v24.16.0 or higher** (current LTS, includes `npm`). Verify with `node --version` and `npm --version`

## What you're Building

During setup, your backend verifies that the session token from your frontend is valid and was issued by JTL.

To do this:

* Your backend authenticates with JTL using its client credentials
* Fetches JTL's public keys (JWKS)
* And uses them to verify the session token's signature

```mermaid theme={null}
sequenceDiagram
    participant FE as Your Frontend
    participant BE as Your Backend (Express)
    participant Auth as JTL Auth
    participant API as JTL Cloud API

    FE->>BE: POST /api/connect-tenant (session token)
    BE->>Auth: POST /oauth2/token (client credentials)
    Auth-->>BE: Access token (JWT)
    BE->>API: GET /.well-known/jwks.json (with access token)
    API-->>BE: Public keys (JWKS)
    BE->>BE: Verify session token signature
    BE-->>FE: Verified tenant info
```

Once verified, the token tells your backend which tenant (merchant) and user is using your app. Your backend can then make tenant-scoped requests to the JTL Cloud API on their behalf.

## 1. Set up the Project

Create a `backend` folder alongside the existing `frontend` folder, then initialize it.

```bash theme={null}
cd my-jtl-app
mkdir backend
cd backend
npm init -y
```

Your project structure now looks like this:

```
my-jtl-app/
├── frontend/      # From the previous page
└── backend/       # New
```

## 2. Install Packages

```bash theme={null}
npm install express cors jose
npm install -D typescript tsx @types/node @types/express @types/cors
```

| Package      | Purpose                                                                |
| ------------ | ---------------------------------------------------------------------- |
| `express`    | HTTP server and routing                                                |
| `cors`       | Allows the frontend dev server to call the backend during development  |
| `jose`       | Verifies JWTs using JTL's public keys                                  |
| `typescript` | Type checking and `tsc` compiler                                       |
| `tsx`        | Runs TypeScript files directly during development without a build step |
| `@types/*`   | Type definitions for the runtime packages                              |

## 3. Configure TypeScript

Create `backend/tsconfig.json`:

```json theme={null}
{
	"compilerOptions": {
		"target": "ES2022",
		"module": "NodeNext",
		"moduleResolution": "NodeNext",
		"esModuleInterop": true,
		"strict": true,
		"skipLibCheck": true,
		"outDir": "dist",
		"rootDir": "src"
	},
	"include": ["src/**/*"]
}
```

This configuration uses Node's native ESM support (`NodeNext`) with strict type checking. The `outDir` and `rootDir` settings keep compiled output separate from source files when you eventually build for production.

## 4. Set up Environment Variables

The backend needs two secrets: a client ID and a client secret, both issued by JTL when you create your app. You will get the real values from the [Partner Portal](https://partner.jtl-cloud.com) in the next page. For now, create the file with placeholder values so the rest of the setup works.

Create `backend/.env`:

```dotenv theme={null}
CLIENT_ID=your-client-id-here
CLIENT_SECRET=your-client-secret-here
PORT=5273
```

The frontend's Vite dev proxy is already configured to forward `/api/*` requests to `localhost:5273`, so the `PORT` value matches that target.

Add `.env` to `backend/.gitignore` so the secrets don't end up in version control:

```
node_modules
dist
.env
```

## 5. Build the Auth Helper

The first piece of the backend is a function that authenticates with JTL using your client credentials and returns an access token. This token has two uses: fetching the public keys for session token verification, and making tenant-scoped calls to the JTL Cloud API.

Create `backend/src/jtl-auth.ts`:

```typescript theme={null}
const AUTH_ENDPOINT = 'https://auth.jtl-cloud.com/oauth2/token';
const API_BASE_URL = 'https://api.jtl-cloud.com';

export function getApiBaseUrl(): string {
	return API_BASE_URL;
}

export async function getJwt(): Promise<string> {
	const clientId = process.env.CLIENT_ID;
	const clientSecret = process.env.CLIENT_SECRET;

	if (!clientId || !clientSecret) {
		throw new Error('CLIENT_ID and CLIENT_SECRET must be defined in .env');
	}

	const authString = Buffer.from(`${clientId}:${clientSecret}`).toString(
		'base64',
	);

	const response = await fetch(AUTH_ENDPOINT, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded',
			Authorization: `Basic ${authString}`,
		},
		body: new URLSearchParams({ grant_type: 'client_credentials' }),
	});

	const data = await response.json();

	if (!response.ok) {
		throw new Error(
			`Failed to fetch JWT (${response.status}): ${data.error}`,
		);
	}

	return data.access_token;
}
```

This encodes the client credentials as Base64, sends a `client_credentials` grant request to JTL's auth endpoint, and returns the resulting access token. The token is short-lived, so the function fetches a fresh one on each call. For higher-traffic backends you'd add caching.

## 6. Build the Session Verifier

With an access token in hand, the backend can fetch the public keys it needs to verify session tokens. The `jose` library handles the cryptographic work once it has the public key in the right format.

Create `backend/src/verify-session.ts`:

```typescript theme={null}
import { importJWK, jwtVerify } from 'jose';
import { getJwt, getApiBaseUrl } from './jtl-auth.js';

export interface SessionTokenPayload {
	exp: number;
	userId: string;
	tenantId: string;
	tenantSlug: string;
}

export async function verifySessionToken(
	sessionToken: string,
): Promise<SessionTokenPayload> {
	const accessToken = await getJwt();
	const baseUrl = getApiBaseUrl();

	const response = await fetch(`${baseUrl}/account/.well-known/jwks.json`, {
		headers: { Authorization: `Bearer ${accessToken}` },
	});

	if (!response.ok) {
		throw new Error(`Failed to fetch JWKS (${response.status})`);
	}

	const jwks = await response.json();
	
	// Select the signing key by its JWK properties (use=sig, alg=EdDSA)
	const signingKey = jwks.keys.find(
		(k: { use?: string; alg?: string }) =>
			k.use === 'sig' && k.alg === 'EdDSA',
	);
 
	if (!signingKey) {
		throw new Error('No EdDSA signing key found in JWKS');
	}
 
	const publicKey = await importJWK(signingKey, 'EdDSA');

	const { payload } = await jwtVerify(sessionToken, publicKey);
	return payload as unknown as SessionTokenPayload;
}
```

The function fetches the JWKS document from the JTL API, imports the first key as an EdDSA public key, and asks `jose` to verify the session token's signature against it. If the signature is valid and the token isn't expired, `jwtVerify` returns the decoded payload. If anything is wrong, it throws an error.

## 7. Build the Connect Tenant Endpoint

Now connect the verifier into an Express route. The frontend's shell layout sends the session token to `/api/connect-tenant` and expects a tenant ID back.

Create `backend/src/server.ts`:

```typescript theme={null}
import express, { type Request, type Response } from 'express';
import cors from 'cors';
import { verifySessionToken } from './verify-session.js';

const app = express();
const port = Number(process.env.PORT) || 5273;

app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());

app.get('/api/connect-tenant', async (req: Request, res: Response) => {
    const sessionToken = req.headers['x-session-id'] as string;

    if (!sessionToken) {
        return res.status(400).json({ error: 'Session token is required' });
    }

    try {
        const payload = await verifySessionToken(sessionToken);

        // In a production app, you'd store the tenant connection in your database here
        console.log('Tenant connected:', payload.tenantId);

        return res.json({
            success: true,
            tenantId: payload.tenantId,
            userId: payload.userId,
            tenantSlug: payload.tenantSlug,
        });
    } catch (error) {
        console.error('Connection failed:', error);
        return res
            .status(401)
            .json({ error: 'Failed to verify session token' });
    }
});

app.listen(port, () => {
    console.log(`Backend listening on http://localhost:${port}`);
});
```

CORS is configured to allow requests from the Vite dev server on port 5173. The dev proxy in `vite.config.ts` already routes frontend `fetch('/api/...')` calls to this backend, but the CORS middleware is a useful safety net during development and stays out of the way in production.

See the [Tenant Mapping](/guides/cloud-apps/authentication-login#tenant-mapping) section for more on managing tenants in production.

## 8. Add Run Scripts

Open `backend/package.json` and replace the `scripts` block:

```json theme={null}
"scripts": {
    "dev": "tsx watch --env-file=.env src/server.ts",
    "build": "tsc",
    "start": "node --env-file=.env dist/server.js"
}
```

The `dev` script uses `tsx watch` to run TypeScript directly, restarting the server when any source file changes. The `--env-file=.env` flag loads environment variables natively without needing `dotenv`. The `build` and `start` scripts compile to JavaScript and run the compiled output for production.

Also update the `"type": "commonjs"` to `"type": "module"` below the `scripts` in the `package.json` file so that Node can treat `.js` files as ES modules.

## 9. Run the Backend

Start the dev server:

```bash theme={null}
npm run dev
```

You should see:

```
Backend listening on http://localhost:5273
```

In a second terminal, send a request with a fake session token to confirm the route is reachable:

```bash theme={null}
curl http://localhost:5273/api/connect-tenant \
  -H "x-Session-id: fake-token"
```

You should get back a `401` response with `{"error":"Failed to verify session token"}`. A real session token from the App Shell will follow the same path and succeed.

## Common Issues

<AccordionGroup>
  <Accordion title="'Cannot find module ./jtl-auth.js' or similar import error">
    This error usually means TypeScript and Node are resolving modules differently.

    With `"type": "module"` in `package.json` and `"module": "NodeNext"` in `tsconfig.json`, Node expects ES module imports to include file extensions. Even if your source file is `jtl-auth.ts`, the import must use `./jtl-auth.js`.

    TypeScript resolves this correctly during development, and Node finds the compiled `.js` file at runtime.

    If you prefer not to use `.js` extensions, switch to `"module": "CommonJS"` in `tsconfig.json` and remove `"type": "module"` from `package.json`.
  </Accordion>

  <Accordion title="'CLIENT_ID and CLIENT_SECRET must be defined in .env'">
    This means the backend started without loading your environment variables.

    The most common cause is running Node without the `--env-file=.env` flag. In that case, the `.env` file exists but is never read.

    The `dev` and `start` scripts already include this flag. If you're running the server manually, add it back or use `npm run dev`.

    Also confirm that the `.env` file is inside the `backend/` directory. The path is resolved relative to where Node is executed.
  </Accordion>

  <Accordion title="'Failed to fetch JWT (401)' from the auth endpoint">
    A 401 from the auth endpoint means the credentials are not valid.

    If you are still using placeholder values, this is expected. Real credentials are provided after registering your app in the Partner Portal.

    If you have already registered:

    * check for typos or extra spaces in `.env`
    * restart the dev server after making changes

    Environment variables are only read at startup, so updates to `.env` require a restart.
  </Accordion>

  <Accordion title="CORS error in the browser console">
    This means the browser blocked the response due to an origin mismatch.

    The backend allows requests from `http://localhost:5173`, which is the default Vite dev server port. If Vite runs on a different port (for example, 5174), the request will be rejected.

    Update the `origin` in `server.ts` to match the actual port, or restart Vite on 5173.

    If you're using the Vite dev proxy for `/api/*`, CORS should not appear. Seeing this error usually means the request is being made directly to the backend instead of going through the proxy.
  </Accordion>
</AccordionGroup>

## Next: Connect and Fetch Data

The backend verifies session tokens and is ready to call the JTL Cloud API. The remaining work is registering your app with JTL to get real credentials, installing the app in the JTL Hub, and pulling product data from JTL-Wawi:

<CardGroup cols={2}>
  <Card title="Connect and Fetch Data" icon="link" href="/get-started/quick-start/from-scratch/connect-and-fetch">
    Register your app, install it in the JTL Hub, and fetch products from
    JTL-Wawi.
  </Card>
</CardGroup>
