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);
import { toReqRes, toFetchResponse } from "fetch-to-node";
// Node.js compatible request handler
const nodeHandler = (req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
data: "Hello World!",
}),
);
};
// Create Node.js-compatible req and res from request
const { req, res } = toReqRes(webRequest);
// Create a Response object based on res, and return it
const webResponse = await toFetchResponse(res);