Guide

Node.js support


This is an advanced section, explaining internal mechanism of srvx for Node.js support.

As explained in the why srvx section, Node.js uses different API for handling incoming http requests and sending responses.

srvx internally uses a lightweight proxy system that wraps node:IncomingMessage as Request and converts the final state of node:ServerResponse to a Response object.

How Node.js compatibility works

For each incoming request, instead of fully cloning Node.js request object with into a new Request instance, srvx creates a proxy that translates all property access and method calls between two interfaces.

With this method, we add minimum amount of overhead and can optimize internal implementation to leverage most of the possibilities with Node.js native primitives. This method also has the advantage that there is only one source of trust (Node.js request instance) and any changes to each interface will reflect the other (node:IncomingMessage <> Request), maximizing compatibility. srvx will never patch of modify the global Request and Response constructors, keeping runtime natives untouched.

Internally, the fetch wrapper looks like this:

function nodeHandler(nodeReq: IncomingMessage, nodeRes: ServerResponse) {
  const request = new NodeRequestProxy(nodeReq);
  const response = await server.fetch(request);
  await sendNodeResponse(nodeRes, response);
}

... NodeRequestProxy, wraps node:IncomingMessage as a standard Request interface.
... On first request.body access, it starts reading request body as a ReadableStream.
... request.headers is a proxy (NodeReqHeadersProxy) around nodeReq.headers providing a standard Headers interface.
... When accessing request.url getter, it creates a full URL string (including protocol, hostname and path) from nodeReq.url and nodeReq.headers (host).
... Other request APIs are also implemented similarly.

sendNodeResponse, handles the Response object returned from server fetch method.

... status, statusText, and headers will be set.
... set-cookie header will be properly split (with cookie-es).
... If response has body, it will be streamed to node response.
... The promise will be resolved after the response is sent and callback called by Node.js.

Fast response

When initializing a new Response in Node.js, a lot of extra internals have to be initialized including a ReadableStream object for response.body and Headers for response.headers which adds significant overhead since Node.js response handling does not need them.

Until there will be native Response handling support in Node.js http module, srvx provides a faster class NodeFastResponse. You can use this instead to replace Response and improve performance.

Note: we will never, ever patch global Response and advice everyone to avoid doing this, please!

import { serve } from "srvx";
import { NodeFastResponse } from "srvx/node-utils";

const server = serve({
  port: 3000,
  fetch() {
    return new NodeFastResponse("Hello!");
  },
});

await server.ready();

console.log(`Server running at ${server.url}`);

You can locally run benchmarks by cloning srvx repository and running npm run bench:node [--all] script. Speedup in v22.8.0 was roughly %94!

Reverse compatibility

srvx converts a fetch-like Request => Response handler to node:IncomingMessage => node:ServerResponse handler that is compatible with Node.js runtime.

If you want to instead convert a Node.js server handler (like Express) with (req, IncomingMessage, res: ServerResponse) signature to fetch-like handler (Request => Response) that can work without Node.js runtime you can instead use unjs/unenv Node.js compatibility layer to emulate Node.js API.

// unenv does not have any Node.js dependency
import { createFetch, createCall } from "unenv/runtime/fetch/index";
const nodeHandler = async (
  req /* IncomingMessage */,
  res /* ServerResponse */,
) => {
  res.end(JSON.stringify({ "req.url": req.url, "req.headers": req.headers }));
};
const serverFetch = createFetch(createCall(nodeHandler));
const res = await serverFetch("/test", { headers: { foo: "bar" } });
console.log(res);