---
title: HTTP serving
---

# HTTP serving

Serve Vincle-rendered pages over HTTP: with streaming, negotiation, and
minimal boilerplate. For pre-rendered files, see
[Static generation](/integration/static).

## Using `serve()`

The `serve()` helper creates a `Response` from a page component and an
adapter. It handles streaming, headers, and content negotiation in one call.

```tsx
import { serve } from "@vincle/flow/http";
import { NativeAdapter } from "@vincle/flow/adapters";

Bun.serve({
  port: 3000,
  async fetch(req) {
    return serve(req, () => <Page />, NativeAdapter);
  },
});
```

The response body is a stream: the shell is flushed immediately: the browser starts parsing HTML while
fragments are still rendering.

### Signature

```tsx
serve(
  req: Request,
  page: (negotiation: Negotiation) => JSX.Element,
  adapter: StreamingAdapter,
  options?: FlowOptions & ResponseInit & {
    negotiate?: Negotiate;
    mode?: "full" | "fragment";
  },
): Promise<Response>
```

### Options

| Option           | Type                                   | Default  | Description                       |
| ---------------- | -------------------------------------- | -------- | --------------------------------- |
| `signal`         | `AbortSignal`                          | N/A      | Abort all pending renders         |
| `onError`        | `(error, info) => JSX.Element \| void` | N/A      | Per-fragment error handler        |
| `defaultTimeout` | `number`                               | N/A      | Per-fragment timeout (ms)         |
| `negotiate`      | `(req: Request) => Negotiation`        | N/A      | Extract per-request hints         |
| `mode`           | `"full" \| "fragment"`                 | `"full"` | Render full page or fragment-only |
| `headers`        | `HeadersInit`                          | N/A      | Additional response headers       |
| `status`         | `number`                               | `200`    | Response status code              |
| `statusText`     | `string`                               | N/A      | Response status text              |

## Using Node.js `http`

For environments where `Response` is not available, use `renderToStream`
directly and write each chunk to your response as it comes, without buffering.
The same pattern works with Express, Fastify, Hono, or anything built on
`node:http`.

```tsx
import { Slot, Defer, renderToStream } from "@vincle/flow";
import { NativeAdapter } from "@vincle/flow/adapters";
import http from "node:http";

declare function fetchComments(): Promise<{ text: string }[]>;

async function Comments() {
  const items = await fetchComments();
  return (
    <ul>
      {items.map((c) => (
        <li>{c.text}</li>
      ))}
    </ul>
  );
}

function Page() {
  return (
    <html>
      <body>
        <h1>My page</h1>
        <Slot name="comments">
          <p>Loading comments…</p>
        </Slot>
        <Defer target="comments">{() => <Comments />}</Defer>
      </body>
    </html>
  );
}

http
  .createServer(async (_req, res) => {
    const stream = renderToStream(() => <Page />, NativeAdapter);

    // Shell flushes first; deferred fragments follow as they resolve.
    res.writeHead(200, {
      "Content-Type": "text/html; charset=utf-8",
      "Transfer-Encoding": "chunked",
    });

    for await (const chunk of stream) {
      res.write(chunk);
    }
    res.end();
  })
  .listen(3000);

console.log("Listening on http://localhost:3000");
```

## HTMX negotiation

HTMX sends headers like `HX-Target` to indicate which fragment a request
expects. Vincle provides `negotiateHtmx` to handle this automatically:

```tsx
import { serve, negotiateHtmx } from "@vincle/flow/http";
import { HtmxAdapter } from "@vincle/flow/adapters";

Bun.serve({
  port: 3000,
  async fetch(req) {
    return serve(req, () => <Page />, HtmxAdapter, {
      negotiate: negotiateHtmx,
    });
  },
});
```

The response includes `Vary: HX-Target` so shared caches never serve a
fragment response to a full-page navigation, or vice versa.

### Custom negotiation

Write your own negotiator for any client library that uses custom headers:

```tsx
import type { Negotiate } from "@vincle/flow";

const negotiateMyLib: Negotiate = (req) => {
  const fragment = req.headers.get("X-Fragment-Id") ?? undefined;
  return {
    headers: { Vary: "X-Fragment-Id" },
    target: fragment,
  };
};
```

## Full-page vs fragment mode

By default, every request renders the full page: shell + all deferred
fragments. Use `mode: "fragment"` to suppress the shell and render only
the targeted fragment's content. This is useful when a client library
fetches a single fragment independently (an HTMX `hx-get`, a Turbo
`<turbo-frame src>`).

```tsx
serve(req, () => <Page />, NativeAdapter, { mode: "fragment" });
```

A `mode` passed to `serve()` overrides the one returned by the negotiator.
