> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praxa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Praxa backend with Express

> Create an authenticated Express 5 route for Praxa tasks with validation, deterministic retries, and integration tests.

This pattern keeps Praxa behind your existing Express authentication,
validation, rate-limit, and audit middleware.

```mermaid theme={null}
sequenceDiagram
  participant C as Client
  participant E as Express route
  participant P as Praxa
  C->>E: POST /api/tasks plus app session
  E->>E: authenticate, validate, derive key
  E->>P: POST /v1/execute
  P-->>E: 202 RunResource or problem
  E-->>C: safe no-store response
```

## Prerequisites

Before you begin, prepare:

* a trusted server runtime and application authentication boundary;
* a disposable personal workspace Praxa key with only the tutorial's required scopes;
* synthetic input plus a persisted application request ID for replay tests;
* a fake upstream for unit tests and a non-production environment for canaries;
* an acceptance assertion that proves the authenticated Express route rejects unsafe input before admitting and reading one task.

## 1. Install and configure

```bash theme={null}
npm install express@5
export PRAXA_API_KEY="praxa_sk_<redacted>"
```

Enable JSON parsing with a bounded body limit before the route:

```ts theme={null}
import express from "express";

const praxaApiKey = process.env.PRAXA_API_KEY;
if (!praxaApiKey) throw new Error("PRAXA_API_KEY is required");

const app = express();
app.use(express.json({ limit: "32kb" }));
```

## 2. Add the route

```ts theme={null}
import { createHash } from "node:crypto";

app.post("/api/tasks", requireSession, async (request, response) => {
  const task = request.body?.task;
  const requestId = request.body?.requestId;

  if (
    typeof task !== "string" ||
    task.length < 1 ||
    task.length > 16_000 ||
    typeof requestId !== "string" ||
    requestId.length < 16 ||
    requestId.length > 128
  ) {
    return response.status(400).json({ error: "Invalid request" });
  }

  const key =
    "express:" +
    createHash("sha256")
      .update(request.user.id + "\0" + requestId)
      .digest("hex");

  const upstream = await fetch("https://api.praxa.io/v1/execute", {
    method: "POST",
    headers: {
      authorization: "Bearer " + praxaApiKey,
      "content-type": "application/json",
      "idempotency-key": key,
    },
    body: JSON.stringify({
      apiVersion: "v1",
      requestId: key,
      mode: "task",
      task: { input: task },
      idempotencyKey: key,
    }),
  });

  response.set(
    "content-type",
    upstream.headers.get("content-type") ?? "application/json",
  );
  response.set("cache-control", "no-store");
  return response.status(upstream.status).send(await upstream.text());
});
```

Express 5 forwards rejected promises from async handlers to error middleware.
Your final error handler should return a customer-safe problem and never echo
headers, tokens, or upstream bodies that may contain sensitive context.

## 3. Add replay-safe client behavior

Generate the application request ID when the user starts the action. Store it
with the pending UI state and reuse it after transport failures:

```ts theme={null}
const pending = {
  requestId: crypto.randomUUID(),
  task: "Summarize the incident and propose a safe next action.",
};

await fetch("/api/tasks", {
  method: "POST",
  credentials: "include",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(pending),
});
```

## 4. Test end to end

Use your normal HTTP test client and an intercepted upstream:

| Test                            | Expected result                        |
| ------------------------------- | -------------------------------------- |
| Signed-out request              | <code>401</code>; zero upstream calls  |
| Oversized task                  | <code>400</code>; zero upstream calls  |
| Valid request                   | One Praxa request with a server key    |
| Exact retry                     | Same idempotency key and logical run   |
| Changed body under the same key | Praxa <code>409</code>                 |
| Revoked server key              | Safe auth failure; no token reflection |

In staging, submit one disposable task, poll the returned run, and record the
terminal state separately from the admission response.

## Troubleshooting

| Symptom                             | Fix                                                               |
| ----------------------------------- | ----------------------------------------------------------------- |
| `request.body` is undefined         | Register bounded JSON parsing before the route                    |
| Auth middleware runs after Praxa    | Put authentication before validation and execution in route order |
| Error handler exposes upstream text | Map to a customer-safe problem and log only redacted metadata     |
| Timeout creates new work            | Preserve and reuse the original request ID/body                   |
| Too many open connections           | Reuse the process-level Fetch/HTTP client and bound concurrency   |
| Proxy changes streaming behavior    | Disable buffering and test SSE through the deployed proxy         |

## Best practices

* Set a strict JSON body limit.
* Use one error-mapping middleware for Praxa status and problem codes.
* Derive identity from `request.user`, never `request.body`.
* Set `Cache-Control: no-store` on run and mutation projections.
* Forward request abort through an `AbortSignal` where supported.
* Test with a fake upstream, then run a disposable staging canary.

<Card title="Express 5 migration guide" icon="arrow-up-right-from-square" href="https://expressjs.com/en/guide/migrating-5/">
  Confirm the current async error and routing behavior in the official guide.
</Card>

## Optimize for production

* Reuse one configured HTTP or SDK client per process and bound concurrent upstream work.
* Prefer durable admission plus asynchronous readback over holding application requests open.
* Cache only non-sensitive, tenant-scoped reads within their documented freshness window.
* Measure p50/p95 latency, admission-to-terminal time, retries, conflicts, and connection reuse before tuning.

Optimize only after the correctness and isolation matrix passes. Lower latency or cost is not an improvement if verified outcomes, authority checks, or recovery rates regress.

## Cleanup and next steps

1. Revoke the disposable Praxa key and require a later request to fail.
2. Remove synthetic application records and any temporary environment files.
3. Cancel or archive unresolved test runs according to the application policy.
4. Retain only redacted request, run, and verification identifiers needed for the test record.

After cleanup, run the [shared integration test matrix](/tutorials/test-your-integration) and record any environment-specific check that remains pending.

## Frequently asked questions

### What proves this tutorial works?

The minimum observable result is that the authenticated Express route rejects unsafe input before admitting and reading one task. A compile, package import, mocked response, or initial admission alone does not prove the complete workflow.

### Can a browser, mobile app, or model prompt hold the credential?

No. Browser and mobile bundles are inspectable. Keep the Praxa credential in the trusted application backend.

### How should an ambiguous mutation be retried?

Persist the exact logical input and idempotency key before the first attempt. Reconcile through authoritative readback or replay the exact request with that same key before creating new work.

### What should we monitor after release?

Monitor application auth failures, upstream status/code, latency, retries, replay conflicts, and terminal run outcomes. Alert on authorization bypass, cross-tenant disclosure, repeated conflicts, or cleanup failure.
