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.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +25 -184
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
package/README.md
CHANGED
|
@@ -6,37 +6,40 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/zero-http)
|
|
8
8
|
[](https://www.npmjs.com/package/zero-http)
|
|
9
|
-
[](https://github.com/tonywied17/zero-http
|
|
9
|
+
[](https://github.com/tonywied17/zero-http)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
12
|
[](package.json)
|
|
13
13
|
|
|
14
|
-
> **Zero-dependency
|
|
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** —
|
|
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
|
|
21
|
-
- **Built-in
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
75
|
-
| `Router()` | function | Create a standalone
|
|
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
|
|
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
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
|
|
90
|
-
|
|
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)`
|
|
95
|
-
| `get` | `get(path, [opts], ...handlers)` | Register GET route handlers.
|
|
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.
|
|
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
|
|
108
|
-
| `handler` | property | Bound
|
|
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
|
-
|
|
161
|
+
**Route options object** — pass as the first argument after the path:
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
167
|
+
// HTTP-only route
|
|
168
|
+
app.get('/public', { secure: false }, (req, res) => res.json({ public: true }))
|
|
169
|
+
```
|
|
128
170
|
|
|
129
|
-
|
|
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
|
-
|
|
173
|
+
---
|
|
143
174
|
|
|
144
|
-
|
|
175
|
+
### Request (`req`)
|
|
145
176
|
|
|
146
|
-
|
|
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
|
-
|
|
179
|
+
#### Properties
|
|
153
180
|
|
|
154
181
|
| Property | Type | Description |
|
|
155
182
|
|---|---|---|
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `headers` | object |
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
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
|
-
|
|
198
|
+
#### Getters
|
|
172
199
|
|
|
173
|
-
|
|
|
174
|
-
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
|
183
|
-
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
239
|
+
---
|
|
198
240
|
|
|
199
|
-
|
|
241
|
+
### Response (`res`)
|
|
200
242
|
|
|
201
|
-
|
|
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
|
-
|
|
245
|
+
#### Properties
|
|
213
246
|
|
|
214
|
-
|
|
|
215
|
-
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
+
#### Terminal Methods
|
|
233
267
|
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
309
|
+
---
|
|
246
310
|
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
505
|
+
#### rateLimit([opts])
|
|
299
506
|
|
|
300
|
-
|
|
507
|
+
In-memory per-IP rate limiter with sliding window.
|
|
301
508
|
|
|
302
|
-
|
|
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
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
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
|
-
|
|
630
|
+
```js
|
|
631
|
+
app.use('/public', serveStatic('./public', {
|
|
632
|
+
maxAge: 86400000, // 1 day
|
|
633
|
+
extensions: ['html'],
|
|
634
|
+
dotfiles: 'deny'
|
|
635
|
+
}))
|
|
636
|
+
```
|
|
339
637
|
|
|
340
|
-
|
|
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
|
-
| `
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
681
|
+
#### csrf([opts])
|
|
349
682
|
|
|
350
|
-
|
|
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
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
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
|
-
|
|
735
|
+
```js
|
|
736
|
+
const { createApp, errorHandler, NotFoundError } = require('zero-http')
|
|
737
|
+
const app = createApp()
|
|
370
738
|
|
|
371
|
-
|
|
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\|
|
|
378
|
-
| `timeout` | number | — | Request timeout in
|
|
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`
|
|
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
|
-
|
|
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
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
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
|
-
|
|
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
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
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
|
-
|
|
412
|
-
|
|
999
|
+
key: fs.readFileSync('key.pem'),
|
|
1000
|
+
cert: fs.readFileSync('cert.pem')
|
|
413
1001
|
}, () => console.log('HTTPS on 443'))
|
|
414
1002
|
```
|
|
415
1003
|
|
|
416
|
-
|
|
1004
|
+
HTTPS awareness is built into every module:
|
|
417
1005
|
|
|
418
|
-
|
|
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
|
-
|
|
1015
|
+
---
|
|
421
1016
|
|
|
422
|
-
|
|
423
|
-
const { createApp } = require('zero-http')
|
|
424
|
-
const app = createApp()
|
|
1017
|
+
### Environment Config — `env`
|
|
425
1018
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
455
|
-
|
|
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(
|
|
461
|
-
app.use(
|
|
462
|
-
|
|
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(
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
495
|
-
body/
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1455
|
+
app.js — App class (middleware pipeline, routing, listen, ws upgrade)
|
|
1456
|
+
body/ — body parsers (json, urlencoded, text, raw, multipart)
|
|
1457
|
+
debug.js — namespaced 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
|
-
|
|
1475
|
+
npm test # vitest run (single pass)
|
|
1476
|
+
npm run test:watch # vitest (watch mode)
|
|
510
1477
|
```
|
|
511
1478
|
|
|
512
1479
|
## License
|