# Express

Source: https://permdock.com/docs/adapters/express

permdock/express wraps the Fetch kernel as Express middleware, exposing req.permdock and a protect guard, with Problem Details errors routed through Express error handling.

## Purpose [#purpose]

Express 4 and 5 use Node's `IncomingMessage` / `ServerResponse` rather than Fetch objects. `permdock/express` converts the request once, delegates to the [server kernel](/docs/adapters/server-kernel), attaches the request-scoped `PermDock` as `req.permdock`, and provides `protect` as ordinary middleware. A PermDock error raised by `permdock()`, `protect` or the decision endpoint (a rejected Web Bot Auth signature, a validation failure) is answered with Problem Details directly; any other rejection is forwarded to `next(err)` so Express 4 does not hang (the fail-closed fix permix needed in its Express adapter).

## API [#api]

```ts
import express from "express";
import { createPermDock } from "permdock/express";
import { policy } from "./policy";
import { permissions } from "./permissions";

export const { permdock, protect, errorHandler } = createPermDock(policy, {
  subject: (req) => req.user ?? null,
});

const app = express();
app.use(permdock()); // req.permdock is the request-scoped PermDock
app.use(express.json()); // either order: permdock() never reads the body stream
app.delete(
  "/posts/:id",
  protect(permissions.post.delete, (req) => loadPost(req.params.id)),
  async (req, res) => {
    await deletePost(req.permdockData);
    res.status(204).end();
  },
);
app.use(errorHandler()); // PermDockDeniedError → 403 application/problem+json
```

| Export | Role |
| --- | --- |
| `permdock()` | Middleware. Calls `subject(req)`, builds the instance, sets `req.permdock`. The `Request` type extension is scoped to the factory result (a typed `Request` alias is exported), not to Express globally. |
| `protect(permission, loadData?, options?)` | Middleware. The `tenant` option (`(req) => req.params.org`) is resolved for each `protect`, after routing. Loads data for instance actions (`req.permdockData`), decides, and either calls `next()` or writes the 403 Problem Details response. PermDock errors become Problem Details; other rejections go to `next(err)`. |
| `errorHandler()` | Error middleware that maps `PermDockDeniedError`, `PermDockApprovalRequiredError` and `PermDockValidationError` thrown inside handlers to Problem Details responses; other errors pass through. |
| `permdockHandler()` | Router (`mergeParams: true`) mounting the AuthZEN-shaped decision endpoint: `app.use('/api/permdock', permdockHandler())`, or `app.use('/:org/permdock', permdockHandler())` to decide in the path tenant. |
| `openapi` | Kernel hook contract for `express-openapi` or hand-written documents. |

### Typing `req.permdock` [#typing-reqpermdock]

The factory returns a `Request` type alias (`PermDockRequest`) with `permdock` and `permdockData` declared. Handlers that need the instance annotate their `req` parameter with it, or use the returned `withPermDock(fn)` wrapper that supplies the type. Global augmentation of `express-serve-static-core` is deliberately avoided: two factories (for example, one per tenant policy) would otherwise fight over the same declaration, and a test double would inherit production types.

```ts
app.get(
  "/posts",
  withPermDock(async (req, res) => {
    res.json(req.permdock.filter(permissions.post.read, await listPosts()));
  }),
);
```

## Request lifecycle [#request-lifecycle]

`permdock/express` follows the [shared adapter contract](/docs/adapters) through the [server kernel](/docs/adapters/server-kernel). What differs on Express:

* `permdock()` runs after your auth middleware. The Web `Request` behind `req.permdock` reads the Node stream only when something consumes its body, so `express.json()`, multer or busboy mounted after `permdock()` still receive the whole body.
* `protect` stores the loaded row on `req.permdockData`, so the handler does not run the loader twice. Assertions thrown inside handlers are routed by `errorHandler()`.
* A sub-router needs `express.Router({ mergeParams: true })` for a `tenant` resolver that reads a parent param such as `:org`.
* `protect` used before `permdock()` throws a descriptive error at the first request rather than reading `undefined`.
* Decision events carry `req.method` and `req.route.path`.

Express 4 and 5 are both supported (`express >=4`): rejections PermDock does not map are forwarded to `next(err)`, which Express 4 needs and Express 5 tolerates. Denials are the kernel's Problem Details responses.

## Example app [#example-app]

`apps/examples/express`: posts API on Express 5 with `permdock()`, `protect`, `errorHandler()`, the decision endpoint, and supertest-based tests for the deny and approval-required bodies. No test runs Express 4; the catalog pins Express 5. `tests/integration/src/http/express.test.ts` runs the [`testHttpAdapter`](/docs/adapters/testing) scenarios on `node:http` with `permdock()` mounted before `express.json()`, a multer upload and a `mergeParams` sub-router.

## Related standards [#related-standards]

* [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi).
