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.
Files changed (42) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +82 -1
  3. package/documentation/public/data/api.json +154 -33
  4. package/documentation/public/data/examples.json +35 -11
  5. package/documentation/public/data/options.json +14 -8
  6. package/documentation/public/index.html +109 -17
  7. package/documentation/public/scripts/data-sections.js +4 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +201 -0
  10. package/documentation/public/scripts/ui.js +6 -6
  11. package/documentation/public/scripts/uploads.js +9 -9
  12. package/documentation/public/styles.css +12 -12
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +23 -0
  15. package/documentation/public/vendor/icons/router.svg +29 -0
  16. package/documentation/public/vendor/icons/sse.svg +25 -0
  17. package/documentation/public/vendor/icons/websocket.svg +21 -0
  18. package/index.js +21 -4
  19. package/lib/app.js +145 -15
  20. package/lib/body/json.js +3 -0
  21. package/lib/body/multipart.js +2 -0
  22. package/lib/body/raw.js +3 -0
  23. package/lib/body/text.js +3 -0
  24. package/lib/body/urlencoded.js +3 -0
  25. package/lib/{fetch.js → fetch/index.js} +30 -1
  26. package/lib/http/index.js +9 -0
  27. package/lib/{request.js → http/request.js} +7 -1
  28. package/lib/{response.js → http/response.js} +70 -1
  29. package/lib/middleware/compress.js +194 -0
  30. package/lib/middleware/index.js +12 -0
  31. package/lib/router/index.js +278 -0
  32. package/lib/sse/index.js +8 -0
  33. package/lib/sse/stream.js +322 -0
  34. package/lib/ws/connection.js +440 -0
  35. package/lib/ws/handshake.js +122 -0
  36. package/lib/ws/index.js +12 -0
  37. package/package.json +1 -1
  38. package/lib/router.js +0 -87
  39. /package/lib/{cors.js → middleware/cors.js} +0 -0
  40. /package/lib/{logger.js → middleware/logger.js} +0 -0
  41. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  42. /package/lib/{static.js → middleware/static.js} +0 -0
