# [Node adapter \(Express & Fastify\)](https://rebilder.com/docs/adapters/node)

> Connect-style middleware for Node HTTP servers (`@rebilder/gateway/node`), written against structural types and importing nothing from express, fastify, or node:http.

- **Updated:** 2026-08-12
- **Publisher:** Rebilder

## Zero framework imports, by structural typing

The adapter imports **nothing** from `express`, `fastify`, or even `node:http`. It is written against structural types (`NodeRequestLike`, `NodeResponseLike`) that describe only what the adapter reads and writes. Any real Node request/response object satisfies them, so the gateway’s zero-external-deps invariant holds and no `@types` packages ride along.

## Install & Express integration

terminal

```
npm install @rebilder/gateway        # pnpm add / yarn add
```

server.ts

```
import express from 'express'
import { createGatewayMiddleware } from '@rebilder/gateway/node'
import { gatewayConfig } from './gateway-config'   // same GatewayConfig as every adapter

const app = express()
app.use(createGatewayMiddleware(gatewayConfig))    // before your routes
// ... your existing routes serve HTML exactly as before
```

Semantics are `handleRequest`’s, mapped onto the middleware contract:

- **Markdown path** (agent + matching source): status and headers copied from the core response (`text/markdown; charset=utf-8`, `Vary: Accept`, `X-Rebilder-Path: markdown`), body written with `res.end()`. `next()` is not called because the gateway answered. The body is buffered before writing rather than streamed, which is deliberately fine: rendered markdown is capped by `maxBytes` (default 5KB; trymumm runs 8KB), so there is nothing worth streaming.
- **Pass-through** (`null`): `next()` for humans, crawlers, unmatched URLs, and throwing sources; your pipeline runs untouched.
- **Any adapter error** (e.g. an unparseable Host header): `next()`. The gateway never crashes a merchant’s server and never leaves a request hanging.

## How the web-standard Request is built

The web-standard `Request` is built from the Node request: host from the `Host` header; protocol from `x-forwarded-proto` (first value, set by your proxy/LB) → Express’s `req.protocol` → `socket.encrypted`; and Express’s `originalUrl` preferred over `url` (mounted routers rewrite `url`; sources must see the real path). Repeated (array) headers are appended per value.

`toWebRequest(nodeReq, options?)` is exported for reuse if you want core `handleRequest` semantics against a raw Node request yourself, for custom servers and other frameworks.

## Fastify

Two options, no fastify-specific code in the package:

Option A: @fastify/middie

```
// Option A — Fastify's middleware compat layer (@fastify/middie):
import middie from '@fastify/middie'
await fastify.register(middie)
fastify.use(createGatewayMiddleware(gatewayConfig))
```

Option B: onRequest hook

```
// Option B — a 3-line onRequest hook over the raw req/res (no plugin needed):
const gateway = createGatewayMiddleware(gatewayConfig)
fastify.addHook('onRequest', (req, reply, done) => {
  gateway(req.raw, reply.raw, done) // markdown answered on res; otherwise done() continues
})
```

With Option B, Fastify’s routing never sees gateway-answered requests (the response is written on the raw socket), and every pass-through continues through `done()` into your normal routes.