zero-http 0.2.5 → 0.3.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 (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +25 -184
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
package/README.md CHANGED
@@ -6,37 +6,40 @@
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/zero-http.svg)](https://www.npmjs.com/package/zero-http)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/zero-http.svg)](https://www.npmjs.com/package/zero-http)
9
- [![GitHub](https://img.shields.io/badge/GitHub-zero--http--npm-blue.svg)](https://github.com/tonywied17/zero-http-npm)
9
+ [![GitHub](https://img.shields.io/badge/GitHub-zero--http--npm-blue.svg)](https://github.com/tonywied17/zero-http)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
11
  [![Node.js](https://img.shields.io/badge/node-%3E%3D14-brightgreen.svg)](https://nodejs.org)
12
12
  [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](package.json)
13
13
 
14
- > **Zero-dependency, Express-like HTTP/HTTPS server with built-in WebSocket, Server-Sent Events, response compression, modular routing, and a tiny fetch client.**
14
+ > **Zero-dependency backend framework for Node.js — Express-like routing, built-in ORM, WebSocket, SSE, security middleware, body parsers, response compression, and a tiny fetch client.**
15
+
16
+ > **Full API reference, interactive playground, and live demos at [z-http.com](https://z-http.com)**
15
17
 
16
18
  ## Features
17
19
 
18
- - **Zero dependencies** — implemented using Node core APIs only
20
+ - **Zero dependencies** — built entirely on Node.js core APIs
19
21
  - **Express-like API** — `createApp()`, `use()`, `get()`, `post()`, `put()`, `delete()`, `patch()`, `head()`, `options()`, `all()`, `listen()`
20
- - **HTTPS support** — pass `{ key, cert }` to `listen()` for TLS; `req.secure` and `req.protocol` available everywhere
21
- - **Built-in WebSocket server** — `app.ws('/path', handler)` with RFC 6455 framing, ping/pong, sub-protocols, `verifyClient`, per-connection data store
22
- - **Server-Sent Events** — `res.sse()` returns a rich stream controller with auto-IDs, keep-alive, retry hints, and event counting
23
- - **Response compression** — `compress()` middleware with brotli/gzip/deflate negotiation, threshold and filter options
24
- - **Router sub-apps** — `Router()` factory with `.use()` mounting, nested sub-routers, route chaining, and introspection via `app.routes()`
25
- - **Protocol-aware routing** — `{ secure: true }` option on routes to match HTTPS-only or HTTP-only requests
26
- - **Built-in middlewares** — `cors()`, `json()`, `urlencoded()`, `text()`, `raw()`, `multipart()`, `rateLimit()`, `logger()`, `compress()`
27
- - **Body parser HTTPS enforcement** — `{ requireSecure: true }` on any body parser to reject non-TLS requests with 403
28
- - **Streaming multipart parser** — writes file parts to disk and exposes `req.body.files` and `req.body.fields`
29
- - **Tiny `fetch` replacement** — server-side HTTP/HTTPS client with TLS options passthrough, progress callbacks, abort support
30
- - **Static file serving** — 60+ MIME types, dotfile policy, caching, extension fallback
31
- - **Error handling** — automatic 500 responses for thrown errors, global error handler via `app.onError()`
32
- - **Rate limiting** — in-memory IP-based rate limiter with configurable windows
33
- - **Request logger** — colorized dev/short/tiny log formats
22
+ - **HTTP & HTTPS** — pass `{ key, cert }` to `listen()` for TLS; `req.secure` and `req.protocol` everywhere
23
+ - **Built-in ORM** — `Database.connect()` with memory, JSON, SQLite, MySQL, PostgreSQL, and MongoDB adapters; `Model` base class with schema validation, CRUD, timestamps, soft deletes, scopes, hooks, and a fluent `Query` builder
24
+ - **Environment config** — typed `.env` loader with schema validation, multi-environment file support (`.env.local`, `.env.production`), and property-style access (`env.PORT`)
25
+ - **Built-in WebSocket server** — `app.ws('/path', handler)` with RFC 6455 framing, auto-ping, sub-protocols, `verifyClient`, rooms, and broadcasting via `WebSocketPool`
26
+ - **Server-Sent Events** — `res.sse()` returns a chainable stream controller with auto-IDs, keep-alive, retry hints, and event counting
27
+ - **12 built-in middlewares** — `cors()`, `helmet()`, `compress()`, `rateLimit()`, `logger()`, `timeout()`, `requestId()`, `cookieParser()`, `csrf()`, `validate()`, `errorHandler()`, and static file serving
28
+ - **5 body parsers** — `json()`, `urlencoded()`, `text()`, `raw()`, `multipart()` with HTTPS enforcement option
29
+ - **Request validation** — `validate()` middleware with typed schema for body, query, and params supports string, number, boolean, email, url, uuid, date, and custom validators
30
+ - **CSRF protection** — double-submit cookie pattern with HMAC tokens, automatic rotation, and configurable paths
31
+ - **Error handling** — `HttpError` classes for every common status code, `errorHandler()` middleware with dev/production formatting, and `createError()` factory
32
+ - **Debug logging** — namespaced logger with levels (trace → fatal), `DEBUG=app:*` pattern matching, colors, timestamps, and JSON mode
33
+ - **Router sub-apps** — `Router()` with nested mounting, route chaining, wildcard/param patterns, protocol-aware routing, and full introspection
34
+ - **Request & response helpers** — content negotiation, cookies, caching, range parsing, file downloads, redirects, and more
35
+ - **Tiny `fetch` replacement** — server-side HTTP/HTTPS client with TLS passthrough, progress callbacks, and abort support
36
+ - **Security built-in** — CRLF injection prevention, prototype pollution filtering, path traversal guards, filename sanitization
34
37
 
35
38
  ```bash
36
39
  npm install zero-http
37
40
  ```
38
41
 
39
- ## Quick start
42
+ ## Quick Start
40
43
 
41
44
  ```js
42
45
  const { createApp, json } = require('zero-http')
@@ -44,55 +47,103 @@ const app = createApp()
44
47
 
45
48
  app.use(json())
46
49
  app.post('/echo', (req, res) => res.json({ received: req.body }))
47
- app.listen(3000)
50
+ app.listen(3000, () => console.log('Listening on :3000'))
48
51
  ```
49
52
 
50
53
  ## Demo
51
54
 
52
- You can view the live documentation and playground at https://z-http.com, or run the demo locally:
55
+ Live documentation and playground at **https://z-http.com**, or run locally:
53
56
 
54
57
  ```bash
55
58
  npm run docs
56
59
  # open http://localhost:3000
57
60
  ```
58
61
 
62
+ ---
63
+
59
64
  ## API Reference
60
65
 
66
+ ### Exports
67
+
61
68
  All exports are available from the package root:
62
69
 
63
70
  ```js
64
71
  const {
65
- createApp, Router, cors, fetch,
66
- json, urlencoded, text, raw, multipart,
67
- static: serveStatic, rateLimit, logger, compress,
68
- WebSocketConnection, SSEStream
72
+ createApp, Router, cors, fetch,
73
+ json, urlencoded, text, raw, multipart,
74
+ static: serveStatic,
75
+ rateLimit, logger, compress,
76
+ helmet, timeout, requestId, cookieParser,
77
+ csrf, validate, errorHandler,
78
+ env, Database, Model, TYPES, Query,
79
+ HttpError, NotFoundError, BadRequestError, ValidationError, createError, isHttpError,
80
+ debug,
81
+ WebSocketConnection, WebSocketPool, SSEStream
69
82
  } = require('zero-http')
70
83
  ```
71
84
 
72
85
  | Export | Type | Description |
73
86
  |---|---|---|
74
- | `createApp()` | function | Create a new application instance (router + middleware stack). |
75
- | `Router()` | function | Create a standalone Router for modular route grouping. |
76
- | `cors` | function | CORS middleware factory. |
77
- | `fetch` | function | Node HTTP/HTTPS client with TLS options passthrough. |
87
+ | `createApp()` | function | Create a new application instance. |
88
+ | `Router()` | function | Create a standalone router for modular route grouping. |
78
89
  | `json` | function | JSON body parser factory. |
79
90
  | `urlencoded` | function | URL-encoded body parser factory. |
80
91
  | `text` | function | Text body parser factory. |
81
- | `raw` | function | Raw bytes parser factory. |
82
- | `multipart` | function | Streaming multipart parser factory. |
92
+ | `raw` | function | Raw/binary body parser factory. |
93
+ | `multipart` | function | Streaming multipart/form-data parser factory. |
83
94
  | `static` | function | Static file serving middleware factory. |
95
+ | `cors` | function | CORS middleware factory. |
96
+ | `helmet` | function | Security headers middleware factory. |
97
+ | `compress` | function | Response compression middleware (brotli/gzip/deflate). |
84
98
  | `rateLimit` | function | In-memory rate-limiting middleware factory. |
85
99
  | `logger` | function | Request-logging middleware factory. |
86
- | `compress` | function | Response compression middleware (brotli/gzip/deflate). |
87
- | `WebSocketConnection` | class | WebSocket connection wrapper (for advanced usage). |
88
- | `SSEStream` | class | SSE stream controller (for advanced usage). |
89
-
90
- ### createApp() methods
100
+ | `timeout` | function | Request timeout middleware factory. |
101
+ | `requestId` | function | Request ID middleware factory. |
102
+ | `cookieParser` | function | Cookie parsing middleware factory. |
103
+ | `csrf` | function | CSRF protection middleware factory. |
104
+ | `validate` | function | Request validation middleware factory. |
105
+ | `errorHandler` | function | Configurable error-handling middleware factory. |
106
+ | `env` | proxy | Typed environment variable loader and accessor. |
107
+ | `Database` | class | ORM database connection factory. |
108
+ | `Model` | class | Base model class for defining database entities. |
109
+ | `TYPES` | enum | Column type constants for model schemas. |
110
+ | `Query` | class | Fluent query builder. |
111
+ | `HttpError` | class | Base HTTP error class with status code. |
112
+ | `BadRequestError` | class | 400 error. |
113
+ | `UnauthorizedError` | class | 401 error. |
114
+ | `ForbiddenError` | class | 403 error. |
115
+ | `NotFoundError` | class | 404 error. |
116
+ | `MethodNotAllowedError` | class | 405 error. |
117
+ | `ConflictError` | class | 409 error. |
118
+ | `GoneError` | class | 410 error. |
119
+ | `PayloadTooLargeError` | class | 413 error. |
120
+ | `UnprocessableEntityError` | class | 422 error. |
121
+ | `ValidationError` | class | 422 error with field-level details. |
122
+ | `TooManyRequestsError` | class | 429 error. |
123
+ | `InternalError` | class | 500 error. |
124
+ | `NotImplementedError` | class | 501 error. |
125
+ | `BadGatewayError` | class | 502 error. |
126
+ | `ServiceUnavailableError` | class | 503 error. |
127
+ | `createError` | function | Create an `HttpError` by status code. |
128
+ | `isHttpError` | function | Check if a value is an `HttpError` instance. |
129
+ | `debug` | function | Namespaced debug logger factory. |
130
+ | `fetch` | function | Server-side HTTP/HTTPS client. |
131
+ | `WebSocketConnection` | class | WebSocket connection wrapper. |
132
+ | `WebSocketPool` | class | WebSocket connection & room manager. |
133
+ | `SSEStream` | class | SSE stream controller. |
134
+
135
+ ---
136
+
137
+ ### createApp()
138
+
139
+ Creates an application instance with a middleware pipeline, router, and server lifecycle.
140
+
141
+ #### Methods
91
142
 
92
143
  | Method | Signature | Description |
93
144
  |---|---|---|
94
- | `use` | `use(fn)` or `use(path, fn)` or `use(path, router)` | Register middleware globally, scoped to a path prefix, or mount a sub-router. |
95
- | `get` | `get(path, [opts], ...handlers)` | Register GET route handlers. Optional `{ secure }` options object. |
145
+ | `use` | `use(fn)` / `use(path, fn)` / `use(path, router)` | Register middleware globally, scoped to a path prefix, or mount a sub-router. Path-scoped middleware strips the prefix before calling downstream. |
146
+ | `get` | `get(path, [opts], ...handlers)` | Register GET route handlers. |
96
147
  | `post` | `post(path, [opts], ...handlers)` | Register POST route handlers. |
97
148
  | `put` | `put(path, [opts], ...handlers)` | Register PUT route handlers. |
98
149
  | `delete` | `delete(path, [opts], ...handlers)` | Register DELETE route handlers. |
@@ -102,303 +153,840 @@ const {
102
153
  | `all` | `all(path, [opts], ...handlers)` | Register handlers for ALL HTTP methods. |
103
154
  | `ws` | `ws(path, [opts], handler)` | Register a WebSocket upgrade handler. |
104
155
  | `onError` | `onError(fn)` | Register a global error handler `fn(err, req, res, next)`. |
105
- | `listen` | `listen(port, [tlsOpts], [cb])` | Start HTTP or HTTPS server. Pass `{ key, cert }` for TLS. |
156
+ | `listen` | `listen(port, [tlsOpts], [cb])` | Start HTTP or HTTPS server. Returns the underlying `http.Server`. |
106
157
  | `close` | `close([cb])` | Gracefully close the server. |
107
- | `routes` | `routes()` | Return a flat list of all registered routes (introspection). |
108
- | `handler` | property | Bound request handler for `http.createServer(app.handler)`. |
158
+ | `routes` | `routes()` | Return a flat list of all registered routes including sub-routers and WebSocket handlers. |
159
+ | `handler` | property | Bound `(req, res)` handler for use with `http.createServer(app.handler)`. |
109
160
 
110
- ### Request (`req`) properties & helpers
161
+ **Route options object** pass as the first argument after the path:
111
162
 
112
- | Property / Method | Type | Description |
113
- |---|---|---|
114
- | `method` | string | HTTP method (GET, POST, etc.). |
115
- | `url` | string | Request URL (path + query). |
116
- | `headers` | object | Raw request headers. |
117
- | `query` | object | Parsed query string. |
118
- | `params` | object | Route parameters (populated by router). |
119
- | `body` | any | Parsed body (populated by body parsers). |
120
- | `ip` | string | Remote IP address of the client. |
121
- | `secure` | boolean | `true` when the connection is over TLS (HTTPS). |
122
- | `protocol` | string | `'https'` or `'http'`. |
123
- | `get(name)` | function | Get a request header (case-insensitive). |
124
- | `is(type)` | function | Check if Content-Type matches a type (e.g. `'json'`, `'text/html'`). |
125
- | `raw` | object | Underlying `http.IncomingMessage`. |
163
+ ```js
164
+ // HTTPS-only route
165
+ app.get('/admin', { secure: true }, (req, res) => res.json({ admin: true }))
126
166
 
127
- ### Response (`res`) helpers
167
+ // HTTP-only route
168
+ app.get('/public', { secure: false }, (req, res) => res.json({ public: true }))
169
+ ```
128
170
 
129
- | Method | Signature | Description |
130
- |---|---|---|
131
- | `status` | `status(code)` | Set HTTP status code. Chainable. |
132
- | `set` | `set(name, value)` | Set a response header. Chainable. |
133
- | `get` | `get(name)` | Get a previously-set response header. |
134
- | `type` | `type(ct)` | Set Content-Type (accepts shorthand like `'json'`, `'html'`, `'text'`). Chainable. |
135
- | `send` | `send(body)` | Send a response; auto-detects Content-Type for strings, objects, and Buffers. |
136
- | `json` | `json(obj)` | Set JSON Content-Type and send object. |
137
- | `text` | `text(str)` | Set text/plain and send string. |
138
- | `html` | `html(str)` | Set text/html and send string. |
139
- | `redirect` | `redirect([status], url)` | Redirect to URL (default 302). |
140
- | `sse` | `sse([opts])` | Open a Server-Sent Events stream. Returns an `SSEStream` controller. |
171
+ > **Tip:** Use `app.handler` when you need manual control over the HTTP server, e.g. sharing with other libraries or running on a custom port.
141
172
 
142
- ### WebSocket — `app.ws(path, [opts], handler)`
173
+ ---
143
174
 
144
- Register a WebSocket upgrade handler. The handler receives `(ws, req)` where `ws` is a `WebSocketConnection`.
175
+ ### Request (`req`)
145
176
 
146
- | Option | Type | Default | Description |
147
- |---|---:|---|---|
148
- | `maxPayload` | number | `1048576` | Maximum incoming frame size in bytes (1 MB). |
149
- | `pingInterval` | number | `30000` | Auto-ping interval in ms. `0` to disable. |
150
- | `verifyClient` | function | — | `(req) => boolean` — return `false` to reject the upgrade with 403. |
177
+ Every handler receives an enhanced request object wrapping the native `http.IncomingMessage`.
151
178
 
152
- **WebSocketConnection properties:**
179
+ #### Properties
153
180
 
154
181
  | Property | Type | Description |
155
182
  |---|---|---|
156
- | `id` | string | Unique connection ID (e.g. `ws_1_l8x3k`). |
157
- | `readyState` | number | 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED. |
158
- | `protocol` | string | Negotiated sub-protocol. |
159
- | `headers` | object | Upgrade request headers. |
160
- | `ip` | string | Remote IP address. |
161
- | `query` | object | Parsed query params from the upgrade URL. |
162
- | `url` | string | Full upgrade URL. |
163
- | `secure` | boolean | `true` for WSS connections. |
164
- | `extensions` | string | Requested WebSocket extensions header. |
165
- | `maxPayload` | number | Max incoming payload bytes. |
166
- | `connectedAt` | number | Timestamp (ms) of connection. |
167
- | `uptime` | number | Milliseconds since connection (computed). |
168
- | `bufferedAmount` | number | Bytes waiting to be flushed. |
169
- | `data` | object | Arbitrary user-data store. |
183
+ | `method` | string | HTTP method (`GET`, `POST`, etc.). |
184
+ | `url` | string | Full request URL including query string. |
185
+ | `path` | string | URL path without query string. |
186
+ | `headers` | object | Lowercased request headers. |
187
+ | `query` | object | Parsed query string key-value pairs. |
188
+ | `params` | object | Named route parameters (e.g. `{ id: '42' }` from `/users/:id`). |
189
+ | `body` | any | Parsed request body (populated by body parsers). |
190
+ | `ip` | string\|null | Remote IP address. |
191
+ | `secure` | boolean | `true` when connection is over TLS. |
192
+ | `protocol` | string | `'https'` or `'http'`. |
193
+ | `cookies` | object | Parsed cookies (populated by `cookieParser()`). |
194
+ | `signedCookies` | object | Verified signed cookies (populated by `cookieParser(secret)`). |
195
+ | `locals` | object | Request-scoped data store shared between middleware and handlers. |
196
+ | `raw` | object | Underlying `http.IncomingMessage` for advanced use. |
170
197
 
171
- **WebSocketConnection methods:**
198
+ #### Getters
172
199
 
173
- | Method | Description |
174
- |---|---|
175
- | `send(data, [opts])` | Send text or binary message. `opts.binary` to force binary frame. |
176
- | `sendJSON(obj)` | Send JSON-serialised text message. |
177
- | `ping([payload])` | Send a ping frame. |
178
- | `pong([payload])` | Send a pong frame. |
179
- | `close([code], [reason])` | Graceful close with optional status code. |
180
- | `terminate()` | Forcefully destroy the socket. |
181
- | `on(event, fn)` | Listen for `'message'`, `'close'`, `'error'`, `'ping'`, `'pong'`, `'drain'`. |
182
- | `once(event, fn)` | One-time event listener. |
183
- | `off(event, fn)` | Remove a listener. |
184
- | `removeAllListeners([event])` | Remove all listeners for an event, or all events. |
185
- | `listenerCount(event)` | Return the number of listeners for an event. |
186
-
187
- Example:
188
-
189
- ```js
190
- app.ws('/chat', { maxPayload: 64 * 1024 }, (ws, req) => {
191
- ws.send('Welcome!')
192
- ws.on('message', data => ws.send('echo: ' + data))
193
- ws.on('close', () => console.log(ws.id, 'left'))
200
+ | Getter | Returns | Description |
201
+ |---|---|---|
202
+ | `hostname` | string\|undefined | Hostname from `Host` header (strips port). |
203
+ | `fresh` | boolean | `true` if client cache is still valid (based on `ETag`/`If-Modified-Since`). |
204
+ | `stale` | boolean | Inverse of `fresh`. |
205
+ | `xhr` | boolean | `true` if `X-Requested-With: XMLHttpRequest` is present. |
206
+
207
+ #### Methods
208
+
209
+ | Method | Signature | Returns | Description |
210
+ |---|---|---|---|
211
+ | `get` | `get(name)` | string\|undefined | Get a request header (case-insensitive). |
212
+ | `is` | `is(type)` | boolean | Check if `Content-Type` matches. Accepts shorthand (`'json'`, `'html'`) or full MIME. |
213
+ | `accepts` | `accepts(...types)` | string\|false | Content negotiation — returns the first acceptable type from the `Accept` header, or `false`. |
214
+ | `subdomains` | `subdomains(offset = 2)` | string[] | Extract subdomains. E.g. `'v2.api.example.com'` → `['api', 'v2']`. |
215
+ | `range` | `range(size)` | object\|number | Parse the `Range` header. Returns `{ type, ranges: [{ start, end }] }`, `-1` (unsatisfiable), or `-2` (malformed). |
216
+
217
+ ```js
218
+ app.get('/api/data', (req, res) => {
219
+ // Content negotiation
220
+ if (req.accepts('json')) return res.json({ data: [] })
221
+ if (req.accepts('html')) return res.html('<ul></ul>')
222
+ res.status(406).send('Not Acceptable')
223
+ })
224
+
225
+ // Conditional GET
226
+ app.get('/resource', (req, res) => {
227
+ if (req.fresh) return res.sendStatus(304)
228
+ res.set('ETag', '"v1"').json({ data: 'new' })
229
+ })
230
+
231
+ // Range requests for streaming
232
+ app.get('/video', (req, res) => {
233
+ const r = req.range(fileSize)
234
+ if (r === -1) return res.sendStatus(416)
235
+ // serve partial content...
194
236
  })
195
237
  ```
196
238
 
197
- ### Server-Sent Events — `res.sse([opts])`
239
+ ---
198
240
 
199
- Open an SSE stream. Returns an `SSEStream` controller.
241
+ ### Response (`res`)
200
242
 
201
- | Option | Type | Default | Description |
202
- |---|---:|---|---|
203
- | `status` | number | `200` | HTTP status code for the SSE response. |
204
- | `retry` | number | — | Reconnection interval hint (ms) sent to client. |
205
- | `keepAlive` | number | `0` | Auto keep-alive comment interval (ms). |
206
- | `keepAliveComment` | string | `'ping'` | Comment text sent by the keep-alive timer. |
207
- | `autoId` | boolean | `false` | Auto-increment event IDs. |
208
- | `startId` | number | `1` | Starting value for auto-IDs. |
209
- | `pad` | number | `0` | Bytes of initial padding (helps flush proxy buffers). |
210
- | `headers` | object | — | Additional response headers. |
243
+ Every handler receives an enhanced response object wrapping `http.ServerResponse`.
211
244
 
212
- **SSEStream methods:**
245
+ #### Properties
213
246
 
214
- | Method | Description |
215
- |---|---|
216
- | `send(data, [id])` | Send an unnamed data event. Objects are auto-JSON-serialised. |
217
- | `sendJSON(obj, [id])` | Alias for `send()` with an object. |
218
- | `event(name, data, [id])` | Send a named event. |
219
- | `comment(text)` | Send a comment line (keep-alive or debug). |
220
- | `retry(ms)` | Update the reconnection interval hint. |
221
- | `keepAlive(ms, [comment])` | Start/restart a keep-alive timer. |
222
- | `flush()` | Flush buffered data through proxies. |
223
- | `close()` | Close the stream from the server side. |
224
- | `on(event, fn)` | Listen for `'close'` or `'error'` events. |
225
- | `once(event, fn)` | One-time event listener. |
226
- | `off(event, fn)` | Remove a specific event listener. |
227
- | `removeAllListeners([event])` | Remove all listeners for an event, or all events. |
228
- | `listenerCount(event)` | Return the number of listeners for an event. |
247
+ | Property | Type | Description |
248
+ |---|---|---|
249
+ | `locals` | object | Shared request-scoped data store (same object as `req.locals`). |
250
+ | `headersSent` | boolean | Whether response headers have already been flushed. |
251
+
252
+ #### Chainable Methods
253
+
254
+ These return `this` so you can chain calls:
229
255
 
230
- **SSEStream properties:** `connected`, `eventCount`, `bytesSent`, `connectedAt`, `uptime`, `lastEventId`, `secure`, `data`.
256
+ | Method | Signature | Description |
257
+ |---|---|---|
258
+ | `status` | `status(code)` | Set the HTTP status code. |
259
+ | `set` | `set(name, value)` | Set a response header. Throws on CRLF injection attempt. |
260
+ | `type` | `type(contentType)` | Set `Content-Type`. Accepts shorthand: `'json'`, `'html'`, `'text'`, `'xml'`, `'form'`, `'bin'`. |
261
+ | `append` | `append(name, value)` | Append to a header (comma-separated if it already exists). |
262
+ | `vary` | `vary(field)` | Add a field to the `Vary` header. |
263
+ | `cookie` | `cookie(name, value, [opts])` | Set an HTTP cookie on the response. |
264
+ | `clearCookie` | `clearCookie(name, [opts])` | Expire a cookie (must match `path`/`domain` of original). |
231
265
 
232
- **SSEStream events:** `'close'` (client disconnect or server close), `'error'` (write error).
266
+ #### Terminal Methods
233
267
 
234
- Example:
268
+ These finalize and send the response (not chainable):
269
+
270
+ | Method | Signature | Description |
271
+ |---|---|---|
272
+ | `send` | `send(body)` | Send response. Auto-detects Content-Type: Buffer → `application/octet-stream`, string → `text/html`, object → JSON. |
273
+ | `json` | `json(obj)` | Send JSON response with `application/json` Content-Type. |
274
+ | `text` | `text(str)` | Send plain text response. |
275
+ | `html` | `html(str)` | Send HTML response. |
276
+ | `sendStatus` | `sendStatus(code)` | Send the status code with the standard reason phrase as the body. |
277
+ | `redirect` | `redirect([status], url)` | Redirect to a URL (default `302`). |
278
+ | `sendFile` | `sendFile(path, [opts], [cb])` | Stream a file to the response. Options: `root` (base directory), `headers` (additional headers). |
279
+ | `download` | `download(path, [filename], [cb])` | Prompt a file download. Sets `Content-Disposition: attachment`. |
280
+ | `sse` | `sse([opts])` | Open an SSE stream. Returns an `SSEStream` controller. |
281
+ | `get` | `get(name)` | Get a previously-set response header. |
282
+
283
+ #### `res.cookie()` Options
284
+
285
+ | Option | Type | Default | Description |
286
+ |---|---|---|---|
287
+ | `domain` | string | — | Cookie domain. |
288
+ | `path` | string | `'/'` | Cookie path. |
289
+ | `expires` | Date | — | Expiration date. |
290
+ | `maxAge` | number | — | Max age in **seconds** (takes precedence over `expires`). |
291
+ | `httpOnly` | boolean | `true` | Prevent client-side JavaScript access. |
292
+ | `secure` | boolean | — | Only send over HTTPS. |
293
+ | `sameSite` | string | `'Lax'` | `'Strict'`, `'Lax'`, or `'None'`. |
235
294
 
236
295
  ```js
237
- app.get('/events', (req, res) => {
238
- const sse = res.sse({ retry: 5000, keepAlive: 30000, autoId: true })
239
- sse.send('hello')
240
- sse.event('update', { x: 1 })
241
- sse.on('close', () => console.log('client disconnected'))
242
- })
296
+ // Set a session cookie
297
+ res.cookie('session', token, { maxAge: 3600, httpOnly: true, secure: true })
298
+
299
+ // Set and send
300
+ res.status(200).set('X-Custom', 'value').json({ ok: true })
301
+
302
+ // File download
303
+ res.download('/reports/q4.pdf', 'Q4-Report.pdf')
304
+
305
+ // Status-only response
306
+ res.sendStatus(204) // sends "No Content"
243
307
  ```
244
308
 
245
- ### Router sub-apps
309
+ ---
246
310
 
247
- Create modular route groups with `Router()`:
311
+ ### Router
312
+
313
+ Create modular route groups with `Router()`. Routers support all the same HTTP verb methods as `createApp()`, plus mounting and chaining.
314
+
315
+ #### Methods
316
+
317
+ | Method | Signature | Description |
318
+ |---|---|---|
319
+ | `get/post/put/delete/patch/options/head/all` | `(path, [opts], ...handlers)` | Register route handlers. Chainable (returns `this`). |
320
+ | `use` | `use(prefix, router)` | Mount a child router at a prefix. |
321
+ | `route` | `route(path)` | Returns a chainable object for registering multiple methods on one path. |
322
+ | `inspect` | `inspect([prefix])` | Return a flat list of all routes. |
323
+
324
+ #### Route Patterns
325
+
326
+ | Pattern | Example | `req.params` |
327
+ |---|---|---|
328
+ | Named parameter | `/users/:id` | `{ id: '42' }` |
329
+ | Multiple params | `/users/:userId/posts/:postId` | `{ userId: '1', postId: '5' }` |
330
+ | Wildcard catch-all | `/files/*` | `{ '0': 'path/to/file.txt' }` |
248
331
 
249
332
  ```js
250
333
  const { createApp, Router, json } = require('zero-http')
251
334
  const app = createApp()
252
- const api = Router()
253
335
 
336
+ // Modular API router
337
+ const api = Router()
254
338
  api.get('/users', (req, res) => res.json([]))
255
339
  api.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
340
+ api.post('/users', (req, res) => res.status(201).json(req.body))
341
+
342
+ // Nested router
343
+ const v2 = Router()
344
+ v2.get('/health', (req, res) => res.json({ status: 'ok', version: 2 }))
345
+ api.use('/v2', v2)
256
346
 
257
347
  app.use(json())
258
348
  app.use('/api', api)
259
- app.listen(3000)
349
+
350
+ // Route chaining — multiple methods on one path
351
+ const items = Router()
352
+ items.route('/items')
353
+ .get((req, res) => res.json([]))
354
+ .post((req, res) => res.status(201).json(req.body))
355
+ .delete((req, res) => res.sendStatus(204))
356
+ app.use(items)
260
357
 
261
358
  // Introspection
262
359
  console.log(app.routes())
263
- // [{ method: 'GET', path: '/api/users' }, { method: 'GET', path: '/api/users/:id' }]
360
+ // [{ method: 'GET', path: '/api/users' }, { method: 'GET', path: '/api/users/:id' }, ...]
264
361
  ```
265
362
 
266
- Route chaining:
363
+ ---
364
+
365
+ ### Body Parsers
366
+
367
+ All body parsers accept these common options:
368
+
369
+ | Option | Type | Default | Description |
370
+ |---|---|---|---|
371
+ | `limit` | number\|string | `'1mb'` | Maximum body size. Accepts bytes or unit strings like `'10kb'`, `'2mb'`. |
372
+ | `type` | string\|function | (varies) | Content-Type to match. String pattern or `(contentType) => boolean`. |
373
+ | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with `403 HTTPS required`. |
374
+
375
+ #### json([opts])
376
+
377
+ Parse JSON request bodies into `req.body`.
378
+
379
+ | Option | Default | Description |
380
+ |---|---|---|
381
+ | `strict` | `true` | Only accept objects and arrays (reject primitive roots). |
382
+ | `reviver` | — | Custom reviver function passed to `JSON.parse()`. |
267
383
 
268
384
  ```js
269
- const router = Router()
270
- router.route('/items')
271
- .get((req, res) => res.json([]))
272
- .post((req, res) => res.status(201).json(req.body))
385
+ app.use(json({ limit: '500kb', strict: true }))
273
386
  ```
274
387
 
275
- Protocol-aware routes:
388
+ #### urlencoded([opts])
389
+
390
+ Parse URL-encoded form bodies into `req.body`.
391
+
392
+ | Option | Default | Description |
393
+ |---|---|---|
394
+ | `extended` | `false` | Enable nested bracket syntax: `a[b][c]=1` → `{ a: { b: { c: '1' } } }`. |
395
+
396
+ > **Security:** When `extended: true`, keys containing `__proto__`, `constructor`, or `prototype` are automatically filtered to prevent prototype pollution.
276
397
 
277
398
  ```js
278
- // Only matches HTTPS requests
279
- app.get('/secret', { secure: true }, (req, res) => res.json({ secret: 42 }))
280
- // Only matches plain HTTP
281
- app.get('/public', { secure: false }, (req, res) => res.json({ public: true }))
399
+ app.use(urlencoded({ extended: true }))
400
+ ```
401
+
402
+ #### text([opts])
403
+
404
+ Read the body as a plain string into `req.body`.
405
+
406
+ | Option | Default | Description |
407
+ |---|---|---|
408
+ | `encoding` | `'utf8'` | Character encoding. |
409
+
410
+ #### raw([opts])
411
+
412
+ Read the body as a `Buffer` into `req.body`.
413
+
414
+ #### multipart([opts])
415
+
416
+ Stream file uploads to disk and collect form fields.
417
+
418
+ | Option | Default | Description |
419
+ |---|---|---|
420
+ | `dir` | `os.tmpdir()/zero-http-uploads` | Upload directory (created automatically). |
421
+ | `maxFileSize` | — | Maximum file size in bytes. Sends `413` on exceed. |
422
+
423
+ Sets `req.body = { fields, files }` where each file entry has: `originalFilename`, `storedName`, `path`, `contentType`, `size`.
424
+
425
+ > **Security:** Filenames are sanitized — path traversal sequences, null bytes, and unsafe characters are stripped.
426
+
427
+ ```js
428
+ app.post('/upload', multipart({ dir: './uploads', maxFileSize: 5 * 1024 * 1024 }), (req, res) => {
429
+ res.json({ files: req.body.files, fields: req.body.fields })
430
+ })
431
+ ```
432
+
433
+ ---
434
+
435
+ ### Middleware
436
+
437
+ #### helmet([opts])
438
+
439
+ Set security-related HTTP headers. All options can be set to `false` to disable.
440
+
441
+ | Option | Type | Default | Description |
442
+ |---|---|---|---|
443
+ | `contentSecurityPolicy` | object\|false | (permissive CSP) | CSP directives object, or `false` to disable. |
444
+ | `crossOriginOpenerPolicy` | string\|false | `'same-origin'` | Cross-Origin-Opener-Policy value. |
445
+ | `crossOriginResourcePolicy` | string\|false | `'same-origin'` | Cross-Origin-Resource-Policy value. |
446
+ | `crossOriginEmbedderPolicy` | boolean | `false` | Set COEP to `require-corp`. |
447
+ | `dnsPrefetchControl` | boolean | `true` | Set `X-DNS-Prefetch-Control: off`. |
448
+ | `frameguard` | string\|false | `'deny'` | `X-Frame-Options` — `'deny'` or `'sameorigin'`. |
449
+ | `hidePoweredBy` | boolean | `true` | Remove the `X-Powered-By` header. |
450
+ | `hsts` | boolean | `true` | Enable `Strict-Transport-Security`. |
451
+ | `hstsMaxAge` | number | `15552000` | HSTS max-age in seconds (~180 days). |
452
+ | `hstsIncludeSubDomains` | boolean | `true` | Include subdomains in HSTS. |
453
+ | `hstsPreload` | boolean | `false` | Add the `preload` directive. |
454
+ | `ieNoOpen` | boolean | `true` | Set `X-Download-Options: noopen`. |
455
+ | `noSniff` | boolean | `true` | Set `X-Content-Type-Options: nosniff`. |
456
+ | `permittedCrossDomainPolicies` | string\|false | `'none'` | `X-Permitted-Cross-Domain-Policies` value. |
457
+ | `referrerPolicy` | string\|false | `'no-referrer'` | `Referrer-Policy` value. |
458
+ | `xssFilter` | boolean | `false` | Set `X-XSS-Protection: 1; mode=block` (legacy). |
459
+
460
+ ```js
461
+ app.use(helmet())
462
+
463
+ // Customized
464
+ app.use(helmet({
465
+ frameguard: 'sameorigin',
466
+ hsts: false,
467
+ contentSecurityPolicy: false,
468
+ referrerPolicy: 'same-origin'
469
+ }))
470
+ ```
471
+
472
+ #### cors([opts])
473
+
474
+ CORS middleware with automatic preflight (`OPTIONS → 204`) handling.
475
+
476
+ | Option | Type | Default | Description |
477
+ |---|---|---|---|
478
+ | `origin` | string\|array | `'*'` | Allowed origin(s). Use an array for multiple origins, or a `.suffix` string for subdomain matching. |
479
+ | `methods` | string | `'GET,POST,PUT,DELETE,OPTIONS'` | Allowed HTTP methods. |
480
+ | `allowedHeaders` | string | `'Content-Type,Authorization'` | Headers the client can send. |
481
+ | `exposedHeaders` | string | — | Headers the browser can read from the response. |
482
+ | `credentials` | boolean | `false` | Set `Access-Control-Allow-Credentials: true`. |
483
+ | `maxAge` | number | — | Preflight cache duration in seconds. |
484
+
485
+ ```js
486
+ app.use(cors({ origin: ['https://app.example.com', 'https://admin.example.com'], credentials: true }))
282
487
  ```
283
488
 
284
- ### Response compression`compress([opts])`
489
+ > **Tip:** When using `credentials: true`, you must specify explicit origins browsers reject `*` with credentials.
490
+
491
+ #### compress([opts])
285
492
 
286
- Negotiates the best encoding from `Accept-Encoding`: brotli (`br`) > gzip > deflate. Brotli requires Node ≥ 11.7. Automatically skips SSE (`text/event-stream`) streams.
493
+ Response compression middleware. Negotiates the best encoding from `Accept-Encoding`: brotli > gzip > deflate. Brotli requires Node ≥ 11.7. Automatically skips SSE streams.
287
494
 
288
495
  | Option | Type | Default | Description |
289
- |---|---:|---|---|
290
- | `threshold` | number | `1024` | Minimum response size in bytes before compressing. |
496
+ |---|---|---|---|
497
+ | `threshold` | number | `1024` | Minimum response size (bytes) before compressing. |
291
498
  | `level` | number | `-1` | zlib compression level (1–9, or -1 for default). |
292
499
  | `filter` | function | — | `(req, res) => boolean` — return `false` to skip compression. |
293
500
 
294
501
  ```js
295
- app.use(compress({ threshold: 512 }))
502
+ app.use(compress({ threshold: 512, level: 6 }))
296
503
  ```
297
504
 
298
- ### Body parsers
505
+ #### rateLimit([opts])
299
506
 
300
- All body parsers accept a `requireSecure` option. When `true`, non-HTTPS requests are rejected with `403 HTTPS required`.
507
+ In-memory per-IP rate limiter with sliding window.
301
508
 
302
- #### json([opts])
509
+ | Option | Type | Default | Description |
510
+ |---|---|---|---|
511
+ | `windowMs` | number | `60000` | Time window in milliseconds. |
512
+ | `max` | number | `100` | Max requests per window per key. |
513
+ | `message` | string | `'Too many requests…'` | Error message body. |
514
+ | `statusCode` | number | `429` | HTTP status for rate-limited responses. |
515
+ | `keyGenerator` | function | `(req) => req.ip` | Custom key extraction function. |
516
+
517
+ Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers on every response. Adds `Retry-After` when the limit is exceeded.
518
+
519
+ ```js
520
+ // Strict API rate limiting
521
+ app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))
522
+
523
+ // Custom key (e.g. by API key instead of IP)
524
+ app.use(rateLimit({ keyGenerator: (req) => req.get('x-api-key') || req.ip }))
525
+ ```
526
+
527
+ > **Note:** Rate limit state is in-memory and resets on server restart. For distributed systems, use an external store.
528
+
529
+ #### logger([opts])
530
+
531
+ Request logger that logs method, URL, status code, and response time.
303
532
 
304
533
  | Option | Type | Default | Description |
305
- |---|---:|---|---|
306
- | `limit` | number\|string | none | Maximum body size (bytes or unit string like `'1mb'`). |
307
- | `reviver` | function | | Function passed to `JSON.parse` for custom reviving. |
308
- | `strict` | boolean | `true` | When `true` only accepts objects/arrays (rejects primitives). |
309
- | `type` | string\|function | `'application/json'` | MIME matcher for the parser. |
310
- | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
534
+ |---|---|---|---|
535
+ | `format` | string | `'dev'` | `'dev'` (colorized with status), `'short'`, or `'tiny'`. |
536
+ | `logger` | function | `console.log` | Custom log output function. |
537
+ | `colors` | boolean | auto (TTY) | Enable/disable ANSI colors. |
311
538
 
312
- #### urlencoded([opts])
539
+ ```js
540
+ app.use(logger({ format: 'dev' }))
541
+ ```
542
+
543
+ #### timeout(ms, [opts])
544
+
545
+ Automatically send a timeout response if the handler doesn't respond within the time limit.
313
546
 
314
547
  | Option | Type | Default | Description |
315
- |---|---:|---|---|
316
- | `extended` | boolean | `false` | When `true` supports nested bracket syntax (`a[b]=1`). |
317
- | `limit` | number\|string | none | Maximum body size. |
318
- | `type` | string\|function | `'application/x-www-form-urlencoded'` | MIME matcher. |
319
- | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
548
+ |---|---|---|---|
549
+ | `ms` (1st arg) | number | `30000` | Timeout in milliseconds. |
550
+ | `status` | number | `408` | HTTP status code for timeout response. |
551
+ | `message` | string | `'Request Timeout'` | Error message body. |
320
552
 
321
- #### text([opts])
553
+ Sets `req.timedOut = true` on timeout so downstream handlers can check.
554
+
555
+ ```js
556
+ app.use(timeout(5000))
557
+
558
+ app.get('/slow', (req, res) => {
559
+ // Long operation...
560
+ if (req.timedOut) return // response already sent
561
+ res.json({ result })
562
+ })
563
+ ```
564
+
565
+ #### requestId([opts])
566
+
567
+ Generate or extract a unique request ID for tracing.
322
568
 
323
569
  | Option | Type | Default | Description |
324
- |---|---:|---|---|
325
- | `type` | string\|function | `text/*` | MIME matcher for text bodies. |
326
- | `limit` | number\|string | none | Maximum body size. |
327
- | `encoding` | string | `utf8` | Character encoding used to decode bytes. |
328
- | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
570
+ |---|---|---|---|
571
+ | `header` | string | `'X-Request-Id'` | Response header name. |
572
+ | `generator` | function | UUID v4 | Custom ID generator `() => string`. |
573
+ | `trustProxy` | boolean | `false` | Trust and reuse the incoming `X-Request-Id` header. |
329
574
 
330
- #### raw([opts])
575
+ Sets `req.id` and adds the ID to the response header.
576
+
577
+ ```js
578
+ app.use(requestId())
579
+
580
+ app.get('/test', (req, res) => {
581
+ console.log('Request ID:', req.id)
582
+ res.json({ id: req.id })
583
+ })
584
+ ```
585
+
586
+ #### cookieParser([secret], [opts])
587
+
588
+ Parse the `Cookie` header into `req.cookies`. Optionally verify signed cookies.
589
+
590
+ | Parameter | Type | Description |
591
+ |---|---|---|
592
+ | `secret` | string\|string[] | Secret(s) for signed cookie verification. |
593
+ | `options.decode` | boolean | URI-decode cookie values (default `true`). |
594
+
595
+ Populates `req.cookies` (all cookies) and `req.signedCookies` (verified signed cookies only). Failed signatures are silently excluded.
596
+
597
+ **Static method:** `cookieParser.sign(value, secret)` — manually sign a value. Returns `'s:<value>.<HMAC-SHA256>'`.
598
+
599
+ ```js
600
+ const secret = 'my-secret'
601
+ app.use(cookieParser(secret))
602
+
603
+ app.get('/profile', (req, res) => {
604
+ const user = req.signedCookies.user // verified
605
+ const theme = req.cookies.theme // unsigned
606
+ res.json({ user, theme })
607
+ })
608
+
609
+ app.post('/login', (req, res) => {
610
+ const signed = cookieParser.sign(req.body.username, secret)
611
+ res.cookie('user', signed, { httpOnly: true, maxAge: 86400 })
612
+ res.json({ ok: true })
613
+ })
614
+ ```
615
+
616
+ #### static(rootPath, [opts])
617
+
618
+ Serve static files from `rootPath`. Supports 60+ MIME types.
331
619
 
332
620
  | Option | Type | Default | Description |
333
- |---|---:|---|---|
334
- | `type` | string\|function | `application/octet-stream` | MIME matcher for raw parser. |
335
- | `limit` | number\|string | none | Maximum body size. |
336
- | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
621
+ |---|---|---|---|
622
+ | `index` | string\|false | `'index.html'` | Default file for directory requests, or `false` to disable. |
623
+ | `maxAge` | number | `0` | `Cache-Control` max-age in **milliseconds**. |
624
+ | `dotfiles` | string | `'ignore'` | `'allow'`, `'deny'` (403), or `'ignore'` (404). |
625
+ | `extensions` | string[] | — | Fallback extensions to try (e.g. `['html', 'htm']`). |
626
+ | `setHeaders` | function | — | Hook `(res, filePath) => void` for custom headers. |
627
+
628
+ > **Security:** Path traversal (`../`), null bytes, and directory escape attempts are blocked.
337
629
 
338
- #### multipart(opts)
630
+ ```js
631
+ app.use('/public', serveStatic('./public', {
632
+ maxAge: 86400000, // 1 day
633
+ extensions: ['html'],
634
+ dotfiles: 'deny'
635
+ }))
636
+ ```
339
637
 
340
- Streaming multipart parser that writes file parts to disk and collects fields.
638
+ #### validate(schema, [opts])
639
+
640
+ Request validation middleware. Validates `req.body`, `req.query`, and `req.params` against a typed schema. Returns `422` with detailed errors on failure.
641
+
642
+ **Schema targets:** `body`, `query`, `params` — each is an object mapping field names to rules.
643
+
644
+ | Rule | Type | Description |
645
+ |---|---|---|
646
+ | `type` | string | `'string'`, `'integer'`, `'number'`, `'float'`, `'boolean'`, `'array'`, `'date'`, `'email'`, `'url'`, `'uuid'`, `'json'`. Auto-coerces from strings. |
647
+ | `required` | boolean | Fail if the field is missing or empty. |
648
+ | `default` | any | Default value when absent (can be a function). |
649
+ | `min` / `max` | number | Numeric range constraints. |
650
+ | `minLength` / `maxLength` | number | String length constraints. |
651
+ | `minItems` / `maxItems` | number | Array length constraints. |
652
+ | `match` | RegExp | Pattern the value must match. |
653
+ | `enum` | array | Whitelist of allowed values. |
654
+ | `validate` | function | Custom validator `(value) => errorString \| undefined`. |
341
655
 
342
656
  | Option | Type | Default | Description |
343
- |---|---:|---|---|
344
- | `dir` | string | `os.tmpdir()/zero-http-uploads` | Upload directory. |
345
- | `maxFileSize` | number | none | Maximum file size in bytes. Returns 413 on exceed. |
346
- | `requireSecure` | boolean | `false` | Reject non-HTTPS requests with 403. |
657
+ |---|---|---|---|
658
+ | `stripUnknown` | boolean | `true` | Remove fields not defined in the schema. |
659
+
660
+ ```js
661
+ const { createApp, json, validate } = require('zero-http')
662
+ const app = createApp()
663
+ app.use(json())
664
+
665
+ app.post('/users', validate({
666
+ body: {
667
+ name: { type: 'string', required: true, minLength: 1, maxLength: 100 },
668
+ email: { type: 'email', required: true },
669
+ age: { type: 'integer', min: 0, max: 150 },
670
+ role: { type: 'string', enum: ['user', 'admin'], default: 'user' },
671
+ },
672
+ query: {
673
+ format: { type: 'string', enum: ['json', 'xml'], default: 'json' },
674
+ },
675
+ }), (req, res) => {
676
+ // req.body and req.query are validated and sanitised
677
+ res.json(req.body)
678
+ })
679
+ ```
347
680
 
348
- ### static(rootPath, opts)
681
+ #### csrf([opts])
349
682
 
350
- Serve static files from `rootPath`.
683
+ CSRF protection using the double-submit cookie + header/body token pattern. Safe methods (`GET`, `HEAD`, `OPTIONS`) are skipped automatically and receive a token cookie. State-changing requests must include the token.
684
+
685
+ The middleware checks for a matching token in:
686
+ 1. `req.headers['x-csrf-token']`
687
+ 2. `req.body._csrf` (if body is parsed)
688
+ 3. `req.query._csrf`
351
689
 
352
690
  | Option | Type | Default | Description |
353
- |---|---:|---|---|
354
- | `index` | string\|false | `'index.html'` | File to serve for directory requests. |
355
- | `maxAge` | number\|string | `0` | Cache-Control `max-age` (ms or unit string). |
356
- | `dotfiles` | string | `'ignore'` | `'allow'|'deny'|'ignore'`. |
357
- | `extensions` | string[] | | Fallback extensions to try. |
358
- | `setHeaders` | function | — | Hook `(res, filePath) => {}` for custom headers. |
691
+ |---|---|---|---|
692
+ | `cookie` | string | `'_csrf'` | Name of the double-submit cookie. |
693
+ | `header` | string | `'x-csrf-token'` | Request header that carries the token. |
694
+ | `saltLength` | number | `18` | Bytes of randomness for token generation. |
695
+ | `secret` | string | (auto) | HMAC secret. Auto-generated per process if omitted. |
696
+ | `ignoreMethods` | string[] | `['GET','HEAD','OPTIONS']` | HTTP methods to skip. |
697
+ | `ignorePaths` | string[] | `[]` | Path prefixes to skip (e.g. `['/api/webhooks']`). |
698
+ | `onError` | function | — | Custom error handler `(req, res) => {}`. |
699
+
700
+ Tokens are rotated automatically on every state-changing request. Access the current token via `req.csrfToken`.
701
+
702
+ ```js
703
+ const { createApp, csrf, cookieParser, json } = require('zero-http')
704
+ const app = createApp()
705
+
706
+ app.use(cookieParser())
707
+ app.use(json())
708
+ app.use(csrf())
709
+
710
+ // Read the token for forms or SPAs
711
+ app.get('/form', (req, res) => {
712
+ res.json({ csrfToken: req.csrfToken })
713
+ })
714
+
715
+ // State-changing requests are protected automatically
716
+ app.post('/transfer', (req, res) => {
717
+ res.json({ ok: true })
718
+ })
719
+ ```
359
720
 
360
- ### cors([opts])
721
+ > **Note:** `csrf()` requires `cookieParser()` to be applied first so it can read the cookie token.
722
+
723
+ #### errorHandler([opts])
724
+
725
+ Configurable error-handling middleware that formats error responses based on environment (dev vs production), integrates with `HttpError` classes, and supports custom formatters.
361
726
 
362
727
  | Option | Type | Default | Description |
363
- |---|---:|---|---|
364
- | `origin` | string\|boolean\|array | `'*'` | Allowed origin(s). `.suffix` for subdomain matching. |
365
- | `methods` | string | `'GET,POST,PUT,DELETE,OPTIONS'` | Allowed methods. |
366
- | `credentials` | boolean | `false` | Set `Access-Control-Allow-Credentials`. |
367
- | `allowedHeaders` | string | — | Headers allowed in requests. |
728
+ |---|---|---|---|
729
+ | `stack` | boolean | `NODE_ENV !== 'production'` | Include stack traces in responses. |
730
+ | `log` | boolean | `true` | Log errors to console. |
731
+ | `logger` | function | `console.error` | Custom log function. |
732
+ | `formatter` | function | — | Custom response formatter: `(err, req, isDev) => object`. |
733
+ | `onError` | function | — | Callback on every error: `(err, req, res) => void`. |
368
734
 
369
- ### fetch(url, opts)
735
+ ```js
736
+ const { createApp, errorHandler, NotFoundError } = require('zero-http')
737
+ const app = createApp()
370
738
 
371
- Node HTTP/HTTPS client. Returns `{ status, statusText, ok, secure, url, headers, text(), json(), arrayBuffer() }`.
739
+ app.use(errorHandler())
740
+
741
+ app.get('/users/:id', (req, res) => {
742
+ throw new NotFoundError('User not found')
743
+ // → 404 { error: 'User not found', code: 'NOT_FOUND', statusCode: 404 }
744
+ })
745
+
746
+ // Custom formatter
747
+ app.use(errorHandler({
748
+ formatter: (err, req, isDev) => ({
749
+ message: err.message,
750
+ ...(isDev && { stack: err.stack }),
751
+ }),
752
+ }))
753
+ ```
754
+
755
+ ---
756
+
757
+ ### fetch(url, [opts])
758
+
759
+ Zero-dependency server-side HTTP/HTTPS client.
372
760
 
373
761
  | Option | Type | Default | Description |
374
- |---|---:|---|---|
375
- | `method` | string | `GET` | HTTP method. |
762
+ |---|---|---|---|
763
+ | `method` | string | `'GET'` | HTTP method. |
376
764
  | `headers` | object | — | Request headers. |
377
- | `body` | Buffer\|string\|Stream\|URLSearchParams\|object | — | Request body (objects auto-JSON-encoded). |
378
- | `timeout` | number | — | Request timeout in ms. |
765
+ | `body` | string\|Buffer\|object\|URLSearchParams\|ReadableStream | — | Request body. Objects are auto-JSON-encoded with appropriate `Content-Type`. |
766
+ | `timeout` | number | — | Request timeout in milliseconds. |
379
767
  | `signal` | AbortSignal | — | Cancel the request. |
380
- | `agent` | object | — | Custom agent for pooling/proxies. |
381
- | `onDownloadProgress` / `onUploadProgress` | function | — | `{ loaded, total }` callbacks. |
768
+ | `agent` | object | — | Custom HTTP agent for pooling/proxies. |
769
+ | `onDownloadProgress` | function | — | `({ loaded, total }) => void` progress callback. |
770
+ | `onUploadProgress` | function | — | `({ loaded, total }) => void` progress callback. |
382
771
 
383
772
  **TLS options** (passed through for `https:` URLs): `rejectUnauthorized`, `ca`, `cert`, `key`, `pfx`, `passphrase`, `servername`, `ciphers`, `secureProtocol`, `minVersion`, `maxVersion`.
384
773
 
385
- ### rateLimit([opts])
774
+ **Response object:**
775
+
776
+ | Property/Method | Type | Description |
777
+ |---|---|---|
778
+ | `status` | number | HTTP status code. |
779
+ | `statusText` | string | Reason phrase. |
780
+ | `ok` | boolean | `true` when `status` is 200–299. |
781
+ | `secure` | boolean | `true` if HTTPS. |
782
+ | `url` | string | Final request URL. |
783
+ | `headers` | object | Response headers with `.get(name)` method and `.raw` property. |
784
+ | `text()` | Promise\<string\> | Read the body as a string. |
785
+ | `json()` | Promise\<object\> | Read and parse the body as JSON. |
786
+ | `arrayBuffer()` | Promise\<Buffer\> | Read the body as a Buffer. |
787
+
788
+ ```js
789
+ const { fetch } = require('zero-http')
790
+
791
+ // Simple GET
792
+ const res = await fetch('https://api.example.com/data')
793
+ const data = await res.json()
794
+
795
+ // POST with JSON
796
+ const res = await fetch('https://api.example.com/users', {
797
+ method: 'POST',
798
+ body: { name: 'Alice' }, // auto-serialized
799
+ headers: { Authorization: 'Bearer token' }
800
+ })
801
+
802
+ // Download with progress
803
+ await fetch('https://example.com/bigfile.zip', {
804
+ onDownloadProgress: ({ loaded, total }) => {
805
+ console.log(`${Math.round(loaded / total * 100)}%`)
806
+ }
807
+ })
808
+
809
+ // mTLS client certificate
810
+ await fetch('https://internal.api/data', {
811
+ cert: fs.readFileSync('client.crt'),
812
+ key: fs.readFileSync('client.key'),
813
+ ca: fs.readFileSync('ca.crt')
814
+ })
815
+ ```
816
+
817
+ ---
818
+
819
+ ### WebSocket — `app.ws(path, [opts], handler)`
820
+
821
+ Register a WebSocket upgrade handler. The handler receives `(ws, req)` where `ws` is a `WebSocketConnection`.
822
+
823
+ #### Options
386
824
 
387
825
  | Option | Type | Default | Description |
388
- |---|---:|---|---|
389
- | `windowMs` | number | `60000` | Time window in ms. |
390
- | `max` | number | `100` | Max requests per window per key. |
391
- | `message` | string | `'Too many requests…'` | Error message. |
392
- | `statusCode` | number | `429` | HTTP status for rate-limited responses. |
393
- | `keyGenerator` | function | `(req) => req.ip` | Custom key extraction. |
826
+ |---|---|---|---|
827
+ | `maxPayload` | number | `1048576` | Maximum incoming frame size in bytes (1 MB). |
828
+ | `pingInterval` | number | `30000` | Auto-ping interval in ms. Set to `0` to disable. |
829
+ | `verifyClient` | function | — | `(req) => boolean` return `false` to reject the upgrade with `403`. |
394
830
 
395
- ### logger([opts])
831
+ #### WebSocketConnection — Properties
832
+
833
+ | Property | Type | Description |
834
+ |---|---|---|
835
+ | `id` | string | Unique connection ID (e.g. `ws_1_l8x3k`). |
836
+ | `readyState` | number | `0`=CONNECTING, `1`=OPEN, `2`=CLOSING, `3`=CLOSED. |
837
+ | `protocol` | string | Negotiated sub-protocol (from `Sec-WebSocket-Protocol`). |
838
+ | `extensions` | string | Requested WebSocket extensions header. |
839
+ | `headers` | object | Upgrade request headers. |
840
+ | `ip` | string\|null | Remote IP address. |
841
+ | `query` | object | Parsed query params from the upgrade URL. |
842
+ | `url` | string | Full upgrade URL path. |
843
+ | `secure` | boolean | `true` for WSS connections. |
844
+ | `maxPayload` | number | Max incoming payload bytes. |
845
+ | `connectedAt` | number | Timestamp (ms) of connection. |
846
+ | `uptime` | number | Milliseconds since connection (getter). |
847
+ | `bufferedAmount` | number | Bytes waiting to be flushed (getter). |
848
+ | `data` | object | Arbitrary per-connection data store. |
849
+
850
+ #### WebSocketConnection — Methods
851
+
852
+ | Method | Signature | Description |
853
+ |---|---|---|
854
+ | `send` | `send(data, [opts])` | Send text or binary message. `opts.binary` forces binary frame. Returns `false` on backpressure. |
855
+ | `sendJSON` | `sendJSON(obj)` | Send JSON-serialized text message. |
856
+ | `ping` | `ping([payload], [cb])` | Send a ping frame. |
857
+ | `pong` | `pong([payload], [cb])` | Send a pong frame. |
858
+ | `close` | `close([code], [reason])` | Graceful close with optional status code (`1000`–`4999`). |
859
+ | `terminate` | `terminate()` | Forcefully destroy the socket (no close frame). |
860
+ | `on` | `on(event, fn)` | Listen for events: `'message'`, `'close'`, `'error'`, `'ping'`, `'pong'`, `'drain'`. |
861
+ | `once` | `once(event, fn)` | One-time event listener. |
862
+ | `off` | `off(event, fn)` | Remove a specific listener. |
863
+ | `removeAllListeners` | `removeAllListeners([event])` | Remove all listeners (optionally for one event). |
864
+ | `listenerCount` | `listenerCount(event)` | Count registered listeners for an event. |
865
+
866
+ #### WebSocketPool — Connection & Room Manager
867
+
868
+ Manage groups of WebSocket connections with room-based broadcasting.
869
+
870
+ | Method | Signature | Description |
871
+ |---|---|---|
872
+ | `add` | `add(ws)` | Track a connection (auto-removes on close). |
873
+ | `remove` | `remove(ws)` | Remove from pool and all rooms. |
874
+ | `join` | `join(ws, room)` | Add a connection to a named room. |
875
+ | `leave` | `leave(ws, room)` | Remove a connection from a room. |
876
+ | `broadcast` | `broadcast(data, [exclude])` | Send to ALL connections (optionally excluding one). |
877
+ | `broadcastJSON` | `broadcastJSON(obj, [exclude])` | Broadcast JSON to all. |
878
+ | `toRoom` | `toRoom(room, data, [exclude])` | Send to all connections in a room. |
879
+ | `toRoomJSON` | `toRoomJSON(room, obj, [exclude])` | Send JSON to a room. |
880
+ | `in` | `in(room)` | Get all connections in a room. |
881
+ | `roomsOf` | `roomsOf(ws)` | Get all rooms a connection belongs to. |
882
+ | `closeAll` | `closeAll([code], [reason])` | Close every connection gracefully. |
883
+ | `size` | getter | Total active connection count. |
884
+ | `clients` | getter | Array of all connections. |
885
+ | `rooms` | getter | Array of active room names. |
886
+ | `roomSize` | `roomSize(room)` | Connection count in a room. |
887
+
888
+ ```js
889
+ const { createApp, WebSocketPool } = require('zero-http')
890
+ const app = createApp()
891
+ const pool = new WebSocketPool()
892
+
893
+ app.ws('/chat', (ws, req) => {
894
+ const room = req.query.room || 'general'
895
+ pool.add(ws)
896
+ pool.join(ws, room)
897
+
898
+ ws.data.username = req.query.name || 'Anonymous'
899
+
900
+ pool.toRoomJSON(room, {
901
+ type: 'join',
902
+ user: ws.data.username,
903
+ count: pool.roomSize(room)
904
+ }, ws)
905
+
906
+ ws.on('message', (msg) => {
907
+ pool.toRoom(room, `${ws.data.username}: ${msg}`, ws)
908
+ })
909
+
910
+ ws.on('close', () => {
911
+ pool.toRoomJSON(room, { type: 'leave', user: ws.data.username })
912
+ })
913
+ })
914
+
915
+ app.listen(3000)
916
+ ```
917
+
918
+ ---
919
+
920
+ ### Server-Sent Events — `res.sse([opts])`
921
+
922
+ Open an SSE stream. Returns a chainable `SSEStream` controller.
923
+
924
+ #### Options
396
925
 
397
926
  | Option | Type | Default | Description |
398
- |---|---:|---|---|
399
- | `format` | string | `'dev'` | `'dev'` (colorized), `'short'`, or `'tiny'`. |
400
- | `logger` | function | `console.log` | Custom log function. |
401
- | `colors` | boolean | auto (TTY) | Enable/disable ANSI colors. |
927
+ |---|---|---|---|
928
+ | `status` | number | `200` | HTTP status code. |
929
+ | `retry` | number | | Reconnection interval hint (ms) sent to client. |
930
+ | `keepAlive` | number | `0` | Auto keep-alive comment interval (ms). |
931
+ | `keepAliveComment` | string | `'ping'` | Comment text for keep-alive pings. |
932
+ | `autoId` | boolean | `false` | Auto-increment event IDs. |
933
+ | `startId` | number | `1` | Starting value for auto-IDs. |
934
+ | `pad` | number | `0` | Bytes of initial padding (helps flush proxy buffers). |
935
+ | `headers` | object | — | Additional response headers. |
936
+
937
+ #### SSEStream — Methods (all chainable)
938
+
939
+ | Method | Signature | Description |
940
+ |---|---|---|
941
+ | `send` | `send(data, [id])` | Send unnamed data event. Objects are auto-JSON-serialized. |
942
+ | `sendJSON` | `sendJSON(obj, [id])` | Send object as JSON data. |
943
+ | `event` | `event(name, data, [id])` | Send a named event. |
944
+ | `comment` | `comment(text)` | Send a comment line (invisible to `EventSource.onmessage`). |
945
+ | `retry` | `retry(ms)` | Update the reconnection interval hint. |
946
+ | `keepAlive` | `keepAlive(ms, [comment])` | Start/restart the keep-alive timer. |
947
+ | `flush` | `flush()` | Flush response buffer through proxies. |
948
+ | `close` | `close()` | Close the stream from the server side. |
949
+
950
+ #### SSEStream — Properties
951
+
952
+ | Property | Type | Description |
953
+ |---|---|---|
954
+ | `connected` | boolean | Whether the stream is still open. |
955
+ | `eventCount` | number | Total events sent. |
956
+ | `bytesSent` | number | Total bytes written. |
957
+ | `connectedAt` | number | Connection timestamp (ms). |
958
+ | `uptime` | number | Milliseconds since connected. |
959
+ | `lastEventId` | string\|null | `Last-Event-ID` from the client reconnection header. |
960
+ | `secure` | boolean | `true` if HTTPS. |
961
+ | `data` | object | Arbitrary per-stream data store. |
962
+
963
+ #### SSEStream — Events
964
+
965
+ | Event | Description |
966
+ |---|---|
967
+ | `'close'` | Client disconnected or server called `close()`. |
968
+ | `'error'` | Write error on the underlying response. |
969
+
970
+ ```js
971
+ app.get('/events', (req, res) => {
972
+ const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 })
973
+
974
+ sse.send('connected')
975
+
976
+ const interval = setInterval(() => {
977
+ sse.event('tick', { time: Date.now() })
978
+ }, 1000)
979
+
980
+ sse.on('close', () => {
981
+ clearInterval(interval)
982
+ console.log(`Client was connected for ${sse.uptime}ms, sent ${sse.eventCount} events`)
983
+ })
984
+ })
985
+ ```
986
+
987
+ > **Tip:** Use `pad` when deploying behind reverse proxies (like Nginx) that buffer small responses. A 2KB pad typically forces the first flush.
988
+
989
+ ---
402
990
 
403
991
  ### HTTPS
404
992
 
@@ -408,105 +996,484 @@ const { createApp } = require('zero-http')
408
996
  const app = createApp()
409
997
 
410
998
  app.listen(443, {
411
- key: fs.readFileSync('key.pem'),
412
- cert: fs.readFileSync('cert.pem')
999
+ key: fs.readFileSync('key.pem'),
1000
+ cert: fs.readFileSync('cert.pem')
413
1001
  }, () => console.log('HTTPS on 443'))
414
1002
  ```
415
1003
 
416
- All modules respect HTTPS: `req.secure`, `req.protocol`, `ws.secure`, `sse.secure`, and `fetch()` response `secure` property.
1004
+ HTTPS awareness is built into every module:
417
1005
 
418
- ## Examples
1006
+ | API | Property | Value |
1007
+ |---|---|---|
1008
+ | Request | `req.secure` / `req.protocol` | `true` / `'https'` |
1009
+ | WebSocket | `ws.secure` | `true` for WSS |
1010
+ | SSE | `sse.secure` | `true` over TLS |
1011
+ | Fetch response | `res.secure` | `true` if `https:` |
1012
+ | Body parsers | `requireSecure` option | Reject non-TLS with 403 |
1013
+ | Routes | `{ secure: true }` option | Match HTTPS-only |
419
1014
 
420
- WebSocket chat:
1015
+ ---
421
1016
 
422
- ```js
423
- const { createApp } = require('zero-http')
424
- const app = createApp()
1017
+ ### Environment Config — `env`
425
1018
 
426
- app.ws('/chat', (ws, req) => {
427
- ws.send('Welcome to the chat!')
428
- ws.on('message', msg => {
429
- ws.send(`You said: ${msg}`)
430
- })
1019
+ Zero-dependency typed environment variable system. Loads `.env` files, validates against a typed schema, and exposes values via property access or function call.
1020
+
1021
+ #### File Loading Order
1022
+
1023
+ Files are loaded in precedence order (later overrides earlier). `process.env` always takes final precedence.
1024
+
1025
+ 1. `.env` — shared defaults
1026
+ 2. `.env.local` — local overrides (gitignored)
1027
+ 3. `.env.{NODE_ENV}` — environment-specific (e.g. `.env.production`)
1028
+ 4. `.env.{NODE_ENV}.local` — env-specific local overrides
1029
+
1030
+ #### Schema Types
1031
+
1032
+ `string`, `number`, `integer`, `boolean`, `port`, `array`, `json`, `url`, `enum`
1033
+
1034
+ #### env.load(schema, [opts])
1035
+
1036
+ | Option | Type | Default | Description |
1037
+ |---|---|---|---|
1038
+ | `path` | string | `process.cwd()` | Directory to load `.env` files from. |
1039
+ | `override` | boolean | `false` | Write file values into `process.env`. |
1040
+
1041
+ Schema fields support: `type`, `required`, `default`, `min`, `max`, `match`, `separator` (for arrays), `values` (for enums).
1042
+
1043
+ ```js
1044
+ const { env } = require('zero-http')
1045
+
1046
+ env.load({
1047
+ PORT: { type: 'port', default: 3000 },
1048
+ DATABASE_URL: { type: 'string', required: true },
1049
+ DEBUG: { type: 'boolean', default: false },
1050
+ ALLOWED_ORIGINS: { type: 'array', separator: ',' },
1051
+ LOG_LEVEL: { type: 'enum', values: ['debug','info','warn','error'], default: 'info' },
431
1052
  })
432
1053
 
433
- app.listen(3000)
1054
+ env.PORT // => 3000 (number, not string)
1055
+ env('PORT') // => 3000 (callable)
1056
+ env.get('PORT') // => 3000
1057
+ env.DEBUG // => false (boolean)
1058
+ env.require('DATABASE_URL') // throws if missing
1059
+ env.has('PORT') // => true
1060
+ env.all() // => { PORT: 3000, DATABASE_URL: '...', ... }
434
1061
  ```
435
1062
 
436
- Server-Sent Events:
1063
+ Throws on startup if required variables are missing or values fail type validation — **fail fast, not at runtime**.
1064
+
1065
+ ---
1066
+
1067
+ ### Database / ORM
1068
+
1069
+ Built-in ORM with support for memory, JSON file, SQLite, MySQL, PostgreSQL, and MongoDB. Memory and JSON adapters work out of the box; other adapters use "bring your own driver" (`better-sqlite3`, `mysql2`, `pg`, `mongodb`).
1070
+
1071
+ #### Database.connect(adapter, [opts])
1072
+
1073
+ | Adapter | Driver Required | Options |
1074
+ |---|---|---|
1075
+ | `'memory'` | none | — |
1076
+ | `'json'` | none | `{ path: './data.json' }` |
1077
+ | `'sqlite'` | `better-sqlite3` | `{ filename: './db.sqlite' }` |
1078
+ | `'mysql'` | `mysql2` | `{ host, port, user, password, database }` |
1079
+ | `'postgres'` | `pg` | `{ host, port, user, password, database }` |
1080
+ | `'mongo'` | `mongodb` | `{ url, database }` |
437
1081
 
438
1082
  ```js
439
- app.get('/events', (req, res) => {
440
- const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 })
1083
+ const { Database, Model, TYPES } = require('zero-http')
1084
+
1085
+ const db = Database.connect('memory')
1086
+ // or
1087
+ const db = Database.connect('sqlite', { filename: './app.db' })
1088
+ ```
1089
+
1090
+ #### Model
1091
+
1092
+ Define database entities by extending `Model`. Register with a database and call `sync()` to create tables.
441
1093
 
442
- const interval = setInterval(() => {
443
- sse.event('tick', { time: Date.now() })
444
- }, 1000)
1094
+ | Static Property | Type | Default | Description |
1095
+ |---|---|---|---|
1096
+ | `table` | string | (required) | Table/collection name. |
1097
+ | `schema` | object | `{}` | Column definitions. |
1098
+ | `timestamps` | boolean | `false` | Auto-manage `createdAt`/`updatedAt`. |
1099
+ | `softDelete` | boolean | `false` | Use `deletedAt` instead of real deletion. |
1100
+ | `hidden` | string[] | `[]` | Fields excluded from `toJSON()` (e.g. passwords). |
1101
+ | `scopes` | object | `{}` | Named reusable query conditions. |
445
1102
 
446
- sse.on('close', () => clearInterval(interval))
1103
+ #### TYPES
1104
+
1105
+ Column type constants for schemas:
1106
+
1107
+ `STRING`, `INTEGER`, `FLOAT`, `BOOLEAN`, `DATE`, `DATETIME`, `JSON`, `TEXT`, `BLOB`, `UUID`
1108
+
1109
+ #### Schema Constraints
1110
+
1111
+ | Constraint | Description |
1112
+ |---|---|
1113
+ | `primaryKey` | Mark as primary key. |
1114
+ | `autoIncrement` | Auto-increment (integer PKs). |
1115
+ | `required` | Value must be provided. |
1116
+ | `unique` | Enforce uniqueness. |
1117
+ | `default` | Default value (or function). |
1118
+ | `minLength` / `maxLength` | String length. |
1119
+ | `min` / `max` | Numeric range. |
1120
+ | `enum` | Allowed values whitelist. |
1121
+ | `match` | RegExp pattern. |
1122
+ | `nullable` | Allow null values. |
1123
+
1124
+ ```js
1125
+ class User extends Model {
1126
+ static table = 'users'
1127
+ static schema = {
1128
+ id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
1129
+ name: { type: TYPES.STRING, required: true, maxLength: 100 },
1130
+ email: { type: TYPES.STRING, required: true, unique: true },
1131
+ role: { type: TYPES.STRING, enum: ['user', 'admin'], default: 'user' },
1132
+ password: { type: TYPES.STRING, required: true },
1133
+ }
1134
+ static timestamps = true
1135
+ static softDelete = true
1136
+ static hidden = ['password']
1137
+ static scopes = {
1138
+ active: q => q.where('active', true),
1139
+ admins: q => q.where('role', 'admin'),
1140
+ olderThan: (q, age) => q.where('age', '>', age),
1141
+ }
1142
+ }
1143
+
1144
+ db.register(User)
1145
+ await db.sync()
1146
+ ```
1147
+
1148
+ #### CRUD Operations
1149
+
1150
+ ```js
1151
+ // Create
1152
+ const user = await User.create({ name: 'Alice', email: 'a@b.com', password: 'hashed' })
1153
+
1154
+ // Find
1155
+ const users = await User.findAll()
1156
+ const admins = await User.find({ role: 'admin' })
1157
+ const alice = await User.findById(1)
1158
+ const one = await User.findOne({ email: 'a@b.com' })
1159
+
1160
+ // Update
1161
+ await alice.update({ name: 'Alice W.' })
1162
+ await User.updateById(1, { role: 'admin' })
1163
+
1164
+ // Delete
1165
+ await alice.delete()
1166
+ await User.deleteById(2)
1167
+
1168
+ // Count
1169
+ const count = await User.count({ role: 'user' })
1170
+
1171
+ // Scopes
1172
+ const activeAdmins = await User.scope('active').scope('admins').exec()
1173
+ ```
1174
+
1175
+ #### Fluent Query Builder
1176
+
1177
+ ```js
1178
+ const results = await User.query()
1179
+ .where('age', '>', 18)
1180
+ .where('role', 'admin')
1181
+ .orderBy('name', 'asc')
1182
+ .limit(10)
1183
+ .offset(20)
1184
+ .select('name', 'email')
1185
+
1186
+ // Aggregations, grouping, joins also supported
1187
+ ```
1188
+
1189
+ ---
1190
+
1191
+ ### Error Classes — `HttpError`
1192
+
1193
+ Structured HTTP error classes with status codes, machine-readable codes, and optional details. Every error extends `HttpError` and serializes cleanly to JSON.
1194
+
1195
+ ```js
1196
+ const { NotFoundError, ValidationError, createError, isHttpError } = require('zero-http')
1197
+
1198
+ // Throw named errors
1199
+ throw new NotFoundError('User not found')
1200
+ // → { error: 'User not found', code: 'NOT_FOUND', statusCode: 404 }
1201
+
1202
+ // With extra details
1203
+ throw new ValidationError('Invalid input', {
1204
+ email: 'required',
1205
+ age: 'must be >= 18',
447
1206
  })
1207
+ // → { error: 'Invalid input', code: 'VALIDATION_FAILED', statusCode: 422, details: { email: '...', age: '...' } }
1208
+
1209
+ // Factory
1210
+ throw createError(503, 'Database unavailable')
1211
+
1212
+ // Type check
1213
+ if (isHttpError(err)) console.log(err.statusCode)
448
1214
  ```
449
1215
 
450
- Full-featured server:
1216
+ **Available error classes:** `HttpError`, `BadRequestError` (400), `UnauthorizedError` (401), `ForbiddenError` (403), `NotFoundError` (404), `MethodNotAllowedError` (405), `ConflictError` (409), `GoneError` (410), `PayloadTooLargeError` (413), `UnprocessableEntityError` (422), `ValidationError` (422), `TooManyRequestsError` (429), `InternalError` (500), `NotImplementedError` (501), `BadGatewayError` (502), `ServiceUnavailableError` (503).
1217
+
1218
+ ---
1219
+
1220
+ ### Debug Logger — `debug(namespace)`
1221
+
1222
+ Lightweight namespaced logger with levels, colors, timestamps, and pattern-based filtering via the `DEBUG` environment variable.
1223
+
1224
+ **Levels:** `trace` (0) → `debug` (1) → `info` (2) → `warn` (3) → `error` (4) → `fatal` (5) → `silent` (6)
1225
+
1226
+ ```js
1227
+ const { debug } = require('zero-http')
1228
+ const log = debug('app:routes')
1229
+
1230
+ log('shorthand debug message')
1231
+ log.info('server started on port %d', 3000)
1232
+ log.warn('deprecated route used')
1233
+ log.error('failed to connect', err)
1234
+ ```
1235
+
1236
+ **Environment variables:**
1237
+
1238
+ | Variable | Example | Description |
1239
+ |---|---|---|
1240
+ | `DEBUG` | `app:*,router` | Enable specific namespaces (supports glob patterns). Prefix with `-` to exclude. |
1241
+ | `DEBUG_LEVEL` | `warn` | Minimum log level. |
1242
+
1243
+ ```bash
1244
+ # Enable all 'app:' namespaces
1245
+ DEBUG=app:* node server.js
1246
+
1247
+ # Enable everything except noisy modules
1248
+ DEBUG=*,-verbose:* node server.js
1249
+
1250
+ # Show only warnings and above
1251
+ DEBUG_LEVEL=warn node server.js
1252
+ ```
1253
+
1254
+ ---
1255
+
1256
+ ## Examples
1257
+
1258
+ ### Full-Featured Server
451
1259
 
452
1260
  ```js
453
1261
  const path = require('path')
454
- const { createApp, cors, json, urlencoded, text, compress,
455
- static: serveStatic, logger, rateLimit, Router } = require('zero-http')
1262
+ const {
1263
+ createApp, Router, cors, json, urlencoded, text, compress,
1264
+ static: serveStatic, logger, rateLimit, helmet, timeout,
1265
+ requestId, cookieParser, csrf, validate, errorHandler,
1266
+ env, WebSocketPool
1267
+ } = require('zero-http')
1268
+
1269
+ // Load environment config
1270
+ env.load({
1271
+ PORT: { type: 'port', default: 3000 },
1272
+ SECRET: { type: 'string', required: true },
1273
+ })
456
1274
 
457
1275
  const app = createApp()
458
1276
 
1277
+ // Security & logging
1278
+ app.use(helmet())
459
1279
  app.use(logger({ format: 'dev' }))
460
- app.use(cors())
461
- app.use(compress())
462
- app.use(rateLimit({ windowMs: 60000, max: 200 }))
1280
+ app.use(requestId())
1281
+ app.use(timeout(10000))
1282
+
1283
+ // CORS & compression
1284
+ app.use(cors({ origin: 'https://myapp.com', credentials: true }))
1285
+ app.use(compress({ threshold: 512 }))
1286
+
1287
+ // Rate limiting
1288
+ app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 200 }))
1289
+
1290
+ // Body parsers & security
463
1291
  app.use(json({ limit: '1mb' }))
464
1292
  app.use(urlencoded({ extended: true }))
465
1293
  app.use(text())
466
- app.use(serveStatic(path.join(__dirname, 'public')))
1294
+ app.use(cookieParser(env.SECRET))
1295
+ app.use(csrf())
1296
+
1297
+ // Error handler
1298
+ app.use(errorHandler())
1299
+
1300
+ // Static files
1301
+ app.use(serveStatic(path.join(__dirname, 'public'), { maxAge: 86400000 }))
467
1302
 
1303
+ // API routes with validation
468
1304
  const api = Router()
469
- api.get('/health', (req, res) => res.json({ status: 'ok' }))
1305
+ api.get('/health', (req, res) => res.json({ status: 'ok', requestId: req.id }))
470
1306
  api.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
1307
+ api.post('/users', validate({
1308
+ body: {
1309
+ name: { type: 'string', required: true, minLength: 1 },
1310
+ email: { type: 'email', required: true },
1311
+ }
1312
+ }), (req, res) => res.status(201).json(req.body))
471
1313
  app.use('/api', api)
472
1314
 
473
- app.ws('/chat', (ws) => {
474
- ws.on('message', msg => ws.send('echo: ' + msg))
1315
+ // WebSocket with rooms
1316
+ const pool = new WebSocketPool()
1317
+ app.ws('/chat', (ws, req) => {
1318
+ pool.add(ws)
1319
+ pool.join(ws, 'lobby')
1320
+ ws.on('message', msg => pool.toRoom('lobby', msg, ws))
475
1321
  })
476
1322
 
1323
+ // Server-Sent Events
477
1324
  app.get('/events', (req, res) => {
478
- const sse = res.sse({ retry: 3000, autoId: true })
479
- sse.send('connected')
480
- sse.on('close', () => console.log('bye'))
1325
+ const sse = res.sse({ retry: 3000, autoId: true, keepAlive: 30000 })
1326
+ sse.send('connected')
1327
+ sse.on('close', () => console.log('client left'))
481
1328
  })
482
1329
 
1330
+ // Global error handler
483
1331
  app.onError((err, req, res) => {
484
- res.status(500).json({ error: err.message })
1332
+ console.error(err)
1333
+ res.status(500).json({ error: err.message })
1334
+ })
1335
+
1336
+ app.listen(env.PORT, () => console.log(`Server running on :${env.PORT}`))
1337
+ ```
1338
+
1339
+ ### WebSocket Chat with Rooms
1340
+
1341
+ ```js
1342
+ const { createApp, WebSocketPool } = require('zero-http')
1343
+ const app = createApp()
1344
+ const pool = new WebSocketPool()
1345
+
1346
+ app.ws('/chat', (ws, req) => {
1347
+ const room = req.query.room || 'general'
1348
+ ws.data.name = req.query.name || 'Anon'
1349
+
1350
+ pool.add(ws)
1351
+ pool.join(ws, room)
1352
+ pool.toRoomJSON(room, { type: 'join', user: ws.data.name }, ws)
1353
+
1354
+ ws.on('message', msg => {
1355
+ pool.toRoomJSON(room, { type: 'message', user: ws.data.name, text: msg }, ws)
1356
+ })
1357
+
1358
+ ws.on('close', () => {
1359
+ pool.toRoomJSON(room, { type: 'leave', user: ws.data.name })
1360
+ })
1361
+ })
1362
+
1363
+ app.listen(3000)
1364
+ // Connect: ws://localhost:3000/chat?room=dev&name=Alice
1365
+ ```
1366
+
1367
+ ### Real-Time Dashboard with SSE
1368
+
1369
+ ```js
1370
+ const { createApp, helmet, compress } = require('zero-http')
1371
+ const app = createApp()
1372
+ app.use(helmet())
1373
+ app.use(compress())
1374
+
1375
+ const clients = new Set()
1376
+
1377
+ app.get('/dashboard/stream', (req, res) => {
1378
+ const sse = res.sse({ autoId: true, keepAlive: 15000, pad: 2048 })
1379
+ clients.add(sse)
1380
+ sse.on('close', () => clients.delete(sse))
485
1381
  })
486
1382
 
487
- app.listen(3000, () => console.log('Server running on :3000'))
1383
+ // Broadcast metrics every second
1384
+ setInterval(() => {
1385
+ const metrics = { cpu: process.cpuUsage(), mem: process.memoryUsage().heapUsed }
1386
+ for (const sse of clients) sse.event('metrics', metrics)
1387
+ }, 1000)
1388
+
1389
+ app.listen(3000)
488
1390
  ```
489
1391
 
490
- ## File layout
1392
+ ### File Upload API
1393
+
1394
+ ```js
1395
+ const { createApp, json, multipart } = require('zero-http')
1396
+ const app = createApp()
1397
+
1398
+ app.use(json())
1399
+
1400
+ app.post('/upload', multipart({ dir: './uploads', maxFileSize: 10 * 1024 * 1024 }), (req, res) => {
1401
+ const { files, fields } = req.body
1402
+ res.json({
1403
+ uploaded: Object.keys(files).length,
1404
+ description: fields.description || 'No description',
1405
+ details: Object.values(files).map(f => ({
1406
+ name: f.originalFilename,
1407
+ size: f.size,
1408
+ type: f.contentType
1409
+ }))
1410
+ })
1411
+ })
1412
+
1413
+ app.listen(3000)
1414
+ ```
1415
+
1416
+ ### Middleware Composition Tips
1417
+
1418
+ ```js
1419
+ // Middleware runs in registration order
1420
+ app.use(logger()) // 1. Log the request
1421
+ app.use(helmet()) // 2. Set security headers
1422
+ app.use(cors()) // 3. Handle CORS
1423
+ app.use(compress()) // 4. Compress responses
1424
+ app.use(rateLimit()) // 5. Check rate limits
1425
+ app.use(json()) // 6. Parse body
1426
+
1427
+ // Path-scoped middleware
1428
+ app.use('/api', rateLimit({ max: 50 })) // stricter for API
1429
+ app.use('/admin', (req, res, next) => { // custom auth
1430
+ if (!req.get('authorization')) return res.sendStatus(401)
1431
+ next()
1432
+ })
1433
+
1434
+ // Custom middleware pattern
1435
+ app.use((req, res, next) => {
1436
+ req.locals.startTime = Date.now()
1437
+ res.locals.requestId = req.id
1438
+ next()
1439
+ })
1440
+
1441
+ // Error handler goes last
1442
+ app.onError((err, req, res, next) => {
1443
+ const duration = Date.now() - req.locals.startTime
1444
+ console.error(`Error after ${duration}ms:`, err.message)
1445
+ res.status(err.statusCode || 500).json({ error: err.message })
1446
+ })
1447
+ ```
1448
+
1449
+ ---
1450
+
1451
+ ## File Layout
491
1452
 
492
1453
  ```
493
1454
  lib/
494
- app.js core App class (middleware, routing, listen, ws)
495
- body/ — body parsers (json, urlencoded, text, raw, multipart)
496
- fetch/server-side HTTP/HTTPS client
497
- http/ Request & Response wrappers
498
- middleware/ cors, logger, rateLimit, compress, static
499
- router/ Router with sub-app mounting & introspection
500
- sse/ SSEStream controller
501
- ws/ WebSocket connection & handshake
502
- documentation/ — demo server, controllers, and public UI
503
- test/ integration tests
1455
+ app.js — App class (middleware pipeline, routing, listen, ws upgrade)
1456
+ body/ — body parsers (json, urlencoded, text, raw, multipart)
1457
+ debug.jsnamespaced debug logger with levels and colors
1458
+ env/ typed .env loader with schema validation
1459
+ errors.js HttpError classes and factory
1460
+ fetch/ server-side HTTP/HTTPS client
1461
+ http/ Request & Response wrappers with QoL methods
1462
+ middleware/ cors, helmet, logger, rateLimit, compress, static, timeout,
1463
+ requestId, cookieParser, csrf, validate, errorHandler
1464
+ orm/ Database, Model, Query, adapters (memory, json, sqlite, mysql, postgres, mongo)
1465
+ router/ — Router with sub-app mounting, pattern matching & introspection
1466
+ sse/ — SSEStream controller
1467
+ ws/ — WebSocket connection, handshake, and room management
1468
+ documentation/ — live demo server, controllers, and playground UI
1469
+ test/ — vitest test suite (970 tests)
504
1470
  ```
505
1471
 
506
1472
  ## Testing
507
1473
 
508
1474
  ```bash
509
- node test/test.js
1475
+ npm test # vitest run (single pass)
1476
+ npm run test:watch # vitest (watch mode)
510
1477
  ```
511
1478
 
512
1479
  ## License