package/README.md CHANGED
@@ -7,21 +7,26 @@
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D14-brightgreen.svg)](https://nodejs.org)
8
8
  [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](package.json)
9
9
 
10
- > **Zero-dependency, minimal Express-like HTTP server with a tiny fetch replacement and streaming multipart parsing.**
10
+ > **Zero-dependency, Express-like HTTP/HTTPS server with built-in WebSocket, Server-Sent Events, response compression, modular routing, and a tiny fetch client.**
11
11
 
12
12
  ## Features
13
13
 
14
14
  - **Zero dependencies** — implemented using Node core APIs only
15
15
  - **Express-like API** — `createApp()`, `use()`, `get()`, `post()`, `put()`, `delete()`, `patch()`, `head()`, `options()`, `all()`, `listen()`
16
- - **Built-in middlewares** — `cors()`, `json()`, `urlencoded()`, `text()`, `raw()`, `multipart()`, `rateLimit()`, `logger()`
16
+ - **HTTPS support** — pass `{ key, cert }` to `listen()` for TLS; `req.secure` and `req.protocol` available everywhere
17
+ - **Built-in WebSocket server** — `app.ws('/path', handler)` with RFC 6455 framing, ping/pong, sub-protocols, `verifyClient`, per-connection data store
18
+ - **Server-Sent Events** — `res.sse()` returns a rich stream controller with auto-IDs, keep-alive, retry hints, and event counting
19
+ - **Response compression** — `compress()` middleware with brotli/gzip/deflate negotiation, threshold and filter options
20
+ - **Router sub-apps** — `Router()` factory with `.use()` mounting, nested sub-routers, route chaining, and introspection via `app.routes()`
21
+ - **Protocol-aware routing** — `{ secure: true }` option on routes to match HTTPS-only or HTTP-only requests
22
+ - **Built-in middlewares** — `cors()`, `json()`, `urlencoded()`, `text()`, `raw()`, `multipart()`, `rateLimit()`, `logger()`, `compress()`
23
+ - **Body parser HTTPS enforcement** — `{ requireSecure: true }` on any body parser to reject non-TLS requests with 403
17
24
  - **Streaming multipart parser** — writes file parts to disk and exposes `req.body.files` and `req.body.fields`
18
- - **Tiny `fetch` replacement** — convenient server-side HTTP client with progress callbacks
25
+ - **Tiny `fetch` replacement** — server-side HTTP/HTTPS client with TLS options passthrough, progress callbacks, abort support
19
26
  - **Static file serving** — 60+ MIME types, dotfile policy, caching, extension fallback
20
27
  - **Error handling** — automatic 500 responses for thrown errors, global error handler via `app.onError()`
21
- - **Path-prefix middleware** — `app.use('/api', handler)` with automatic URL rewriting
22
28
  - **Rate limiting** — in-memory IP-based rate limiter with configurable windows
23
29
  - **Request logger** — colorized dev/short/tiny log formats
24
-
25
30
 
26
31
  ```bash
27
32
  npm install zero-http
@@ -38,7 +43,7 @@ app.post('/echo', (req, res) => res.json({ received: req.body }))
38
43
  app.listen(3000)
39
44
  ```
40
45
 
41
- Demo
46
+ ## Demo
42
47
 
43
48
  You can view the live documentation and playground at https://zero-http.molex.cloud, or run the demo locally:
44
49
 
@@ -52,41 +57,53 @@ node documentation/full-server.js
52
57
  All exports are available from the package root:
53
58
 
54
59
  ```js
55
- const { createApp, cors, fetch, json, urlencoded, text, raw, multipart, static: serveStatic, rateLimit, logger } = require('zero-http')
60
+ const {
61
+ createApp, Router, cors, fetch,
62
+ json, urlencoded, text, raw, multipart,
63
+ static: serveStatic, rateLimit, logger, compress,
64
+ WebSocketConnection, SSEStream
65
+ } = require('zero-http')
56
66
  ```
57
67
 
58
68
  | Export | Type | Description |
59
69
  |---|---|---|
60
70
  | `createApp()` | function | Create a new application instance (router + middleware stack). |
71
+ | `Router()` | function | Create a standalone Router for modular route grouping. |
61
72
  | `cors` | function | CORS middleware factory. |
62
- | `fetch` | function | Small Node HTTP client with progress callbacks. |
73
+ | `fetch` | function | Node HTTP/HTTPS client with TLS options passthrough. |
63
74
  | `json` | function | JSON body parser factory. |
64
- | `urlencoded` | function | urlencoded body parser factory. |
75
+ | `urlencoded` | function | URL-encoded body parser factory. |
65
76
  | `text` | function | Text body parser factory. |
66
77
  | `raw` | function | Raw bytes parser factory. |
67
78
  | `multipart` | function | Streaming multipart parser factory. |
68
79
  | `static` | function | Static file serving middleware factory. |
69
80
  | `rateLimit` | function | In-memory rate-limiting middleware factory. |
70
81
  | `logger` | function | Request-logging middleware factory. |
82
+ | `compress` | function | Response compression middleware (brotli/gzip/deflate). |
83
+ | `WebSocketConnection` | class | WebSocket connection wrapper (for advanced usage). |
84
+ | `SSEStream` | class | SSE stream controller (for advanced usage). |
71
85
 
72
- createApp() methods
86
+ ### createApp() methods
73
87
 
74
88
  | Method | Signature | Description |
75
89
  |---|---|---|
76
- | `use` | `use(fn)` or `use(path, fn)` | Register middleware globally or scoped to a path prefix. |
77
- | `get` | `get(path, ...handlers)` | Register GET route handlers. |
78
- | `post` | `post(path, ...handlers)` | Register POST route handlers. |
79
- | `put` | `put(path, ...handlers)` | Register PUT route handlers. |
80
- | `delete` | `delete(path, ...handlers)` | Register DELETE route handlers. |
81
- | `patch` | `patch(path, ...handlers)` | Register PATCH route handlers. |
82
- | `options` | `options(path, ...handlers)` | Register OPTIONS route handlers. |
83
- | `head` | `head(path, ...handlers)` | Register HEAD route handlers. |
84
- | `all` | `all(path, ...handlers)` | Register handlers for ALL HTTP methods. |
90
+ | `use` | `use(fn)` or `use(path, fn)` or `use(path, router)` | Register middleware globally, scoped to a path prefix, or mount a sub-router. |
91
+ | `get` | `get(path, [opts], ...handlers)` | Register GET route handlers. Optional `{ secure }` options object. |
92
+ | `post` | `post(path, [opts], ...handlers)` | Register POST route handlers. |
93
+ | `put` | `put(path, [opts], ...handlers)` | Register PUT route handlers. |
94
+ | `delete` | `delete(path, [opts], ...handlers)` | Register DELETE route handlers. |
95
+ | `patch` | `patch(path, [opts], ...handlers)` | Register PATCH route handlers. |
96
+ | `options` | `options(path, [opts], ...handlers)` | Register OPTIONS route handlers. |
97
+ | `head` | `head(path, [opts], ...handlers)` | Register HEAD route handlers. |
98
+ | `all` | `all(path, [opts], ...handlers)` | Register handlers for ALL HTTP methods. |
99
+ | `ws` | `ws(path, [opts], handler)` | Register a WebSocket upgrade handler. |
85
100
  | `onError` | `onError(fn)` | Register a global error handler `fn(err, req, res, next)`. |
86
- | `listen` | `listen(port = 3000, cb)` | Start the HTTP server. Returns the server instance. |
101
+ | `listen` | `listen(port, [tlsOpts], [cb])` | Start HTTP or HTTPS server. Pass `{ key, cert }` for TLS. |
102
+ | `close` | `close([cb])` | Gracefully close the server. |
103
+ | `routes` | `routes()` | Return a flat list of all registered routes (introspection). |
87
104
  | `handler` | property | Bound request handler for `http.createServer(app.handler)`. |
88
105
 
89
- Request (`req`) properties & helpers
106
+ ### Request (`req`) properties & helpers
90
107
 
91
108
  | Property / Method | Type | Description |
92
109
  |---|---|---|
@@ -97,11 +114,13 @@ Request (`req`) properties & helpers
97
114
  | `params` | object | Route parameters (populated by router). |
98
115
  | `body` | any | Parsed body (populated by body parsers). |
99
116
  | `ip` | string | Remote IP address of the client. |
117
+ | `secure` | boolean | `true` when the connection is over TLS (HTTPS). |
118
+ | `protocol` | string | `'https'` or `'http'`. |
100
119
  | `get(name)` | function | Get a request header (case-insensitive). |
101
120
  | `is(type)` | function | Check if Content-Type matches a type (e.g. `'json'`, `'text/html'`). |
102
121
  | `raw` | object | Underlying `http.IncomingMessage`. |
103
122
 
104
- Response (`res`) helpers
123
+ ### Response (`res`) helpers
105
124
 
106
125
  | Method | Signature | Description |
107
126
  |---|---|---|
@@ -114,53 +133,213 @@ Response (`res`) helpers
114
133
  | `text` | `text(str)` | Set text/plain and send string. |
115
134
  | `html` | `html(str)` | Set text/html and send string. |
116
135
  | `redirect` | `redirect([status], url)` | Redirect to URL (default 302). |
136
+ | `sse` | `sse([opts])` | Open a Server-Sent Events stream. Returns an `SSEStream` controller. |
137
+
138
+ ### WebSocket — `app.ws(path, [opts], handler)`
139
+
140
+ Register a WebSocket upgrade handler. The handler receives `(ws, req)` where `ws` is a `WebSocketConnection`.
141
+
142
+ | Option | Type | Default | Description |
143
+ |---|---:|---|---|
144
+ | `maxPayload` | number | `1048576` | Maximum incoming frame size in bytes (1 MB). |
145
+ | `pingInterval` | number | `30000` | Auto-ping interval in ms. `0` to disable. |
146
+ | `verifyClient` | function | — | `(req) => boolean` — return `false` to reject the upgrade with 403. |
147
+
148
+ **WebSocketConnection properties:**
149
+
150
+ | Property | Type | Description |
151
+ |---|---|---|
152
+ | `id` | string | Unique connection ID (e.g. `ws_1_l8x3k`). |
153
+ | `readyState` | number | 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED. |
154
+ | `protocol` | string | Negotiated sub-protocol. |
155
+ | `headers` | object | Upgrade request headers. |
156
+ | `ip` | string | Remote IP address. |
157
+ | `query` | object | Parsed query params from the upgrade URL. |
158
+ | `url` | string | Full upgrade URL. |
159
+ | `secure` | boolean | `true` for WSS connections. |
160
+ | `extensions` | string | Requested WebSocket extensions header. |
161
+ | `maxPayload` | number | Max incoming payload bytes. |
162
+ | `connectedAt` | number | Timestamp (ms) of connection. |
163
+ | `uptime` | number | Milliseconds since connection (computed). |
164
+ | `bufferedAmount` | number | Bytes waiting to be flushed. |
165
+ | `data` | object | Arbitrary user-data store. |
166
+
167
+ **WebSocketConnection methods:**
168
+
169
+ | Method | Description |
170
+ |---|---|
171
+ | `send(data, [opts])` | Send text or binary message. `opts.binary` to force binary frame. |
172
+ | `sendJSON(obj)` | Send JSON-serialised text message. |
173
+ | `ping([payload])` | Send a ping frame. |
174
+ | `pong([payload])` | Send a pong frame. |
175
+ | `close([code], [reason])` | Graceful close with optional status code. |
176
+ | `terminate()` | Forcefully destroy the socket. |
177
+ | `on(event, fn)` | Listen for `'message'`, `'close'`, `'error'`, `'ping'`, `'pong'`, `'drain'`. |
178
+ | `once(event, fn)` | One-time event listener. |
179
+ | `off(event, fn)` | Remove a listener. |
180
+ | `removeAllListeners([event])` | Remove all listeners for an event, or all events. |
181
+ | `listenerCount(event)` | Return the number of listeners for an event. |
182
+
183
+ Example:
184
+
185
+ ```js
186
+ app.ws('/chat', { maxPayload: 64 * 1024 }, (ws, req) => {
187
+ ws.send('Welcome!')
188
+ ws.on('message', data => ws.send('echo: ' + data))
189
+ ws.on('close', () => console.log(ws.id, 'left'))
190
+ })
191
+ ```
192
+
193
+ ### Server-Sent Events — `res.sse([opts])`
194
+
195
+ Open an SSE stream. Returns an `SSEStream` controller.
196
+
197
+ | Option | Type | Default | Description |
198
+ |---|---:|---|---|
199
+ | `status` | number | `200` | HTTP status code for the SSE response. |
200
+ | `retry` | number | — | Reconnection interval hint (ms) sent to client. |
201
+ | `keepAlive` | number | `0` | Auto keep-alive comment interval (ms). |
202
+ | `keepAliveComment` | string | `'ping'` | Comment text sent by the keep-alive timer. |
203
+ | `autoId` | boolean | `false` | Auto-increment event IDs. |
204
+ | `startId` | number | `1` | Starting value for auto-IDs. |
205
+ | `pad` | number | `0` | Bytes of initial padding (helps flush proxy buffers). |
206
+ | `headers` | object | — | Additional response headers. |
207
+
208
+ **SSEStream methods:**
209
+
210
+ | Method | Description |
211
+ |---|---|
212
+ | `send(data, [id])` | Send an unnamed data event. Objects are auto-JSON-serialised. |
213
+ | `sendJSON(obj, [id])` | Alias for `send()` with an object. |
214
+ | `event(name, data, [id])` | Send a named event. |
215
+ | `comment(text)` | Send a comment line (keep-alive or debug). |
216
+ | `retry(ms)` | Update the reconnection interval hint. |
217
+ | `keepAlive(ms, [comment])` | Start/restart a keep-alive timer. |
218
+ | `flush()` | Flush buffered data through proxies. |
219
+ | `close()` | Close the stream from the server side. |
220
+ | `on(event, fn)` | Listen for `'close'` or `'error'` events. |
221
+ | `once(event, fn)` | One-time event listener. |
222
+ | `off(event, fn)` | Remove a specific event listener. |
223
+ | `removeAllListeners([event])` | Remove all listeners for an event, or all events. |
224
+ | `listenerCount(event)` | Return the number of listeners for an event. |
225
+
226
+ **SSEStream properties:** `connected`, `eventCount`, `bytesSent`, `connectedAt`, `uptime`, `lastEventId`, `secure`, `data`.
227
+
228
+ **SSEStream events:** `'close'` (client disconnect or server close), `'error'` (write error).
229
+
230
+ Example:
231
+
232
+ ```js
233
+ app.get('/events', (req, res) => {
234
+ const sse = res.sse({ retry: 5000, keepAlive: 30000, autoId: true })
235
+ sse.send('hello')
236
+ sse.event('update', { x: 1 })
237
+ sse.on('close', () => console.log('client disconnected'))
238
+ })
239
+ ```
240
+
241
+ ### Router sub-apps
242
+
243
+ Create modular route groups with `Router()`:
244
+
245
+ ```js
246
+ const { createApp, Router, json } = require('zero-http')
247
+ const app = createApp()
248
+ const api = Router()
249
+
250
+ api.get('/users', (req, res) => res.json([]))
251
+ api.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
252
+
253
+ app.use(json())
254
+ app.use('/api', api)
255
+ app.listen(3000)
256
+
257
+ // Introspection
258
+ console.log(app.routes())
259
+ // [{ method: 'GET', path: '/api/users' }, { method: 'GET', path: '/api/users/:id' }]
260
+ ```
261
+
262
+ Route chaining:
263
+
264
+ ```js
265
+ const router = Router()
266
+ router.route('/items')
267
+ .get((req, res) => res.json([]))
268
+ .post((req, res) => res.status(201).json(req.body))
269
+ ```
270
+
271
+ Protocol-aware routes:
272
+
273
+ ```js
274
+ // Only matches HTTPS requests
275
+ app.get('/secret', { secure: true }, (req, res) => res.json({ secret: 42 }))
276
+ // Only matches plain HTTP
277
+ app.get('/public', { secure: false }, (req, res) => res.json({ public: true }))
278
+ ```
279
+
280
+ ### Response compression — `compress([opts])`
281
+
282
+ Negotiates the best encoding from `Accept-Encoding`: brotli (`br`) > gzip > deflate. Brotli requires Node ≥ 11.7. Automatically skips SSE (`text/event-stream`) streams.
283
+
284
+ | Option | Type | Default | Description |
285
+ |---|---:|---|---|
286
+ | `threshold` | number | `1024` | Minimum response size in bytes before compressing. |
287
+ | `level` | number | `-1` | zlib compression level (1–9, or -1 for default). |
288
+ | `filter` | function | — | `(req, res) => boolean` — return `false` to skip compression. |
289
+
290
+ ```js
291
+ app.use(compress({ threshold: 512 }))
292
+ ```
117
293
 
118
294
  ### Body parsers
119
295
 
120
- The package exposes parser factory functions under `json`, `urlencoded`, `text`, `raw`, and `multipart`.
296
+ All body parsers accept a `requireSecure` option. When `true`, non-HTTPS requests are rejected with `403 HTTPS required`.
121
297
 
122
- json([opts])
298
+ #### json([opts])
123
299
 
124
300
  | Option | Type | Default | Description |
125
301
  |---|---:|---|---|
126
- | `limit` | number|string | none | Maximum body size (bytes or unit string like `'1mb'`). |
302
+ | `limit` | number\|string | none | Maximum body size (bytes or unit string like `'1mb'`). |
127
303
  | `reviver` | function | — | Function passed to `JSON.parse` for custom reviving. |
128
304
  | `strict` | boolean | `true` | When `true` only accepts objects/arrays (rejects primitives). |
129
- | `type` | string|function | `'application/json'` | MIME matcher for the parser. |
305
+ | `type` | string\|function | `'application/json'` | MIME matcher for the parser. |
306
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
130
307
 
131
- urlencoded([opts])
308
+ #### urlencoded([opts])
132
309
 
133
310
  | Option | Type | Default | Description |
134
311
  |---|---:|---|---|
135
- | `extended` | boolean | `false` | When `true` supports rich nested bracket syntax (a[b]=1, a[]=1). |
136
- | `limit` | number|string | none | Maximum body size. |
137
- | `type` | string|function | `'application/x-www-form-urlencoded'` | MIME matcher. |
312
+ | `extended` | boolean | `false` | When `true` supports nested bracket syntax (`a[b]=1`). |
313
+ | `limit` | number\|string | none | Maximum body size. |
314
+ | `type` | string\|function | `'application/x-www-form-urlencoded'` | MIME matcher. |
315
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
138
316
 
139
- text([opts])
317
+ #### text([opts])
140
318
 
141
319
  | Option | Type | Default | Description |
142
320
  |---|---:|---|---|
143
- | `type` | string|function | `text/*` | MIME matcher for text bodies. |
144
- | `limit` | number|string | none | Maximum body size. |
321
+ | `type` | string\|function | `text/*` | MIME matcher for text bodies. |
322
+ | `limit` | number\|string | none | Maximum body size. |
145
323
  | `encoding` | string | `utf8` | Character encoding used to decode bytes. |
324
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
146
325
 
147
- raw([opts])
326
+ #### raw([opts])
148
327
 
149
328
  | Option | Type | Default | Description |
150
329
  |---|---:|---|---|
151
- | `type` | string|function | `application/octet-stream` | MIME matcher for raw parser. |
152
- | `limit` | number|string | none | Maximum body size. |
330
+ | `type` | string\|function | `application/octet-stream` | MIME matcher for raw parser. |
331
+ | `limit` | number\|string | none | Maximum body size. |
332
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
153
333
 
154
- multipart(opts)
334
+ #### multipart(opts)
155
335
 
156
336
  Streaming multipart parser that writes file parts to disk and collects fields.
157
337
 
158
338
  | Option | Type | Default | Description |
159
339
  |---|---:|---|---|
160
- | `dir` | string | `os.tmpdir()/zero-http-uploads` | Directory to store uploaded files (absolute or relative to `process.cwd()`). |
161
- | `maxFileSize` | number | none | Maximum allowed file size in bytes. Exceeding this returns HTTP 413 and aborts the upload. |
162
-
163
- Behavior: `multipart` writes file parts to disk with a generated name preserving the original extension when possible. On completion `req.body` will be `{ fields, files }` where `files` contains metadata: `originalFilename`, `storedName`, `path`, `contentType`, `size`.
340
+ | `dir` | string | `os.tmpdir()/zero-http-uploads` | Upload directory. |
341
+ | `maxFileSize` | number | none | Maximum file size in bytes. Returns 413 on exceed. |
342
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
164
343
 
165
344
  ### static(rootPath, opts)
166
345
 
@@ -168,137 +347,157 @@ Serve static files from `rootPath`.
168
347
 
169
348
  | Option | Type | Default | Description |
170
349
  |---|---:|---|---|
171
- | `index` | string|false | `'index.html'` | File to serve for directory requests; set `false` to disable. |
172
- | `maxAge` | number|string | `0` | Cache-Control `max-age` (ms or unit string like `'1h'`). |
173
- | `dotfiles` | string | `'ignore'` | `'allow'|'deny'|'ignore'` — how to handle dotfiles. |
174
- | `extensions` | string[] | — | Fallback extensions to try when a request omits an extension. |
175
- | `setHeaders` | function | — | Hook `(res, filePath) => {}` to set custom headers per file. |
350
+ | `index` | string\|false | `'index.html'` | File to serve for directory requests. |
351
+ | `maxAge` | number\|string | `0` | Cache-Control `max-age` (ms or unit string). |
352
+ | `dotfiles` | string | `'ignore'` | `'allow'|'deny'|'ignore'`. |
353
+ | `extensions` | string[] | — | Fallback extensions to try. |
354
+ | `setHeaders` | function | — | Hook `(res, filePath) => {}` for custom headers. |
176
355
 
177
356
  ### cors([opts])
178
357
 
179
- Small CORS middleware. Typical options:
180
-
181
358
  | Option | Type | Default | Description |
182
359
  |---|---:|---|---|
183
- | `origin` | string|boolean|array | `'*'` | Allowed origin(s). Use `false` to disable CORS. |
360
+ | `origin` | string\|boolean\|array | `'*'` | Allowed origin(s). `.suffix` for subdomain matching. |
184
361
  | `methods` | string | `'GET,POST,PUT,DELETE,OPTIONS'` | Allowed methods. |
185
- | `credentials` | boolean | `false` | When true and a specific origin matches, sets `Access-Control-Allow-Credentials`.
362
+ | `credentials` | boolean | `false` | Set `Access-Control-Allow-Credentials`. |
186
363
  | `allowedHeaders` | string | — | Headers allowed in requests. |
187
364
 
188
365
  ### fetch(url, opts)
189
366
 
190
- Small Node HTTP client returning an object with `status`, `headers` and helpers: `text()`, `json()`, `arrayBuffer()`.
367
+ Node HTTP/HTTPS client. Returns `{ status, statusText, ok, secure, url, headers, text(), json(), arrayBuffer() }`.
191
368
 
192
369
  | Option | Type | Default | Description |
193
370
  |---|---:|---|---|
194
371
  | `method` | string | `GET` | HTTP method. |
195
372
  | `headers` | object | — | Request headers. |
196
- | `agent` | object | — | Optional `http`/`https` agent for connection pooling or proxies. |
197
- | `body` | Buffer|string|Stream|URLSearchParams|object | — | Request body. Plain objects are JSON-encoded and `Content-Type` is set to `application/json` if not provided; `URLSearchParams` produce urlencoded bodies. |
198
- | `timeout` | number | — | Request timeout in milliseconds. |
199
- | `signal` | AbortSignal | — | Optional `AbortSignal` to cancel the request. |
200
- | `onUploadProgress` / `onDownloadProgress` | function | — | Callbacks receiving `{ loaded, total }` during transfer. |
201
-
202
- Response: resolved value includes `ok`, `statusText`, and helpers `arrayBuffer()`, `text()`, `json()`.
373
+ | `body` | Buffer\|string\|Stream\|URLSearchParams\|object | — | Request body (objects auto-JSON-encoded). |
374
+ | `timeout` | number | — | Request timeout in ms. |
375
+ | `signal` | AbortSignal | — | Cancel the request. |
376
+ | `agent` | object | — | Custom agent for pooling/proxies. |
377
+ | `onDownloadProgress` / `onUploadProgress` | function | — | `{ loaded, total }` callbacks. |
203
378
 
204
- Example usage:
205
-
206
- ```js
207
- const r = await fetch('https://jsonplaceholder.typicode.com/todos/1', { timeout: 5000 })
208
- const data = await r.json()
209
- ```
379
+ **TLS options** (passed through for `https:` URLs): `rejectUnauthorized`, `ca`, `cert`, `key`, `pfx`, `passphrase`, `servername`, `ciphers`, `secureProtocol`, `minVersion`, `maxVersion`.
210
380
 
211
381
  ### rateLimit([opts])
212
382
 
213
- In-memory, per-IP rate-limiting middleware. Sets standard `X-RateLimit-*` headers.
214
-
215
383
  | Option | Type | Default | Description |
216
384
  |---|---:|---|---|
217
- | `windowMs` | number | `60000` | Time window in milliseconds. |
218
- | `max` | number | `100` | Maximum requests per window per key. |
219
- | `message` | string | `'Too many requests…'` | Error message returned when limit is exceeded. |
385
+ | `windowMs` | number | `60000` | Time window in ms. |
386
+ | `max` | number | `100` | Max requests per window per key. |
387
+ | `message` | string | `'Too many requests…'` | Error message. |
220
388
  | `statusCode` | number | `429` | HTTP status for rate-limited responses. |
221
- | `keyGenerator` | function | `(req) => req.ip` | Custom key extraction function. |
222
-
223
- Example:
224
-
225
- ```js
226
- app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))
227
- ```
389
+ | `keyGenerator` | function | `(req) => req.ip` | Custom key extraction. |
228
390
 
229
391
  ### logger([opts])
230
392
 
231
- Request-logging middleware that prints method, url, status, and response time.
232
-
233
393
  | Option | Type | Default | Description |
234
394
  |---|---:|---|---|
235
- | `format` | string | `'dev'` | Log format: `'dev'` (colorized), `'short'`, or `'tiny'`. |
395
+ | `format` | string | `'dev'` | `'dev'` (colorized), `'short'`, or `'tiny'`. |
236
396
  | `logger` | function | `console.log` | Custom log function. |
237
397
  | `colors` | boolean | auto (TTY) | Enable/disable ANSI colors. |
238
398
 
239
- Example:
399
+ ### HTTPS
240
400
 
241
401
  ```js
242
- app.use(logger({ format: 'dev' }))
402
+ const fs = require('fs')
403
+ const { createApp } = require('zero-http')
404
+ const app = createApp()
405
+
406
+ app.listen(443, {
407
+ key: fs.readFileSync('key.pem'),
408
+ cert: fs.readFileSync('cert.pem')
409
+ }, () => console.log('HTTPS on 443'))
243
410
  ```
244
411
 
245
- ### Error handling
412
+ All modules respect HTTPS: `req.secure`, `req.protocol`, `ws.secure`, `sse.secure`, and `fetch()` response `secure` property.
246
413
 
247
- Thrown errors in route handlers are automatically caught and return a 500 JSON response. Register a custom error handler for more control:
414
+ ## Examples
248
415
 
249
- ```js
250
- app.onError((err, req, res, next) => {
251
- console.error(err)
252
- res.status(500).json({ error: err.message })
253
- })
254
- ```
416
+ WebSocket chat:
255
417
 
256
- ### Path-prefix middleware
418
+ ```js
419
+ const { createApp } = require('zero-http')
420
+ const app = createApp()
257
421
 
258
- Mount middleware on a path prefix. The URL is rewritten so downstream middleware sees relative paths:
422
+ app.ws('/chat', (ws, req) => {
423
+ ws.send('Welcome to the chat!')
424
+ ws.on('message', msg => {
425
+ ws.send(`You said: ${msg}`)
426
+ })
427
+ })
259
428
 
260
- ```js
261
- app.use('/api', myApiRouter)
429
+ app.listen(3000)
262
430
  ```
263
431
 
264
- ## Examples
265
-
266
- Small JSON API:
432
+ Server-Sent Events:
267
433
 
268
434
  ```js
269
- const { createApp, json, cors } = require('zero-http')
270
- const app = createApp()
435
+ app.get('/events', (req, res) => {
436
+ const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 })
271
437
 
272
- app.use(cors({ origin: ['https://example.com'] }))
273
- app.use(json({ limit: '10kb' }))
438
+ const interval = setInterval(() => {
439
+ sse.event('tick', { time: Date.now() })
440
+ }, 1000)
274
441
 
275
- const items = []
276
- app.post('/items', (req, res) => {
277
- items.push(req.body)
278
- res.status(201)
279
- res.json({ ok: true })
442
+ sse.on('close', () => clearInterval(interval))
280
443
  })
281
444
  ```
282
445
 
283
- Upload handler (writes files to disk by default):
446
+ Full-featured server:
284
447
 
285
448
  ```js
286
- app.post('/upload', multipart({ dir: uploadsDir, maxFileSize: 10 * 1024 * 1024 }), (req, res) => {
287
- res.json({ files: req.body.files })
449
+ const path = require('path')
450
+ const { createApp, cors, json, urlencoded, text, compress,
451
+ static: serveStatic, logger, rateLimit, Router } = require('zero-http')
452
+
453
+ const app = createApp()
454
+
455
+ app.use(logger({ format: 'dev' }))
456
+ app.use(cors())
457
+ app.use(compress())
458
+ app.use(rateLimit({ windowMs: 60000, max: 200 }))
459
+ app.use(json({ limit: '1mb' }))
460
+ app.use(urlencoded({ extended: true }))
461
+ app.use(text())
462
+ app.use(serveStatic(path.join(__dirname, 'public')))
463
+
464
+ const api = Router()
465
+ api.get('/health', (req, res) => res.json({ status: 'ok' }))
466
+ api.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
467
+ app.use('/api', api)
468
+
469
+ app.ws('/chat', (ws) => {
470
+ ws.on('message', msg => ws.send('echo: ' + msg))
288
471
  })
289
- ```
290
472
 
291
- Static server example:
473
+ app.get('/events', (req, res) => {
474
+ const sse = res.sse({ retry: 3000, autoId: true })
475
+ sse.send('connected')
476
+ sse.on('close', () => console.log('bye'))
477
+ })
292
478
 
293
- ```js
294
- app.use(static(path.join(__dirname, 'documentation', 'public'), { index: 'index.html', maxAge: '1h' }))
479
+ app.onError((err, req, res) => {
480
+ res.status(500).json({ error: err.message })
481
+ })
482
+
483
+ app.listen(3000, () => console.log('Server running on :3000'))
295
484
  ```
296
485
 
297
486
  ## File layout
298
487
 
299
- - `lib/` — core helpers and middleware (router, fetch, body parsers, static, rate limiter, logger)
300
- - `documentation/` — demo server, controllers and public UI used to showcase features
301
- - `test/` integration tests
488
+ ```
489
+ lib/
490
+ app.js — core App class (middleware, routing, listen, ws)
491
+ body/ — body parsers (json, urlencoded, text, raw, multipart)
492
+ fetch/ — server-side HTTP/HTTPS client
493
+ http/ — Request & Response wrappers
494
+ middleware/ — cors, logger, rateLimit, compress, static
495
+ router/ — Router with sub-app mounting & introspection
496
+ sse/ — SSEStream controller
497
+ ws/ — WebSocket connection & handshake
498
+ documentation/ — demo server, controllers, and public UI
499
+ test/ — integration tests
500
+ ```
302
501
 
303
502
  ## Testing
304
503