Node.js Support

Learn more about Node.js compatibility with srvx.
This is an advanced section, explaining internal mechanism of srvx for Node.js support.

When you want to create a HTTP server using Node.js, you have to use node:http builtin.

Example: Simple Node.js server.

import { createServer } from "node:http";

createServer((req, res) => {
  res.end("Hello, Node.js!");
}).listen(3000);

Whenever a new request is received, the request event is called with two objects: a request req object (node:IncomingMessage) to access HTTP request details and a response res object (node:ServerResponse) that can be used to prepare and send a HTTP response. Popular framework such as Express and Fastify are also based on Node.js server API.

Recent JavaScript server runtimes like Deno and Bun have a different way to define a server which is similar to web fetch API.

Example: Deno HTTP server (learn more):

Deno.serve({ port: 3000 }, (_req, info) => new Response("Hello, Deno!"));

Example: Bun HTTP server (learn more):

Bun.serve({ port: 3000, fetch: (req) => new Response("Hello, Bun!") });

As you probably noticed, there is a difference between Node.js and Deno and Bun. The incoming request is a web Request object and server response is a web Response object. Accessing headers, request path, and preparing response is completely different between Node.js and other runtimes.

While Deno and Bun servers are both based on web standards, There are differences between them. The way to provide options, server lifecycle, access to request info such as client IP which is not part of Request standard are some examples.

How Node.js Compatibility Works

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.

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.

FastResponse

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 alternative Response class. You can use this instead to replace Response and improve performance.

import { serve, FastResponse } from "srvx";

const server = serve({
  port: 3000,
  fetch() {
    return new FastResponse("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 node-mock-http or fetch-to-node (more mature but currently requires some node: polyfills).

import { fetchNodeRequestHandler } from "node-mock-http";

// Node.js compatible request handler
const nodeHandler = (req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(
    JSON.stringify({
      data: "Hello World!",
    }),
  );
};

// Create a Response object
const webResponse = await fetchNodeRequestHandler(nodeHandler, webRequest);