Skip to content

Commit c303f7c

Browse files
committed
Update README
1 parent 1c2477f commit c303f7c

File tree

2 files changed

+205
-76
lines changed

2 files changed

+205
-76
lines changed

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
At a minimum, each package should include:
2+
3+
- `npm test` command for running the tests for just that package
4+
- `npm build` for building output artifacts, including TypeScript definition files and both ESM and CJS builds
5+
- `npm clean` for removing all build artifacts and resetting to a freshly checked out state
6+
7+
Write tests using the `node:test` library and write assertions using `node:assert/strict`.
8+
9+
Prefer using `let` instead of `const` for all local variables. Use `const` for module-scoped variables.

packages/node-fetch-server/README.md

Lines changed: 196 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,216 @@
11
# node-fetch-server
22

3-
`node-fetch-server` allows you to build servers for Node.js that use the [web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) primitives (namely [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)) instead of the traditional `req`/`res` API used in libraries like [Express](https://expressjs.com/).
3+
Build portable Node.js servers using web-standard Fetch API primitives 🚀
44

5-
This web standard API is already used in many places across the JavaScript ecosystem:
5+
`node-fetch-server` brings the simplicity and familiarity of the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to Node.js server development. Instead of dealing with Node's traditional `req`/`res` objects, you work with web-standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects—the same APIs you already use in the browser and modern JavaScript runtimes.
6+
7+
## Why node-fetch-server?
8+
9+
- **Write once, run anywhere**: Your server code becomes portable across Node.js, Deno, Bun, Cloudflare Workers, and other platforms
10+
- **Familiar API**: Use the same Request/Response APIs you already know from client-side development
11+
- **Future-proof**: Align with web standards that are here to stay
12+
- **TypeScript-friendly**: Full type safety with standard web APIs
13+
- **Lightweight**: Minimal overhead while providing a cleaner, more intuitive API
14+
15+
The Fetch API is already the standard for server development in:
616

717
- [`Bun.serve`](https://bun.sh/docs/api/http#bun-serve)
818
- [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/)
919
- [`Deno.serve`](https://docs.deno.com/api/deno/~/Deno.serve)
1020
- [Fastly Compute](https://js-compute-reference-docs.edgecompute.app/docs/)
1121

12-
When you write servers using the `Request` and `Response` APIs, you maximize the chances that your code will be portable across these different JavaScript runtimes.
22+
Now you can use the same pattern in Node.js!
1323

1424
## Features
1525

16-
- Use web standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs for building servers, instead of node-specific API
17-
- Seamless integration with `node:http` and `node:https` modules
18-
- Supports custom hostnames (e.g. using `process.env.HOST` on a VPS to set the host portion of incoming request URLs)
19-
- Supports streaming responses using `new Response(stream)`
20-
- Exposes remote client address info
26+
- ✅ Web-standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs
27+
- ✅ Drop-in integration with `node:http` and `node:https` modules
28+
- ✅ Streaming response support with `ReadableStream`
29+
- ✅ Custom hostname configuration for deployment flexibility
30+
- ✅ Access to client connection info (IP address, port)
31+
- ✅ Full TypeScript support with type definitions
2132

2233
## Installation
2334

2435
```sh
2536
npm install @mjackson/node-fetch-server
2637
```
2738

28-
## Usage
39+
## Quick Start
40+
41+
### Basic Server
2942

3043
```ts
3144
import * as http from 'node:http';
3245
import { createRequestListener } from '@mjackson/node-fetch-server';
3346

34-
function handler(request: Request) {
35-
return new Response('Hello, world!');
47+
// Your request handler uses standard Request/Response objects
48+
async function handler(request: Request) {
49+
let url = new URL(request.url);
50+
51+
// Route based on pathname
52+
if (url.pathname === '/') {
53+
return new Response('Welcome to the home page!');
54+
}
55+
56+
if (url.pathname === '/api/users') {
57+
let users = await getUsers(); // Your async logic here
58+
return Response.json(users);
59+
}
60+
61+
return new Response('Not Found', { status: 404 });
3662
}
3763

64+
// Create a standard Node.js server
3865
let server = http.createServer(createRequestListener(handler));
3966

40-
server.listen(3000);
67+
server.listen(3000, () => {
68+
console.log('Server running at http://localhost:3000');
69+
});
4170
```
4271

43-
By default `request.url` is derived from the value of the `Host` HTTP header and the connection protocol being used. To support custom hostnames using e.g. a `HOST` environment variable, you can use the `host` option:
72+
### Working with Request Data
73+
74+
```ts
75+
async function handler(request: Request) {
76+
// Access request method, headers, and body just like in the browser
77+
if (request.method === 'POST' && request.url.endsWith('/api/users')) {
78+
// Parse JSON body
79+
let userData = await request.json();
80+
81+
// Validate and process...
82+
let newUser = await createUser(userData);
83+
84+
// Return JSON response
85+
return Response.json(newUser, {
86+
status: 201,
87+
headers: { 'Content-Type': 'application/json' },
88+
});
89+
}
90+
91+
return new Response('Method not allowed', { status: 405 });
92+
}
93+
```
94+
95+
### Streaming Responses
96+
97+
Take advantage of web-standard streaming with `ReadableStream`:
98+
99+
```ts
100+
async function handler(request: Request) {
101+
if (request.url.endsWith('/stream')) {
102+
// Create a streaming response
103+
let stream = new ReadableStream({
104+
async start(controller) {
105+
for (let i = 0; i < 5; i++) {
106+
controller.enqueue(new TextEncoder().encode(`Chunk ${i}\n`));
107+
await new Promise((resolve) => setTimeout(resolve, 1000));
108+
}
109+
controller.close();
110+
},
111+
});
112+
113+
return new Response(stream, {
114+
headers: { 'Content-Type': 'text/plain' },
115+
});
116+
}
117+
118+
return new Response('Not Found', { status: 404 });
119+
}
120+
```
121+
122+
### Custom Hostname Configuration
123+
124+
Configure custom hostnames for deployment on VPS or custom environments:
44125

45126
```ts
46-
import * as assert from 'node:assert/strict';
47127
import * as http from 'node:http';
48128
import { createRequestListener } from '@mjackson/node-fetch-server';
49129

50-
function handler(request: Request) {
51-
// This is now true
52-
assert.equal(new URL(request.url).host, process.env.HOST);
53-
return new Response('Hello, world!');
130+
// Use a custom hostname (e.g., from environment variable)
131+
let hostname = process.env.HOST || 'api.example.com';
132+
133+
async function handler(request: Request) {
134+
// request.url will now use your custom hostname
135+
console.log(request.url); // https://api.example.com/path
136+
137+
return Response.json({
138+
message: 'Hello from custom domain!',
139+
url: request.url,
140+
});
54141
}
55142

56-
let server = http.createServer(createRequestListener(handler, { host: process.env.HOST }));
143+
let server = http.createServer(createRequestListener(handler, { host: hostname }));
57144

58145
server.listen(3000);
59146
```
60147

61-
Information about the remote client IP and port is passed as the 2nd argument to your `FetchHandler`:
148+
### Accessing Client Information
149+
150+
Get client connection details (IP address, port) for logging or security:
62151

63152
```ts
64153
import { type FetchHandler } from '@mjackson/node-fetch-server';
65154

66-
let handler: FetchHandler = (request, client) => {
67-
return new Response(`The client IP address is ${client.address}`);
155+
let handler: FetchHandler = async (request, client) => {
156+
// Log client information
157+
console.log(`Request from ${client.address}:${client.port}`);
158+
159+
// Use for rate limiting, geolocation, etc.
160+
if (isRateLimited(client.address)) {
161+
return new Response('Too Many Requests', { status: 429 });
162+
}
163+
164+
return Response.json({
165+
message: 'Hello!',
166+
yourIp: client.address,
167+
});
68168
};
69169
```
70170

71-
## Low-level API
171+
### HTTPS Support
172+
173+
Use with Node.js HTTPS module for secure connections:
174+
175+
```ts
176+
import * as https from 'node:https';
177+
import * as fs from 'node:fs';
178+
import { createRequestListener } from '@mjackson/node-fetch-server';
179+
180+
let options = {
181+
key: fs.readFileSync('private-key.pem'),
182+
cert: fs.readFileSync('certificate.pem'),
183+
};
184+
185+
let server = https.createServer(options, createRequestListener(handler));
186+
187+
server.listen(443, () => {
188+
console.log('HTTPS Server running on port 443');
189+
});
190+
```
72191

73-
In addition to the high-level `createRequestListener()` API, this package also provides 2 low-level APIs that are useful when building custom `fetch`-based servers in Node.js:
192+
## Advanced Usage
74193

75-
- `createRequest(req: http.IncomingMessage, res: http.ServerResponse, options: RequestOptions): Request`
76-
- `sendResponse(res: http.ServerResponse, response: Response): Promise<void>`
194+
### Low-level API
77195

78-
These two functions serve as an efficient, minimal translation layer between Node.js `req`/`res` objects and fetch `Request`/`Response` objects. You could build your own custom server like this:
196+
For more control over request/response handling, use the low-level API:
79197

80198
```ts
81199
import * as http from 'node:http';
82200
import { createRequest, sendResponse } from '@mjackson/node-fetch-server';
83201

84202
let server = http.createServer(async (req, res) => {
203+
// Convert Node.js request to Fetch API Request
85204
let request = createRequest(req, res, { host: process.env.HOST });
86205

87206
try {
88-
let response = await customAppLogic(request);
207+
// Your custom middleware pipeline
208+
let response = await pipeline(authenticate, authorize, handleRequest)(request);
209+
210+
// Convert Fetch API Response back to Node.js response
89211
await sendResponse(res, response);
90212
} catch (error) {
91-
console.error(error);
213+
console.error('Server error:', error);
92214
res.writeHead(500, { 'Content-Type': 'text/plain' });
93215
res.end('Internal Server Error');
94216
}
@@ -97,58 +219,56 @@ let server = http.createServer(async (req, res) => {
97219
server.listen(3000);
98220
```
99221

100-
## Related Packages
222+
The low-level API provides:
101223

102-
- [`fetch-proxy`](https://github.com/mjackson/remix-the-web/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API
224+
- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web Request
225+
- `sendResponse(res, response)` - Sends web Response using Node.js ServerResponse
103226

104-
## Benchmark
227+
This is useful for:
105228

106-
A basic "hello world" benchmark shows `node-fetch-server` introduces considerable overhead on top of a vanilla `node:http` server. However, it is still able to serve more requests per second (and has higher overall throughput) than Express v4, so the slowdown should be acceptable for most applications.
229+
- Building custom middleware systems
230+
- Integrating with existing Node.js code
231+
- Implementing custom error handling
232+
- Performance-critical applications
107233

234+
## Migration from Express
235+
236+
Transitioning from Express? Here's a quick comparison:
237+
238+
```ts
239+
// Express way
240+
app.get('/users/:id', async (req, res) => {
241+
let user = await getUser(req.params.id);
242+
res.json(user);
243+
});
244+
245+
// node-fetch-server way
246+
async function handler(request: Request) {
247+
let url = new URL(request.url);
248+
let match = url.pathname.match(/^\/users\/(\w+)$/);
249+
250+
if (match) {
251+
let user = await getUser(match[1]);
252+
return Response.json(user);
253+
}
254+
255+
return new Response('Not Found', { status: 404 });
256+
}
108257
```
109-
> @mjackson/[email protected] bench /Users/michael/Projects/remix-the-web/packages/node-fetch-server
110-
> bash ./bench/runner.sh
111-
112-
Platform: Darwin (24.0.0)
113-
CPU: Apple M1 Pro
114-
Date: 11/14/2024, 2:30:22 PM
115-
116-
Running benchmark for node:[email protected] ...
117-
118-
Running 30s test @ http://127.0.0.1:3000/
119-
12 threads and 400 connections
120-
Thread Stats Avg Stdev Max +/- Stdev
121-
Latency 9.97ms 31.92ms 786.67ms 99.09%
122-
Req/Sec 4.45k 268.33 6.38k 93.69%
123-
1594257 requests in 30.02s, 326.89MB read
124-
Socket errors: connect 0, read 1317, write 6, timeout 0
125-
Requests/sec: 53110.92
126-
Transfer/sec: 10.89MB
127-
128-
Running benchmark for [email protected] ...
129-
130-
Running 30s test @ http://127.0.0.1:3000/
131-
12 threads and 400 connections
132-
Thread Stats Avg Stdev Max +/- Stdev
133-
Latency 22.74ms 81.06ms 1.46s 98.22%
134-
Req/Sec 2.42k 185.82 4.30k 91.80%
135-
866347 requests in 30.03s, 177.64MB read
136-
Socket errors: connect 0, read 1496, write 3, timeout 0
137-
Requests/sec: 28849.46
138-
Transfer/sec: 5.92MB
139-
140-
Running benchmark for [email protected] ...
141-
142-
Running 30s test @ http://127.0.0.1:3000/
143-
12 threads and 400 connections
144-
Thread Stats Avg Stdev Max +/- Stdev
145-
Latency 36.46ms 125.89ms 1.99s 97.89%
146-
Req/Sec 1.56k 146.86 2.93k 88.25%
147-
558504 requests in 30.06s, 134.76MB read
148-
Socket errors: connect 0, read 1261, write 11, timeout 36
149-
Requests/sec: 18579.11
150-
Transfer/sec: 4.48MB
151-
```
258+
259+
Common patterns:
260+
261+
- `req.body``await request.json()`
262+
- `req.params` → Parse from `new URL(request.url).pathname`
263+
- `req.query``new URL(request.url).searchParams`
264+
- `res.json(data)``Response.json(data)`
265+
- `res.status(404).send()``new Response('', { status: 404 })`
266+
- `res.redirect()``new Response(null, { status: 302, headers: { Location: '/path' } })`
267+
268+
## Related Packages
269+
270+
- [`fetch-proxy`](https://github.com/mjackson/remix-the-web/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API
271+
- [`fetch-router`](https://github.com/mjackson/remix-the-web/tree/main/packages/fetch-router) - URL pattern routing for Fetch API servers
152272

153273
## License
154274

0 commit comments

Comments
 (0)