zero-http 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -115
- package/documentation/full-server.js +82 -1
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +109 -17
- package/documentation/public/scripts/data-sections.js +4 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +201 -0
- package/documentation/public/scripts/ui.js +6 -6
- package/documentation/public/scripts/uploads.js +9 -9
- package/documentation/public/styles.css +12 -12
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +23 -0
- package/documentation/public/vendor/icons/router.svg +29 -0
- package/documentation/public/vendor/icons/sse.svg +25 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +145 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /package/lib/{static.js → middleware/static.js} +0 -0
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
* Organized with controllers and JSDoc comments for clarity and maintainability.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// --- Crash protection ---
|
|
7
|
+
process.on('uncaughtException', (err) => { console.error('[UNCAUGHT]', err); });
|
|
8
|
+
process.on('unhandledRejection', (err) => { console.error('[UNHANDLED]', err); });
|
|
9
|
+
|
|
6
10
|
const path = require('path');
|
|
7
11
|
const fs = require('fs');
|
|
8
12
|
const os = require('os');
|
|
9
|
-
const { createApp, cors, json, urlencoded, text, raw, multipart, static: serveStatic, fetch, logger } = require('..');
|
|
13
|
+
const { createApp, cors, json, urlencoded, text, raw, multipart, static: serveStatic, fetch, logger, compress, Router } = require('..');
|
|
10
14
|
|
|
11
15
|
// --- App Initialization ---
|
|
12
16
|
const app = createApp();
|
|
13
17
|
app.use(logger({ format: 'dev' }));
|
|
14
18
|
app.use(cors());
|
|
19
|
+
app.use(compress());
|
|
15
20
|
app.use(json());
|
|
16
21
|
app.use(urlencoded());
|
|
17
22
|
app.use(text());
|
|
@@ -94,6 +99,82 @@ const proxyController = require('./controllers/proxy');
|
|
|
94
99
|
const proxyFetch = (typeof globalThis !== 'undefined' && globalThis.fetch) || fetch;
|
|
95
100
|
app.get('/proxy', proxyController.proxy(proxyFetch));
|
|
96
101
|
|
|
102
|
+
// --- WebSocket Chat ---
|
|
103
|
+
const wsClients = new Set();
|
|
104
|
+
|
|
105
|
+
app.ws('/ws/chat', { maxPayload: 64 * 1024, pingInterval: 25000 }, (ws, req) =>
|
|
106
|
+
{
|
|
107
|
+
ws.data.name = ws.query.name || 'anon';
|
|
108
|
+
wsClients.add(ws);
|
|
109
|
+
ws.send(JSON.stringify({ type: 'system', text: 'Welcome, ' + ws.data.name + '!' }));
|
|
110
|
+
|
|
111
|
+
// Broadcast join
|
|
112
|
+
for (const c of wsClients)
|
|
113
|
+
{
|
|
114
|
+
if (c !== ws && c.readyState === 1)
|
|
115
|
+
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' joined' }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ws.on('message', (msg) =>
|
|
119
|
+
{
|
|
120
|
+
// Broadcast to all clients including sender
|
|
121
|
+
const payload = JSON.stringify({ type: 'message', name: ws.data.name, text: String(msg) });
|
|
122
|
+
for (const c of wsClients)
|
|
123
|
+
{
|
|
124
|
+
if (c.readyState === 1) c.send(payload);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
ws.on('close', () =>
|
|
129
|
+
{
|
|
130
|
+
wsClients.delete(ws);
|
|
131
|
+
for (const c of wsClients)
|
|
132
|
+
{
|
|
133
|
+
if (c.readyState === 1)
|
|
134
|
+
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' left' }));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- Server-Sent Events ---
|
|
140
|
+
const sseClients = new Set();
|
|
141
|
+
|
|
142
|
+
app.get('/sse/events', (req, res) =>
|
|
143
|
+
{
|
|
144
|
+
const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 });
|
|
145
|
+
sseClients.add(sse);
|
|
146
|
+
sse.send({ type: 'connected', clients: sseClients.size });
|
|
147
|
+
|
|
148
|
+
sse.on('close', () => sseClients.delete(sse));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.post('/sse/broadcast', (req, res) =>
|
|
152
|
+
{
|
|
153
|
+
const data = req.body || {};
|
|
154
|
+
for (const sse of sseClients)
|
|
155
|
+
{
|
|
156
|
+
sse.event('broadcast', data);
|
|
157
|
+
}
|
|
158
|
+
res.json({ sent: sseClients.size });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// --- Router Demo (API sub-app) ---
|
|
162
|
+
const apiRouter = Router();
|
|
163
|
+
|
|
164
|
+
apiRouter.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
|
|
165
|
+
apiRouter.get('/info', (req, res) => res.json({
|
|
166
|
+
secure: req.secure,
|
|
167
|
+
protocol: req.protocol,
|
|
168
|
+
ip: req.ip,
|
|
169
|
+
method: req.method,
|
|
170
|
+
url: req.url
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
app.use('/api', apiRouter);
|
|
174
|
+
|
|
175
|
+
// --- Route introspection ---
|
|
176
|
+
app.get('/debug/routes', (req, res) => res.json(app.routes()));
|
|
177
|
+
|
|
97
178
|
// --- Server Startup ---
|
|
98
179
|
const port = process.env.PORT || 3000;
|
|
99
180
|
const server = app.listen(port, () =>
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "createApp()",
|
|
4
|
-
"description": "Returns an application instance with Express-like routing, middleware support, error handling, and an HTTP server.",
|
|
5
|
-
"example": "const { createApp, json, cors, logger,
|
|
4
|
+
"description": "Returns an application instance with Express-like routing, middleware support, WebSocket handling, error handling, and an HTTP/HTTPS server.",
|
|
5
|
+
"example": "const { createApp, json, cors, logger, compress,\n\tstatic: serveStatic } = require('zero-http')\nconst app = createApp()\n\napp.use(logger({ format: 'dev' }))\napp.use(cors())\napp.use(compress())\napp.use(json({ limit: '10kb' }))\napp.use(serveStatic(path.join(__dirname, 'public')))\n\napp.get('/', (req, res) => res.json({ hello: 'world' }))\n\napp.ws('/chat', (ws, req) => {\n\tws.on('message', msg => ws.send('echo: ' + msg))\n})\n\napp.onError((err, req, res, next) => {\n\tconsole.error(err)\n\tres.status(500).json({ error: err.message })\n})\n\n// HTTP\napp.listen(3000, () => console.log('listening on :3000'))\n\n// HTTPS\napp.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') })",
|
|
6
6
|
"methods": [
|
|
7
7
|
{ "method": "use", "signature": "use(fn)", "description": "Register global middleware; fn(req, res, next)." },
|
|
8
|
-
{ "method": "use", "signature": "use(prefix, fn)", "description": "Register path-scoped middleware. The prefix is stripped from req.url
|
|
9
|
-
{ "method": "get", "signature": "get(path, ...handlers)", "description": "Register GET route handlers." },
|
|
10
|
-
{ "method": "post", "signature": "post(path, ...handlers)", "description": "Register POST route handlers." },
|
|
11
|
-
{ "method": "put", "signature": "put(path, ...handlers)", "description": "Register PUT route handlers." },
|
|
12
|
-
{ "method": "delete", "signature": "delete(path, ...handlers)", "description": "Register DELETE route handlers." },
|
|
13
|
-
{ "method": "patch", "signature": "patch(path, ...handlers)", "description": "Register PATCH route handlers." },
|
|
14
|
-
{ "method": "options", "signature": "options(path, ...handlers)", "description": "Register OPTIONS route handlers." },
|
|
15
|
-
{ "method": "head", "signature": "head(path, ...handlers)", "description": "Register HEAD route handlers." },
|
|
16
|
-
{ "method": "all", "signature": "all(path, ...handlers)", "description": "Register handlers for ALL HTTP methods." },
|
|
8
|
+
{ "method": "use", "signature": "use(prefix, fn|router)", "description": "Register path-scoped middleware or mount a Router sub-app. The prefix is stripped from req.url." },
|
|
9
|
+
{ "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register GET route handlers. Optional opts object for { secure } matching." },
|
|
10
|
+
{ "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register POST route handlers." },
|
|
11
|
+
{ "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register PUT route handlers." },
|
|
12
|
+
{ "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register DELETE route handlers." },
|
|
13
|
+
{ "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register PATCH route handlers." },
|
|
14
|
+
{ "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register OPTIONS route handlers." },
|
|
15
|
+
{ "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register HEAD route handlers." },
|
|
16
|
+
{ "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register handlers for ALL HTTP methods." },
|
|
17
|
+
{ "method": "ws", "signature": "ws(path, [opts], handler)", "description": "Register a WebSocket upgrade handler. handler receives (ws, req)." },
|
|
17
18
|
{ "method": "onError", "signature": "onError(fn)", "description": "Register a global error handler: fn(err, req, res, next). Only one at a time." },
|
|
18
|
-
{ "method": "listen", "signature": "listen(port
|
|
19
|
+
{ "method": "listen", "signature": "listen(port, [tlsOpts], [cb])", "description": "Start HTTP server, or HTTPS if tlsOpts includes { key, cert }. Returns the server instance." },
|
|
20
|
+
{ "method": "close", "signature": "close([cb])", "description": "Gracefully close the server and invoke the optional callback." },
|
|
21
|
+
{ "method": "routes", "signature": "routes()", "description": "Return a flat array of all registered routes for introspection." },
|
|
19
22
|
{ "method": "handler", "signature": "(property)", "description": "Bound request handler suitable for http.createServer(app.handler)." }
|
|
20
23
|
],
|
|
21
24
|
"options": []
|
|
22
25
|
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Router()",
|
|
28
|
+
"description": "Creates a standalone router for modular route grouping. Supports all HTTP method helpers, route chaining via .route(), sub-router mounting via .use(), protocol-aware { secure } matching, and introspection via .routes().",
|
|
29
|
+
"example": "const { createApp, Router, json } = require('zero-http')\nconst app = createApp()\nconst api = Router()\n\napi.use(json())\napi.get('/users', (req, res) => res.json([]))\napi.get('/users/:id', (req, res) => res.json({ id: req.params.id }))\n\n// Route chaining\napi.route('/items')\n\t.get((req, res) => res.json([]))\n\t.post((req, res) => res.status(201).json(req.body))\n\n// HTTPS-only route\napi.get('/secret', { secure: true }, (req, res) => res.json({ classified: true }))\n\napp.use('/api', api)\napp.listen(3000)\n\nconsole.log(app.routes())\n// [{ method:'GET', path:'/api/users' }, { method:'GET', path:'/api/users/:id' }, ...]",
|
|
30
|
+
"methods": [
|
|
31
|
+
{ "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register GET route. Optional { secure } option." },
|
|
32
|
+
{ "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register POST route." },
|
|
33
|
+
{ "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register PUT route." },
|
|
34
|
+
{ "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register DELETE route." },
|
|
35
|
+
{ "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register PATCH route." },
|
|
36
|
+
{ "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register OPTIONS route." },
|
|
37
|
+
{ "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register HEAD route." },
|
|
38
|
+
{ "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register route for all HTTP methods." },
|
|
39
|
+
{ "method": "use", "signature": "use(fn) | use(prefix, fn|router)", "description": "Register middleware or mount a nested sub-router." },
|
|
40
|
+
{ "method": "route", "signature": "route(path)", "description": "Return a chainable object with .get(), .post(), etc. for a single path." },
|
|
41
|
+
{ "method": "routes", "signature": "routes()", "description": "Return an array of all registered routes with method, path, and secure flag." }
|
|
42
|
+
],
|
|
43
|
+
"options": []
|
|
44
|
+
},
|
|
23
45
|
{
|
|
24
46
|
"name": "Request (req)",
|
|
25
|
-
"description": "Wraps the incoming Node http.IncomingMessage with convenient properties and helpers.
|
|
26
|
-
"example": "app.get('/info', (req, res) => {\n\tconsole.log(req.method) // 'GET'\n\tconsole.log(req.url) // '/info?foo=bar'\n\tconsole.log(req.query) // { foo: 'bar' }\n\tconsole.log(req.ip) // '127.0.0.1'\n\tconsole.log(req.get('host')) // 'localhost:3000'\n\tconsole.log(req.is('json')) // false\n\tres.json({ ok: true })\n})",
|
|
47
|
+
"description": "Wraps the incoming Node http.IncomingMessage with convenient properties and helpers. Includes HTTPS detection via req.secure and req.protocol.",
|
|
48
|
+
"example": "app.get('/info', (req, res) => {\n\tconsole.log(req.method) // 'GET'\n\tconsole.log(req.url) // '/info?foo=bar'\n\tconsole.log(req.query) // { foo: 'bar' }\n\tconsole.log(req.ip) // '127.0.0.1'\n\tconsole.log(req.secure) // true/false\n\tconsole.log(req.protocol) // 'https' or 'http'\n\tconsole.log(req.get('host')) // 'localhost:3000'\n\tconsole.log(req.is('json')) // false\n\tres.json({ ok: true })\n})",
|
|
27
49
|
"methods": [
|
|
28
50
|
{ "method": "method", "signature": "(string)", "description": "HTTP method (GET, POST, etc.)." },
|
|
29
51
|
{ "method": "url", "signature": "(string)", "description": "Request URL path + query string." },
|
|
@@ -32,6 +54,8 @@
|
|
|
32
54
|
{ "method": "params", "signature": "(object)", "description": "Route parameters populated by the router (e.g. { id: '42' })." },
|
|
33
55
|
{ "method": "body", "signature": "(any)", "description": "Parsed request body (null until a body parser runs)." },
|
|
34
56
|
{ "method": "ip", "signature": "(string|null)", "description": "Remote IP address from req.socket.remoteAddress." },
|
|
57
|
+
{ "method": "secure", "signature": "(boolean)", "description": "true when the connection is over TLS (HTTPS)." },
|
|
58
|
+
{ "method": "protocol", "signature": "(string)", "description": "'https' or 'http' based on connection type." },
|
|
35
59
|
{ "method": "raw", "signature": "(object)", "description": "The underlying http.IncomingMessage." },
|
|
36
60
|
{ "method": "get", "signature": "get(name)", "description": "Get a request header (case-insensitive)." },
|
|
37
61
|
{ "method": "is", "signature": "is(type)", "description": "Check if Content-Type contains the given type string (e.g. 'json', 'text/html')." }
|
|
@@ -40,8 +64,8 @@
|
|
|
40
64
|
},
|
|
41
65
|
{
|
|
42
66
|
"name": "Response (res)",
|
|
43
|
-
"description": "Wraps the outgoing Node http.ServerResponse with chainable helpers for setting status, headers, and
|
|
44
|
-
"example": "app.get('/demo', (req, res) => {\n\tres.status(200)\n\t .set('X-Custom', 'hello')\n\t .type('json')\n\t .json({ message: 'ok' })\n})\n\napp.get('/page', (req, res) => res.html('<h1>Hello</h1>'))\napp.get('/go', (req, res) => res.redirect(301, '/new-location'))",
|
|
67
|
+
"description": "Wraps the outgoing Node http.ServerResponse with chainable helpers for setting status, headers, sending responses, and opening an SSE stream.",
|
|
68
|
+
"example": "app.get('/demo', (req, res) => {\n\tres.status(200)\n\t .set('X-Custom', 'hello')\n\t .type('json')\n\t .json({ message: 'ok' })\n})\n\napp.get('/page', (req, res) => res.html('<h1>Hello</h1>'))\napp.get('/go', (req, res) => res.redirect(301, '/new-location'))\n\n// Open an SSE stream\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({ retry: 5000, autoId: true })\n\tsse.send('hello')\n})",
|
|
45
69
|
"methods": [
|
|
46
70
|
{ "method": "status", "signature": "status(code)", "description": "Set HTTP status code. Returns this (chainable)." },
|
|
47
71
|
{ "method": "set", "signature": "set(name, value)", "description": "Set a response header. Returns this (chainable)." },
|
|
@@ -51,57 +75,149 @@
|
|
|
51
75
|
{ "method": "json", "signature": "json(obj)", "description": "Set Content-Type to application/json and send the object." },
|
|
52
76
|
{ "method": "text", "signature": "text(str)", "description": "Set Content-Type to text/plain and send the string." },
|
|
53
77
|
{ "method": "html", "signature": "html(str)", "description": "Set Content-Type to text/html and send the string." },
|
|
54
|
-
{ "method": "redirect", "signature": "redirect([status], url)", "description": "Redirect to URL. Default status is 302." }
|
|
78
|
+
{ "method": "redirect", "signature": "redirect([status], url)", "description": "Redirect to URL. Default status is 302." },
|
|
79
|
+
{ "method": "sse", "signature": "sse([opts])", "description": "Open a Server-Sent Events stream. Returns an SSEStream controller with send(), event(), comment(), close(), etc." }
|
|
80
|
+
],
|
|
81
|
+
"options": []
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "app.ws(path, [opts], handler)",
|
|
85
|
+
"description": "Register a WebSocket upgrade handler on the application. Uses RFC 6455 framing with built-in ping/pong, sub-protocol negotiation, and client verification. The handler receives (ws, req) where ws is a WebSocketConnection with a rich event-driven API.",
|
|
86
|
+
"example": "const { createApp } = require('zero-http')\nconst app = createApp()\n\nconst clients = new Set()\n\napp.ws('/chat', {\n\tmaxPayload: 64 * 1024,\n\tpingInterval: 20000,\n\tverifyClient: (req) => req.headers['x-api-key'] === 'secret'\n}, (ws, req) => {\n\tclients.add(ws)\n\tws.data.name = req.query.name || 'anon'\n\tws.send('Welcome, ' + ws.data.name + '!')\n\n\tws.on('message', msg => {\n\t\tfor (const c of clients) {\n\t\t\tif (c !== ws && c.readyState === 1)\n\t\t\t\tc.send(ws.data.name + ': ' + msg)\n\t\t}\n\t})\n\n\tws.on('close', () => clients.delete(ws))\n})\n\napp.listen(3000)",
|
|
87
|
+
"methods": [
|
|
88
|
+
{ "method": "ws.send", "signature": "send(data, [opts])", "description": "Send a text or binary message. opts.binary forces a binary frame." },
|
|
89
|
+
{ "method": "ws.sendJSON", "signature": "sendJSON(obj)", "description": "JSON-serialize and send as a text frame." },
|
|
90
|
+
{ "method": "ws.ping", "signature": "ping([payload])", "description": "Send a ping frame (auto-pings are built-in via pingInterval)." },
|
|
91
|
+
{ "method": "ws.pong", "signature": "pong([payload])", "description": "Send a pong frame." },
|
|
92
|
+
{ "method": "ws.close", "signature": "close([code], [reason])", "description": "Initiate a graceful close with optional status code and reason." },
|
|
93
|
+
{ "method": "ws.terminate", "signature": "terminate()", "description": "Forcefully destroy the underlying socket." },
|
|
94
|
+
{ "method": "ws.on", "signature": "on(event, fn)", "description": "Listen for events: 'message', 'close', 'error', 'ping', 'pong', 'drain'." },
|
|
95
|
+
{ "method": "ws.once", "signature": "once(event, fn)", "description": "One-time event listener." },
|
|
96
|
+
{ "method": "ws.off", "signature": "off(event, fn)", "description": "Remove a specific event listener." },
|
|
97
|
+
{ "method": "ws.removeAllListeners", "signature": "removeAllListeners([event])", "description": "Remove all listeners for an event, or all events." },
|
|
98
|
+
{ "method": "ws.listenerCount", "signature": "listenerCount(event)", "description": "Return the number of listeners for an event." }
|
|
99
|
+
],
|
|
100
|
+
"options": [
|
|
101
|
+
{ "option": "maxPayload", "type": "number", "default": "1048576 (1 MB)", "notes": "Maximum incoming frame size in bytes. Frames exceeding this close the connection with 1009." },
|
|
102
|
+
{ "option": "pingInterval", "type": "number", "default": "30000", "notes": "Auto-ping interval in ms. Set to 0 to disable." },
|
|
103
|
+
{ "option": "verifyClient", "type": "function", "default": "—", "notes": "fn(req) → boolean. Return false to reject the upgrade with 403 Forbidden." }
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "WebSocketConnection",
|
|
108
|
+
"description": "Represents a single WebSocket connection. Created automatically when a client upgrades to WebSocket at a registered ws() path. Provides an event-driven API with per-connection data store, connection metadata, and binary/text messaging.",
|
|
109
|
+
"example": "// Properties available on each ws connection:\napp.ws('/demo', (ws, req) => {\n\tconsole.log(ws.id) // 'ws_1_a3x9k'\n\tconsole.log(ws.readyState) // 1 (OPEN)\n\tconsole.log(ws.protocol) // negotiated sub-protocol\n\tconsole.log(ws.headers) // upgrade request headers\n\tconsole.log(ws.ip) // remote IP\n\tconsole.log(ws.query) // parsed query params\n\tconsole.log(ws.url) // upgrade URL\n\tconsole.log(ws.secure) // true for wss://\n\tconsole.log(ws.connectedAt) // timestamp of connection\n\tconsole.log(ws.uptime) // ms since connected\n\tconsole.log(ws.bufferedAmount) // pending bytes\n\n\t// Per-connection data store\n\tws.data.role = 'admin'\n\tif (ws.data.role === 'admin') ws.send('admin tools unlocked')\n})",
|
|
110
|
+
"methods": [
|
|
111
|
+
{ "method": "id", "signature": "(string)", "description": "Unique connection identifier (e.g. ws_1_l8x3k)." },
|
|
112
|
+
{ "method": "readyState", "signature": "(number)", "description": "0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED (mirrors the WebSocket spec)." },
|
|
113
|
+
{ "method": "protocol", "signature": "(string)", "description": "Negotiated sub-protocol from the Sec-WebSocket-Protocol handshake." },
|
|
114
|
+
{ "method": "headers", "signature": "(object)", "description": "Headers from the initial HTTP upgrade request." },
|
|
115
|
+
{ "method": "ip", "signature": "(string)", "description": "Remote IP address of the WebSocket client." },
|
|
116
|
+
{ "method": "query", "signature": "(object)", "description": "Parsed query parameters from the upgrade URL." },
|
|
117
|
+
{ "method": "url", "signature": "(string)", "description": "Full upgrade request URL." },
|
|
118
|
+
{ "method": "secure", "signature": "(boolean)", "description": "true when the connection is over TLS (WSS)." },
|
|
119
|
+
{ "method": "extensions", "signature": "(string)", "description": "Requested WebSocket extensions header from the client." },
|
|
120
|
+
{ "method": "maxPayload", "signature": "(number)", "description": "Maximum payload size in bytes for this connection." },
|
|
121
|
+
{ "method": "connectedAt", "signature": "(number)", "description": "Timestamp (ms) when the connection was established." },
|
|
122
|
+
{ "method": "uptime", "signature": "(number)", "description": "Milliseconds since the connection opened (computed getter)." },
|
|
123
|
+
{ "method": "bufferedAmount", "signature": "(number)", "description": "Number of bytes queued for transmission." },
|
|
124
|
+
{ "method": "data", "signature": "(object)", "description": "Arbitrary per-connection data store. Use ws.data.key = value for user data." }
|
|
55
125
|
],
|
|
56
126
|
"options": []
|
|
57
127
|
},
|
|
128
|
+
{
|
|
129
|
+
"name": "res.sse([opts]) / SSEStream",
|
|
130
|
+
"description": "Opens a Server-Sent Events stream from a route handler. Returns an SSEStream controller with methods for sending events, comments, and controlling the stream lifecycle. Supports auto-incrementing IDs, keep-alive, retry hints, and per-stream data store.",
|
|
131
|
+
"example": "app.get('/events', (req, res) => {\n\tconst sse = res.sse({\n\t\tstatus: 200,\n\t\tretry: 5000,\n\t\tkeepAlive: 30000,\n\t\tkeepAliveComment: 'ping',\n\t\tautoId: true,\n\t\tstartId: 1,\n\t\tpad: 2048,\n\t\theaders: { 'X-Stream': 'live' }\n\t})\n\n\t// Send unnamed data event\n\tsse.send('hello')\n\n\t// Send named event with object data\n\tsse.event('update', { x: 1, y: 2 })\n\n\t// Props\n\tconsole.log(sse.connected) // true\n\tconsole.log(sse.eventCount) // 2\n\tconsole.log(sse.bytesSent) // bytes written\n\tconsole.log(sse.uptime) // ms since opened\n\tconsole.log(sse.secure) // true for HTTPS\n\tconsole.log(sse.data) // {} user store\n\n\t// Periodic events\n\tconst iv = setInterval(() => sse.event('tick', Date.now()), 1000)\n\tsse.on('close', () => clearInterval(iv))\n})",
|
|
132
|
+
"methods": [
|
|
133
|
+
{ "method": "send", "signature": "send(data, [id])", "description": "Send unnamed data event. Objects are auto-JSON-serialized." },
|
|
134
|
+
{ "method": "sendJSON", "signature": "sendJSON(obj, [id])", "description": "Alias for send() with an object." },
|
|
135
|
+
{ "method": "event", "signature": "event(name, data, [id])", "description": "Send a named event (event: name\\ndata: ...)." },
|
|
136
|
+
{ "method": "comment", "signature": "comment(text)", "description": "Send a comment line (: text). Useful for keep-alive or debugging." },
|
|
137
|
+
{ "method": "retry", "signature": "retry(ms)", "description": "Send a retry: ms hint to set the client's reconnection interval." },
|
|
138
|
+
{ "method": "keepAlive", "signature": "keepAlive(ms, [comment])", "description": "Start or restart a keep-alive timer that sends periodic comments." },
|
|
139
|
+
{ "method": "flush", "signature": "flush()", "description": "Flush buffered data through proxies (writes 2 KB padding)." },
|
|
140
|
+
{ "method": "close", "signature": "close()", "description": "Close the SSE stream from the server side." },
|
|
141
|
+
{ "method": "on", "signature": "on(event, fn)", "description": "Listen for 'close' or 'error' events." },
|
|
142
|
+
{ "method": "once", "signature": "once(event, fn)", "description": "One-time event listener." },
|
|
143
|
+
{ "method": "off", "signature": "off(event, fn)", "description": "Remove a specific event listener." },
|
|
144
|
+
{ "method": "removeAllListeners", "signature": "removeAllListeners([event])", "description": "Remove all listeners for an event, or all events." },
|
|
145
|
+
{ "method": "listenerCount", "signature": "listenerCount(event)", "description": "Return the number of listeners for an event." }
|
|
146
|
+
],
|
|
147
|
+
"options": [
|
|
148
|
+
{ "option": "retry", "type": "number", "default": "—", "notes": "Reconnection interval hint (ms) sent to the client on stream open." },
|
|
149
|
+
{ "option": "keepAlive", "type": "number", "default": "0", "notes": "Auto keep-alive comment interval in ms. 0 = disabled." },
|
|
150
|
+
{ "option": "keepAliveComment", "type": "string", "default": "ping", "notes": "Comment text sent by the keep-alive timer." },
|
|
151
|
+
{ "option": "autoId", "type": "boolean", "default": "false", "notes": "Auto-increment event IDs starting from startId." },
|
|
152
|
+
{ "option": "startId", "type": "number", "default": "1", "notes": "Starting value for auto-generated IDs." },
|
|
153
|
+
{ "option": "pad", "type": "number", "default": "0", "notes": "Bytes of initial padding (helps flush proxy buffers on connect)." },
|
|
154
|
+
{ "option": "status", "type": "number", "default": "200", "notes": "HTTP status code for the SSE response." },
|
|
155
|
+
{ "option": "headers", "type": "object", "default": "—", "notes": "Additional response headers merged into the SSE response." }
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "compress([opts])",
|
|
160
|
+
"description": "Response compression middleware using brotli, gzip, or deflate based on the client's Accept-Encoding header. Brotli is preferred when available (Node \u2265 11.7). Automatically negotiates encoding, respects threshold, skips SSE streams, and supports a filter function to skip specific requests.",
|
|
161
|
+
"example": "const { createApp, compress } = require('zero-http')\nconst app = createApp()\n\n// Compress responses over 512 bytes (brotli > gzip > deflate)\napp.use(compress({ threshold: 512, level: 6 }))\n\n// Skip compression for specific routes\napp.use(compress({\n\tfilter: (req, res) => !req.url.startsWith('/stream')\n}))\n\napp.get('/big', (req, res) => {\n\tres.json({ data: 'x'.repeat(10000) })\n})",
|
|
162
|
+
"methods": [],
|
|
163
|
+
"options": [
|
|
164
|
+
{ "option": "threshold", "type": "number", "default": "1024", "notes": "Minimum response body size in bytes before compression is applied." },
|
|
165
|
+
{ "option": "level", "type": "number", "default": "-1 (zlib default)", "notes": "Compression level (1–9). Higher = smaller output, more CPU." },
|
|
166
|
+
{ "option": "filter", "type": "function", "default": "—", "notes": "fn(req, res) → boolean. Return false to skip compression for the request." }
|
|
167
|
+
]
|
|
168
|
+
},
|
|
58
169
|
{
|
|
59
170
|
"name": "json([opts])",
|
|
60
|
-
"description": "Parses JSON request bodies and populates req.body. Skips non-matching Content-Types and calls next().",
|
|
61
|
-
"example": "app.use(json({\n\tlimit: '10kb',\n\tstrict: true,\n\treviver: (k, v) => {\n\t\tif (typeof v === 'string' && /^\\d{4}-\\d{2}-\\d{2}T/.test(v)) return new Date(v)\n\t\treturn v\n\t},\n\ttype: 'application/json'\n}))",
|
|
171
|
+
"description": "Parses JSON request bodies and populates req.body. Skips non-matching Content-Types and calls next(). Supports requireSecure to enforce HTTPS.",
|
|
172
|
+
"example": "app.use(json({\n\tlimit: '10kb',\n\tstrict: true,\n\trequireSecure: false,\n\treviver: (k, v) => {\n\t\tif (typeof v === 'string' && /^\\d{4}-\\d{2}-\\d{2}T/.test(v)) return new Date(v)\n\t\treturn v\n\t},\n\ttype: 'application/json'\n}))",
|
|
62
173
|
"options": [
|
|
63
174
|
{ "option": "limit", "type": "number|string", "default": "null (no limit)", "notes": "Maximum body size. Accepts bytes or unit strings like '100kb', '1mb', '1gb'." },
|
|
64
175
|
{ "option": "strict", "type": "boolean", "default": "true", "notes": "When true, only accepts objects and arrays (rejects primitives)." },
|
|
65
176
|
{ "option": "reviver", "type": "function", "default": "—", "notes": "Optional function passed to JSON.parse for custom reviving." },
|
|
66
|
-
{ "option": "type", "type": "string|function", "default": "application/json", "notes": "MIME type to match. Supports wildcards ('*/*') or a custom predicate function." }
|
|
177
|
+
{ "option": "type", "type": "string|function", "default": "application/json", "notes": "MIME type to match. Supports wildcards ('*/*') or a custom predicate function." },
|
|
178
|
+
{ "option": "requireSecure", "type": "boolean", "default": "false", "notes": "When true, rejects non-HTTPS requests with 403 'HTTPS required'." }
|
|
67
179
|
]
|
|
68
180
|
},
|
|
69
181
|
{
|
|
70
182
|
"name": "urlencoded([opts])",
|
|
71
|
-
"description": "Parses application/x-www-form-urlencoded bodies into req.body.",
|
|
72
|
-
"example": "// Simple flat parsing\napp.use(urlencoded())\n\n// Extended mode with nested bracket syntax\napp.use(urlencoded({\n\textended: true,\n\tlimit: '20kb'\n}))\n// a[b][c]=1 → { a: { b: { c: '1' } } }\n// a[]=1&a[]=2 → { a: ['1', '2'] }",
|
|
183
|
+
"description": "Parses application/x-www-form-urlencoded bodies into req.body. Supports requireSecure.",
|
|
184
|
+
"example": "// Simple flat parsing\napp.use(urlencoded())\n\n// Extended mode with nested bracket syntax\napp.use(urlencoded({\n\textended: true,\n\tlimit: '20kb',\n\trequireSecure: false\n}))\n// a[b][c]=1 → { a: { b: { c: '1' } } }\n// a[]=1&a[]=2 → { a: ['1', '2'] }",
|
|
73
185
|
"options": [
|
|
74
186
|
{ "option": "extended", "type": "boolean", "default": "false", "notes": "When true, supports rich nested bracket syntax (a[b]=1, a[]=1). When false, returns flat key/value pairs." },
|
|
75
187
|
{ "option": "limit", "type": "number|string", "default": "null (no limit)", "notes": "Maximum body size (bytes or unit string)." },
|
|
76
|
-
{ "option": "type", "type": "string|function", "default": "application/x-www-form-urlencoded", "notes": "MIME type to match." }
|
|
188
|
+
{ "option": "type", "type": "string|function", "default": "application/x-www-form-urlencoded", "notes": "MIME type to match." },
|
|
189
|
+
{ "option": "requireSecure", "type": "boolean", "default": "false", "notes": "When true, rejects non-HTTPS requests with 403 'HTTPS required'." }
|
|
77
190
|
]
|
|
78
191
|
},
|
|
79
192
|
{
|
|
80
193
|
"name": "text([opts])",
|
|
81
|
-
"description": "Reads plain text request bodies into req.body as a string.",
|
|
194
|
+
"description": "Reads plain text request bodies into req.body as a string. Supports requireSecure.",
|
|
82
195
|
"example": "app.use(text({ type: 'text/*', limit: '50kb', encoding: 'utf8' }))\n\napp.post('/echo-text', (req, res) => {\n\tres.text(req.body) // echo back the plain text\n})",
|
|
83
196
|
"options": [
|
|
84
197
|
{ "option": "type", "type": "string|function", "default": "text/*", "notes": "MIME matcher — uses wildcard by default so it matches text/plain, text/html, etc." },
|
|
85
198
|
{ "option": "limit", "type": "number|string", "default": "null (no limit)", "notes": "Maximum body size." },
|
|
86
|
-
{ "option": "encoding", "type": "string", "default": "utf8", "notes": "Character encoding used to decode the incoming bytes." }
|
|
199
|
+
{ "option": "encoding", "type": "string", "default": "utf8", "notes": "Character encoding used to decode the incoming bytes." },
|
|
200
|
+
{ "option": "requireSecure", "type": "boolean", "default": "false", "notes": "When true, rejects non-HTTPS requests with 403 'HTTPS required'." }
|
|
87
201
|
]
|
|
88
202
|
},
|
|
89
203
|
{
|
|
90
204
|
"name": "raw([opts])",
|
|
91
|
-
"description": "Receives raw bytes as a Buffer on req.body. Useful for binary payloads.",
|
|
205
|
+
"description": "Receives raw bytes as a Buffer on req.body. Useful for binary payloads. Supports requireSecure.",
|
|
92
206
|
"example": "app.post('/binary', raw({ limit: '5mb' }), (req, res) => {\n\tconsole.log(req.body) // <Buffer ...>\n\tconsole.log(req.body.length) // byte count\n\tres.json({ size: req.body.length })\n})",
|
|
93
207
|
"options": [
|
|
94
208
|
{ "option": "type", "type": "string|function", "default": "application/octet-stream", "notes": "MIME type to match for the raw parser." },
|
|
95
|
-
{ "option": "limit", "type": "number|string", "default": "null (no limit)", "notes": "Maximum body size. Returns 413 if exceeded." }
|
|
209
|
+
{ "option": "limit", "type": "number|string", "default": "null (no limit)", "notes": "Maximum body size. Returns 413 if exceeded." },
|
|
210
|
+
{ "option": "requireSecure", "type": "boolean", "default": "false", "notes": "When true, rejects non-HTTPS requests with 403 'HTTPS required'." }
|
|
96
211
|
]
|
|
97
212
|
},
|
|
98
213
|
{
|
|
99
214
|
"name": "multipart([opts])",
|
|
100
|
-
"description": "Streaming multipart parser that writes file parts to disk and collects text fields. Sets req.body to { fields, files }.
|
|
101
|
-
"example": "const uploadsDir = path.join(os.tmpdir(), 'my-uploads')\n\napp.post('/upload', multipart({\n\tdir: uploadsDir,\n\tmaxFileSize: 10 * 1024 * 1024\n}), (req, res) => {\n\t// req.body.fields → { desc: 'a photo' }\n\t// req.body.files → { file: { originalFilename, storedName, path, contentType, size } }\n\tres.json({ files: req.body.files, fields: req.body.fields })\n})",
|
|
215
|
+
"description": "Streaming multipart parser that writes file parts to disk and collects text fields. Sets req.body to { fields, files }. Supports requireSecure.",
|
|
216
|
+
"example": "const uploadsDir = path.join(os.tmpdir(), 'my-uploads')\n\napp.post('/upload', multipart({\n\tdir: uploadsDir,\n\tmaxFileSize: 10 * 1024 * 1024,\n\trequireSecure: false\n}), (req, res) => {\n\t// req.body.fields → { desc: 'a photo' }\n\t// req.body.files → { file: { originalFilename, storedName, path, contentType, size } }\n\tres.json({ files: req.body.files, fields: req.body.fields })\n})",
|
|
102
217
|
"options": [
|
|
103
218
|
{ "option": "dir", "type": "string", "default": "os.tmpdir()/zero-http-uploads", "notes": "Directory to store uploaded files. Relative paths resolve from process.cwd(). Created automatically if missing." },
|
|
104
|
-
{ "option": "maxFileSize", "type": "number", "default": "null (no limit)", "notes": "Maximum file size in bytes. Exceeding this aborts the upload and returns HTTP 413." }
|
|
219
|
+
{ "option": "maxFileSize", "type": "number", "default": "null (no limit)", "notes": "Maximum file size in bytes. Exceeding this aborts the upload and returns HTTP 413." },
|
|
220
|
+
{ "option": "requireSecure", "type": "boolean", "default": "false", "notes": "When true, rejects non-HTTPS requests with 403 'HTTPS required'." }
|
|
105
221
|
]
|
|
106
222
|
},
|
|
107
223
|
{
|
|
@@ -129,8 +245,8 @@
|
|
|
129
245
|
},
|
|
130
246
|
{
|
|
131
247
|
"name": "fetch(url, [opts])",
|
|
132
|
-
"description": "Small Node HTTP/HTTPS client returning a response with status, ok, statusText, headers.get(), and helpers: text(), json(), arrayBuffer(). Supports timeouts, AbortSignal, progress callbacks,
|
|
133
|
-
"example": "const { fetch } = require('zero-http')\n\n// Simple GET with timeout\nconst r = await fetch('https://jsonplaceholder.typicode.com/todos/1', {\n\ttimeout: 5000\n})\nconst data = await r.json()\nconsole.log(r.ok, r.status, data)\n\n// POST with auto-serialized JSON body\nawait fetch('https://httpbin.org/post', {\n\tmethod: 'POST',\n\tbody: { hello: 'world' }\n})\n\n// Download with progress tracking\nawait fetch('https://example.com/big-file.zip', {\n\tonDownloadProgress: ({ loaded, total }) =>\n\t\tconsole.log(`${loaded}/${total} bytes`)\n})",
|
|
248
|
+
"description": "Small Node HTTP/HTTPS client returning a response with status, ok, statusText, secure, url, headers.get(), and helpers: text(), json(), arrayBuffer(). Supports timeouts, AbortSignal, progress callbacks, auto-serialization, and TLS options passthrough for HTTPS URLs.",
|
|
249
|
+
"example": "const { fetch } = require('zero-http')\n\n// Simple GET with timeout\nconst r = await fetch('https://jsonplaceholder.typicode.com/todos/1', {\n\ttimeout: 5000\n})\nconst data = await r.json()\nconsole.log(r.ok, r.status, r.secure, r.url, data)\n\n// POST with auto-serialized JSON body\nawait fetch('https://httpbin.org/post', {\n\tmethod: 'POST',\n\tbody: { hello: 'world' }\n})\n\n// HTTPS with custom TLS options\nawait fetch('https://internal-api.local/data', {\n\trejectUnauthorized: false,\n\tca: fs.readFileSync('ca.pem')\n})\n\n// Download with progress tracking\nawait fetch('https://example.com/big-file.zip', {\n\tonDownloadProgress: ({ loaded, total }) =>\n\t\tconsole.log(`${loaded}/${total} bytes`)\n})",
|
|
134
250
|
"options": [
|
|
135
251
|
{ "option": "method", "type": "string", "default": "GET", "notes": "HTTP method (uppercased internally)." },
|
|
136
252
|
{ "option": "headers", "type": "object", "default": "{}", "notes": "Request headers." },
|
|
@@ -139,7 +255,12 @@
|
|
|
139
255
|
{ "option": "signal", "type": "AbortSignal", "default": "undefined", "notes": "AbortController signal to cancel the request." },
|
|
140
256
|
{ "option": "agent", "type": "http.Agent", "default": "undefined", "notes": "Optional agent for connection pooling or proxies." },
|
|
141
257
|
{ "option": "onDownloadProgress", "type": "function", "default": "—", "notes": "Callback receiving { loaded, total } on each response chunk." },
|
|
142
|
-
{ "option": "onUploadProgress", "type": "function", "default": "—", "notes": "Callback receiving { loaded, total } during body upload.
|
|
258
|
+
{ "option": "onUploadProgress", "type": "function", "default": "—", "notes": "Callback receiving { loaded, total } during body upload." },
|
|
259
|
+
{ "option": "rejectUnauthorized", "type": "boolean", "default": "true", "notes": "TLS option: set false to accept self-signed certs." },
|
|
260
|
+
{ "option": "ca / cert / key / pfx", "type": "string|Buffer", "default": "—", "notes": "TLS options: custom certificate authority, client cert, key, or PKCS12 bundle." },
|
|
261
|
+
{ "option": "passphrase", "type": "string", "default": "—", "notes": "Passphrase for the private key or pfx." },
|
|
262
|
+
{ "option": "servername", "type": "string", "default": "—", "notes": "SNI hostname for TLS negotiation." },
|
|
263
|
+
{ "option": "minVersion / maxVersion", "type": "string", "default": "—", "notes": "TLS version constraints (e.g. 'TLSv1.2', 'TLSv1.3')." }
|
|
143
264
|
]
|
|
144
265
|
},
|
|
145
266
|
{
|
|
@@ -5,6 +5,36 @@
|
|
|
5
5
|
"language": "javascript",
|
|
6
6
|
"code": "const { createApp, json, cors } = require('zero-http')\nconst app = createApp()\n\napp.use(cors({ origin: ['https://example.com'] }))\napp.use(json({ limit: '10kb' }))\n\nconst items = []\n\napp.get('/items', (req, res) => res.json(items))\n\napp.post('/items', (req, res) => {\n\titems.push(req.body)\n\tres.status(201).json({ ok: true, count: items.length })\n})"
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"title": "WebSocket Chat Server",
|
|
10
|
+
"description": "A simple multi-client chat using the built-in WebSocket support. Clients connect, set a name via query param, and broadcast messages to all other connected clients.",
|
|
11
|
+
"language": "javascript",
|
|
12
|
+
"code": "const { createApp } = require('zero-http')\nconst app = createApp()\n\nconst clients = new Set()\n\napp.ws('/chat', {\n\tmaxPayload: 64 * 1024,\n\tpingInterval: 20000\n}, (ws, req) => {\n\tws.data.name = req.query.name || 'anon'\n\tclients.add(ws)\n\tws.send('Welcome, ' + ws.data.name + '!')\n\n\t// Broadcast to all other clients\n\tws.on('message', msg => {\n\t\tfor (const c of clients) {\n\t\t\tif (c !== ws && c.readyState === 1)\n\t\t\t\tc.send(ws.data.name + ': ' + msg)\n\t\t}\n\t})\n\n\tws.on('close', () => clients.delete(ws))\n})\n\napp.listen(3000)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"title": "Server-Sent Events (SSE) Stream",
|
|
16
|
+
"description": "Push real-time events to browser clients using res.sse(). Supports auto-IDs, named events, keep-alive, and graceful cleanup on disconnect.",
|
|
17
|
+
"language": "javascript",
|
|
18
|
+
"code": "const { createApp } = require('zero-http')\nconst app = createApp()\n\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({\n\t\tretry: 5000,\n\t\tautoId: true,\n\t\tkeepAlive: 30000\n\t})\n\n\tsse.send('connected')\n\n\tconst iv = setInterval(() => {\n\t\tsse.event('tick', { time: Date.now() })\n\t}, 1000)\n\n\tsse.on('close', () => {\n\t\tclearInterval(iv)\n\t\tconsole.log('client disconnected')\n\t})\n})\n\n// Browser: const es = new EventSource('/events')\n// es.addEventListener('tick', e => console.log(JSON.parse(e.data)))\n\napp.listen(3000)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"title": "Router Sub-Apps with Introspection",
|
|
22
|
+
"description": "Organize routes into modular Router instances, mount them under prefixes, and use .routes() to inspect the full route table at runtime.",
|
|
23
|
+
"language": "javascript",
|
|
24
|
+
"code": "const { createApp, Router, json } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\nconst users = Router()\nusers.get('/', (req, res) => res.json([]))\nusers.get('/:id', (req, res) => res.json({ id: req.params.id }))\nusers.post('/', (req, res) => res.status(201).json(req.body))\n\nconst posts = Router()\nposts.route('/').get((req, res) => res.json([])).post((req, res) => res.status(201).json(req.body))\nposts.get('/:id', (req, res) => res.json({ id: req.params.id }))\n\napp.use('/api/users', users)\napp.use('/api/posts', posts)\n\napp.get('/debug/routes', (req, res) => res.json(app.routes()))\n\napp.listen(3000)\n// GET /debug/routes returns the full route table"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"title": "Response Compression",
|
|
28
|
+
"description": "Automatically compress responses with brotli, gzip, or deflate above a size threshold using the compress() middleware.",
|
|
29
|
+
"language": "javascript",
|
|
30
|
+
"code": "const { createApp, compress, json } = require('zero-http')\nconst app = createApp()\n\n// brotli > gzip > deflate, auto-negotiated\napp.use(compress({ threshold: 512, level: 6 }))\napp.use(json())\n\napp.get('/big', (req, res) => {\n\t// This response is large enough to be compressed\n\tres.json({ data: 'x'.repeat(10000) })\n})\n\napp.get('/small', (req, res) => {\n\t// Below threshold — sent uncompressed\n\tres.json({ ok: true })\n})\n\napp.listen(3000)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "HTTPS Server",
|
|
34
|
+
"description": "Start an HTTPS server by passing TLS options to listen(). All modules respect req.secure and req.protocol for protocol-aware logic.",
|
|
35
|
+
"language": "javascript",
|
|
36
|
+
"code": "const fs = require('fs')\nconst { createApp, json } = require('zero-http')\nconst app = createApp()\n\napp.use(json())\n\napp.get('/info', (req, res) => {\n\tres.json({\n\t\tsecure: req.secure,\n\t\tprotocol: req.protocol\n\t})\n})\n\n// HTTPS-only route\napp.get('/secret', { secure: true }, (req, res) => {\n\tres.json({ classified: true })\n})\n\napp.listen(443, {\n\tkey: fs.readFileSync('server-key.pem'),\n\tcert: fs.readFileSync('server-cert.pem')\n}, () => console.log('HTTPS on 443'))"
|
|
37
|
+
},
|
|
8
38
|
{
|
|
9
39
|
"title": "File Upload with Multipart Streaming",
|
|
10
40
|
"description": "Stream uploaded files to disk and return metadata. Swap the disk writer for an S3 stream to go cloud-native.",
|
|
@@ -36,16 +66,10 @@
|
|
|
36
66
|
"code": "const { createApp, json } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\napp.get('/fail', (req, res) => {\n\tthrow new Error('Something broke!')\n})\n\napp.onError((err, req, res, next) => {\n\tconsole.error(`[${new Date().toISOString()}]`, err.stack)\n\tres.status(err.statusCode || 500).json({\n\t\terror: err.message,\n\t\tpath: req.url\n\t})\n})\n\napp.listen(3000)"
|
|
37
67
|
},
|
|
38
68
|
{
|
|
39
|
-
"title": "Server-Side Fetch with Progress",
|
|
40
|
-
"description": "Use the built-in fetch client with timeout, abort support,
|
|
41
|
-
"language": "javascript",
|
|
42
|
-
"code": "const { fetch } = require('zero-http')\n\nasync function download(url) {\n\tconst controller = new AbortController()\n\tsetTimeout(() => controller.abort(), 10000)\n\n\tconst res = await fetch(url, {\n\t\ttimeout: 5000,\n\t\tsignal: controller.signal,\n\t\tonDownloadProgress: ({ loaded, total }) =>\n\t\t\tconsole.log(`${loaded}/${total || '?'} bytes`)\n\t})\n\n\tconsole.log(res.ok, res.status, res.statusText)\n\tconst data = await res.json()\n\treturn data\n}\n\ndownload('https://jsonplaceholder.typicode.com/todos/1')\n\t.then(console.log)\n\t.catch(console.error)"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"title": "Path-Prefix Middleware (Sub-App)",
|
|
46
|
-
"description": "Mount middleware on a path prefix. The prefix is stripped from req.url so downstream handlers see relative paths.",
|
|
69
|
+
"title": "Server-Side Fetch with Progress & TLS",
|
|
70
|
+
"description": "Use the built-in fetch client with timeout, abort support, download progress tracking, and custom TLS options for HTTPS URLs.",
|
|
47
71
|
"language": "javascript",
|
|
48
|
-
"code": "const {
|
|
72
|
+
"code": "const { fetch } = require('zero-http')\n\nasync function download(url) {\n\tconst controller = new AbortController()\n\tsetTimeout(() => controller.abort(), 10000)\n\n\tconst res = await fetch(url, {\n\t\ttimeout: 5000,\n\t\tsignal: controller.signal,\n\t\trejectUnauthorized: false, // accept self-signed certs\n\t\tonDownloadProgress: ({ loaded, total }) =>\n\t\t\tconsole.log(`${loaded}/${total || '?'} bytes`)\n\t})\n\n\tconsole.log(res.ok, res.status, res.secure, res.url)\n\tconst data = await res.json()\n\treturn data\n}\n\ndownload('https://jsonplaceholder.typicode.com/todos/1')\n\t.then(console.log)\n\t.catch(console.error)"
|
|
49
73
|
},
|
|
50
74
|
{
|
|
51
75
|
"title": "CORS with Subdomain Matching",
|
|
@@ -55,8 +79,8 @@
|
|
|
55
79
|
},
|
|
56
80
|
{
|
|
57
81
|
"title": "Full-Featured Server Skeleton",
|
|
58
|
-
"description": "Combines logging, CORS, body parsing, rate limiting, static serving, error handling, and route parameters
|
|
82
|
+
"description": "Combines logging, CORS, compression, body parsing, rate limiting, static serving, WebSocket, SSE, Router sub-apps, error handling, and route parameters.",
|
|
59
83
|
"language": "javascript",
|
|
60
|
-
"code": "const path = require('path')\nconst { createApp, cors, json, urlencoded, text,\n\tstatic: serveStatic, logger, rateLimit,
|
|
84
|
+
"code": "const path = require('path')\nconst { createApp, cors, json, urlencoded, text, compress,\n\tstatic: serveStatic, logger, rateLimit, Router } = require('zero-http')\n\nconst app = createApp()\n\n// Middleware stack\napp.use(logger({ format: 'dev' }))\napp.use(cors())\napp.use(compress())\napp.use(rateLimit({ windowMs: 60000, max: 200 }))\napp.use(json({ limit: '1mb' }))\napp.use(urlencoded({ extended: true }))\napp.use(text())\napp.use(serveStatic(path.join(__dirname, 'public')))\n\n// Router sub-app\nconst api = Router()\napi.get('/health', (req, res) => res.json({ status: 'ok' }))\napi.get('/users/:id', (req, res) => res.json({ id: req.params.id }))\napp.use('/api', api)\n\n// WebSocket\napp.ws('/chat', (ws) => {\n\tws.on('message', msg => ws.send('echo: ' + msg))\n})\n\n// SSE\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({ retry: 3000, autoId: true })\n\tsse.send('connected')\n\tsse.on('close', () => console.log('bye'))\n})\n\n// Introspection\napp.get('/debug/routes', (req, res) => res.json(app.routes()))\n\napp.onError((err, req, res) => {\n\tres.status(500).json({ error: err.message })\n})\n\napp.listen(3000, () => console.log('Server running on :3000'))"
|
|
61
85
|
}
|
|
62
86
|
]
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
[
|
|
2
|
-
{ "option": "createApp()", "type": "function", "default": "—", "notes": "Creates an app instance with use(), get(), post(), put(), delete(), patch(), options(), head(), all(), onError(), listen(), and handler." },
|
|
3
|
-
{ "option": "
|
|
4
|
-
{ "option": "
|
|
5
|
-
{ "option": "
|
|
6
|
-
{ "option": "
|
|
7
|
-
{ "option": "
|
|
2
|
+
{ "option": "createApp()", "type": "function", "default": "—", "notes": "Creates an app instance with use(), get(), post(), put(), delete(), patch(), options(), head(), all(), ws(), onError(), listen(port, [tlsOpts], [cb]), close(), routes(), and handler." },
|
|
3
|
+
{ "option": "Router()", "type": "function", "default": "—", "notes": "Creates a standalone router. Methods: get/post/put/delete/patch/options/head/all(path, [opts], ...handlers), use(fn|prefix+router), route(path), routes(). Supports { secure } option." },
|
|
4
|
+
{ "option": "json([opts])", "type": "function", "default": "—", "notes": "Parses JSON bodies into req.body. Options: limit, strict (true), reviver, type ('application/json'), requireSecure (false)." },
|
|
5
|
+
{ "option": "urlencoded([opts])", "type": "function", "default": "—", "notes": "Parses URL-encoded bodies. Options: extended (false), limit, type ('application/x-www-form-urlencoded'), requireSecure (false)." },
|
|
6
|
+
{ "option": "text([opts])", "type": "function", "default": "—", "notes": "Reads plain text into req.body. Options: type ('text/*'), limit, encoding ('utf8'), requireSecure (false)." },
|
|
7
|
+
{ "option": "raw([opts])", "type": "function", "default": "—", "notes": "Reads raw bytes into a Buffer on req.body. Options: type ('application/octet-stream'), limit, requireSecure (false)." },
|
|
8
|
+
{ "option": "multipart([opts])", "type": "function", "default": "—", "notes": "Streams file parts to disk; sets req.body to { fields, files }. Options: dir (os.tmpdir()/zero-http-uploads), maxFileSize, requireSecure (false)." },
|
|
8
9
|
{ "option": "static(rootPath, [opts])", "type": "function", "default": "—", "notes": "Serves static files with 60+ MIME types. Options: index ('index.html'), maxAge (0), dotfiles ('ignore'), extensions, setHeaders." },
|
|
9
10
|
{ "option": "cors([opts])", "type": "function", "default": "—", "notes": "CORS middleware with preflight handling. Options: origin ('*'), methods, allowedHeaders, credentials (false)." },
|
|
10
|
-
|
|
11
|
+
{ "option": "compress([opts])", "type": "function", "default": "\u2014", "notes": "Response compression middleware (brotli/gzip/deflate, auto-negotiated). Brotli preferred when available (Node \u2265 11.7). Skips SSE streams. Options: threshold (1024), level (-1 zlib default), filter fn." },
|
|
12
|
+
{ "option": "fetch(url, [opts])", "type": "function", "default": "—", "notes": "Node HTTP/HTTPS client. Response has ok, status, statusText, secure, url, headers.get(), text(), json(), arrayBuffer(). Options: method, headers, body (auto-serialized), timeout, signal, agent, onDownloadProgress, onUploadProgress, plus TLS options (rejectUnauthorized, ca, cert, key, pfx, passphrase, servername, minVersion, maxVersion)." },
|
|
11
13
|
{ "option": "rateLimit([opts])", "type": "function", "default": "—", "notes": "Per-IP rate limiter. Sets X-RateLimit-* headers. Options: windowMs (60000), max (100), statusCode (429), message, keyGenerator." },
|
|
12
|
-
{ "option": "logger([opts])", "type": "function", "default": "—", "notes": "Request logger with response timing. Options: format ('dev'|'short'|'tiny'), logger (console.log), colors (auto TTY)." }
|
|
14
|
+
{ "option": "logger([opts])", "type": "function", "default": "—", "notes": "Request logger with response timing. Options: format ('dev'|'short'|'tiny'), logger (console.log), colors (auto TTY)." },
|
|
15
|
+
{ "option": "app.ws(path, [opts], handler)", "type": "method", "default": "\u2014", "notes": "Register a WebSocket upgrade handler. handler receives (ws, req). Options: maxPayload (1MB), pingInterval (30s), verifyClient fn. ws has send/sendJSON/ping/pong/close/terminate, on/once/off/removeAllListeners/listenerCount. Events: message/close/error/ping/pong/drain." },
|
|
16
|
+
{ "option": "res.sse([opts])", "type": "method", "default": "\u2014", "notes": "Open an SSE stream. Returns SSEStream with send/sendJSON/event/comment/retry/keepAlive/flush/close and on/once/off/removeAllListeners/listenerCount. Options: status (200), retry, keepAlive (0), keepAliveComment ('ping'), autoId (false), startId (1), pad (0), headers." },
|
|
17
|
+
{ "option": "WebSocketConnection", "type": "class", "default": "\u2014", "notes": "WebSocket connection wrapper. Properties: id, readyState, protocol, extensions, headers, ip, query, url, secure, maxPayload, connectedAt, uptime, bufferedAmount, data (user store)." },
|
|
18
|
+
{ "option": "SSEStream", "type": "class", "default": "\u2014", "notes": "SSE stream controller. Properties: connected, eventCount, bytesSent, connectedAt, uptime, lastEventId, secure, data (user store). Events: close, error." }
|
|
13
19
|
]
|