# Astro

Serve Courier's proxy from an Astro API route rendered on demand, with @noverax/courier-astro.

`@noverax/courier-astro` serves the proxy that `createProxyCaller` and `createProxyQueryCaller` call: one route answers every endpoint, reads over `GET` and writes over `POST`, and streams server-sent events.

## In one command

```bash
npx @noverax/create-courier
```

In an Astro site it adds what this page sets up by hand — the route rendered on demand, the middleware that registers your APIs, and Studio's integration — with a typed example API, its mock, and a page at `/courier`. See [Create Courier](/docs/create-courier).

## Install

```bash
npm install @noverax/courier-astro @noverax/courier astro
npx astro add node
```

The proxy answers each call as it arrives, so its route renders on demand, and that needs a server adapter. `@astrojs/node` is shown; any adapter works.

## Usage

```ts
// src/middleware.ts
import { defineMiddleware } from "astro:middleware"
import { registerSources } from "@noverax/courier"
import { sources } from "./api"

registerSources(sources)

// Astro requires its middleware module to export `onRequest`.
export const onRequest = defineMiddleware((_context, next) => next())
```

```ts
// src/pages/api/request.ts
import { courierHandler } from "@noverax/courier-astro"

// Rendered on demand: a prerendered route answers every call with the
// same file built once at build time.
export const prerender = false

const handler = courierHandler()
export const GET = handler
export const POST = handler
```

`courierHandler()` is zero-arg — sources are read at request time from the core global registry populated by `registerSources()` at module load.

`export const prerender = false` is what keeps the route on demand in Astro's default static output, where every other page stays prerendered. Without it the build renders the route once, and the deployed site answers every call with that file: a `POST` gets 200 and the same error body, with nothing to show it failed. With `output: "server"` routes are on demand already and the line changes nothing.

### Uploads

Astro checks the `Origin` of form submissions — `multipart/form-data`, which uploads use — against the origin it believes it serves, and answers a mismatch with a plain-text 403 "Cross-site POST form submissions are forbidden" before the proxy sees the call. A caller that sends no `Origin` at all, such as a server-side script, is refused the same way. When a same-origin upload is refused in production, Astro could not tell its own address; name it:

```js
// astro.config.mjs
import { defineConfig } from "astro/config"
import node from "@astrojs/node"

export default defineConfig({
  adapter: node({ mode: "standalone" }),
  security: { allowedDomains: [{ protocol: "https", hostname: "app.example.com" }] },
})
```

The proxy reads an upload up to its `maxUploadBytes` — 10 MB unless you pass another, as in `courierHandler({ maxUploadBytes: 50 * 1024 * 1024 })` — and a JSON call up to `maxBodyBytes`, 1 MB.

## The client

Pair with `createProxyCaller` on the client, given the routes Courier writes into `courier.routes.ts` from the endpoints you mark `.expose()` — see [Exposing Endpoints](/docs/proxy-mode/expose):

```ts
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "./courier.routes"

// The routes name the key this source has in your `sources` map —
// it is what the proxy addresses, and it travels as `?_s=<name>`.
const caller = createProxyCaller(apiRoutes, {
  proxyPath: { http: "/api/request" },
})
const users = await caller.users.getAll()
```

## The user's session

An API that keeps a `session` gets the user's session with every call a page's frontmatter, an endpoint, an action or a step it awaits makes, once the request is served with it at hand:

```ts title="src/middleware.ts"
import { requestContext } from "@noverax/courier-astro/request-context"

export const onRequest = requestContext()
```

Put it in a `sequence` when the application has middleware of its own. The session cookies the API sets reach the browser through Astro's `cookies`, as a page's own do. The request is kept in `AsyncLocalStorage`, which Node provides, and Cloudflare with the `nodejs_compat` flag; the main entry imports none of it. A page built ahead of time has no request, and its calls carry no one's session.

Astro reads the host a request was sent to only from a domain it is told to trust, and otherwise takes every request for `http://localhost`, where its own origin check refuses the application's forms and the session cannot tell your pages from another site's. Name your domains in `security.allowedDomains`:

```js title="astro.config.mjs"
export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
  security: { allowedDomains: [{ hostname: "shop.example.com", protocol: "https" }] },
})
```

Astro 5 is checked with `@astrojs/node`. See [User Sessions](/docs/server-side/session).

## Cancelled calls

When the browser cancels a call — an aborted upload, a query it no longer needs — the proxy stops its own upstream request, following the request's signal. `@astrojs/node` fires that signal when the client leaves. The `astro dev` server does not, and exposes no connection to watch instead, so during development a cancelled call still runs to completion upstream.
