# Express

> Two Express routes for MorphScreen, one for connectAgent's runs and one that streams chat completions for a chat UI of your own, with the key kept on the server.

```sh
npm install express
```

Express 5 on Node 18 or later. Keep `MORPHSCREEN_KEY` in the server's environment; it never goes to the page.

## The routes

```js
// Two routes for the page in Express 5: /api/agent carries connectAgent's
// runs on to /v1/ui, and /api/chat streams /v1/chat/completions for a chat
// UI of your own. The key stays on the server, and the server picks the model.
import express from "express";
import { Readable } from "node:stream";

const API = process.env.MORPHSCREEN_API ?? "https://api.morphscreen.com/v1";
const MODEL = process.env.MODEL ?? "deepseek/deepseek-v4.1-flash";
const HEADERS = { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY };

const app = express();
app.use(express.json({ limit: "1mb" }));

// Sends the body on and streams the answer back. When the reader leaves, the
// call to MorphScreen stops too, so no model keeps writing for nobody.
async function relay(req, res, path, body) {
  const stop = new AbortController();
  res.on("close", () => stop.abort());
  const upstream = await fetch(`${API}${path}`, { method: "POST", headers: HEADERS, body: JSON.stringify(body), signal: stop.signal });
  res.status(upstream.status).type(upstream.headers.get("content-type") ?? "text/event-stream");
  Readable.fromWeb(upstream.body).pipe(res);
}

app.post("/api/agent", (req, res) => relay(req, res, "/ui", { ...req.body, forwardedProps: { ...req.body.forwardedProps, model: MODEL } }));

app.post("/api/chat", (req, res) => relay(req, res, "/chat/completions", { model: MODEL, stream: true, messages: req.body.messages }));

app.listen(process.env.PORT ?? 3000);
```

- **`/api/agent`** is for [`connectAgent`](/docs/quickstart#draw-the-conversation): the page sends each run, the route sets the model and passes the run to `/v1/ui`, and the AG-UI events stream back as they come.
- **`/api/chat`** is for a chat UI of your own: it sends the conversation to `/v1/chat/completions` and passes the server-sent events through. Draw each answer with `createAnswer` or [`<Answer>`](/docs/integrations/react), and read the last chunk for the verdict and a repair ([Validation and repair](/docs/format/validation)).

You only need the one your page uses.

## Notes

- **The page's body limit.** A run carries the conversation so far, so `express.json` gets a 1 MB limit; raise it for long conversations.
- **Stopping.** When the reader closes the tab, `res` closes and the call to MorphScreen is aborted, so the model stops writing.
- **Errors pass through.** MorphScreen's status and its JSON error reach the page unchanged: a 401 for a wrong key, a 429 with its wait ([Limits and errors](/docs/production/limits)).

We run this file in CI against a recorded answer: a page with `connectAgent` draws it through `/api/agent`, and `/api/chat` streams it back whole.
