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
@@ -0,0 +1,1139 @@
1
+ [
2
+ {
3
+ "section": "Getting Started",
4
+ "icon": "rocket",
5
+ "items": [
6
+ {
7
+ "name": "Installation",
8
+ "description": "Install zero-http from npm. No external dependencies are required — everything is built in.",
9
+ "example": "npm install zero-http",
10
+ "exampleLang": "bash",
11
+ "tips": [
12
+ "zero-http has zero runtime dependencies — npm install is all you need.",
13
+ "Requires Node.js 18+ (uses crypto.randomUUID, structuredClone, etc.).",
14
+ "TypeScript definitions are included in the package under types/."
15
+ ]
16
+ },
17
+ {
18
+ "name": "Quickstart",
19
+ "description": "Create a minimal server with JSON parsing and static file serving in under 10 lines.",
20
+ "example": "const { createApp, json, static: serveStatic } = require('zero-http')\nconst path = require('path')\nconst app = createApp()\n\napp.use(json({ limit: '10kb' }))\napp.use(serveStatic(path.join(__dirname, 'public'), { index: 'index.html' }))\n\napp.get('/ping', (req, res) => res.json({ pong: true }))\n\napp.listen(3000, () => {\n\tconsole.log('Server listening on http://localhost:3000')\n})",
21
+ "tips": [
22
+ "Middleware runs in registration order — add parsers before route handlers.",
23
+ "All route methods (get, post, put, etc.) return the app, so you can chain them.",
24
+ "Use app.onError() to register a global error handler for uncaught errors."
25
+ ]
26
+ },
27
+ {
28
+ "name": "Full-Featured Server",
29
+ "description": "A complete production-ready skeleton combining security headers, CSRF, cookies, timeouts, request IDs, logging, CORS, compression, body parsing, rate limiting, validation, static serving, WebSocket, SSE, Router sub-apps, app settings, and error handling.",
30
+ "example": "const path = require('path')\nconst { createApp, cors, json, urlencoded, text, compress, helmet, timeout,\n\trequestId, cookieParser, csrf, validate, rateLimit, logger,\n\tstatic: serveStatic, Router, WebSocketPool } = require('zero-http')\n\nconst app = createApp()\n\n// App settings\napp.set('env', process.env.NODE_ENV || 'development')\napp.locals.appName = 'My App'\n\n// Middleware stack (order matters!)\napp.use(requestId()) // tag every request with a unique ID\napp.use(logger()) // log method, url, status, response time\napp.use(helmet()) // security headers (CSP, HSTS, X-Frame, etc.)\napp.use(cors()) // CORS with preflight handling\napp.use(compress()) // brotli/gzip/deflate response compression\napp.use(timeout(30000)) // 30s timeout → 408\napp.use(rateLimit()) // 100 req/min per IP\napp.use(cookieParser('my-secret')) // signed cookies\napp.use(json({ limit: '1mb' })) // JSON body parser\napp.use(urlencoded({ extended: true })) // form body parser\napp.use(text()) // plain text parser\napp.use(csrf()) // CSRF protection\napp.use(serveStatic(path.join(__dirname, 'public')))\n\n// API routes\nconst api = Router()\napi.get('/health', (req, res) => res.json({ status: 'ok', id: req.id }))\napi.post('/users', validate({\n\tbody: {\n\t\tname: { type: 'string', required: true, minLength: 2 },\n\t\temail: { type: 'email', required: true }\n\t}\n}), (req, res) => res.status(201).json(req.body))\napp.use('/api', api)\n\n// WebSocket with rooms\nconst pool = new WebSocketPool()\napp.ws('/chat', (ws, req) => {\n\tpool.add(ws)\n\tpool.join(ws, 'general')\n\tws.on('message', msg => pool.toRoom('general', msg, ws))\n})\n\n// SSE\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({ retry: 3000, autoId: true })\n\tsse.send('connected')\n\tsse.on('close', () => console.log('bye'))\n})\n\napp.onError((err, req, res) => {\n\tres.status(500).json({ error: err.message, requestId: req.id })\n})\n\napp.listen(3000)",
31
+ "tips": [
32
+ "Always place parsers (json, urlencoded) before route handlers.",
33
+ "helmet() should come early in the middleware stack to set security headers on all responses.",
34
+ "cookieParser must come before csrf() since CSRF reads the cookie token."
35
+ ]
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ "section": "Core",
41
+ "icon": "box",
42
+ "items": [
43
+ {
44
+ "name": "createApp",
45
+ "description": "Creates an application instance — the central object for registering middleware, routes, and settings. Supports Express-like set/get, enable/disable, locals, param handlers, groups, chaining, and route introspection.",
46
+ "methods": [
47
+ { "method": "use", "signature": "use([path], ...handlers)", "description": "Register middleware or mount a Router at a prefix. Runs in registration order." },
48
+ { "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register a GET route handler. Also: app.get(key) returns an app setting." },
49
+ { "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register a POST route handler." },
50
+ { "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register a PUT route handler." },
51
+ { "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register a DELETE route handler." },
52
+ { "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register a PATCH route handler." },
53
+ { "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register an OPTIONS route handler." },
54
+ { "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register a HEAD route handler." },
55
+ { "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register a handler for ALL HTTP methods." },
56
+ { "method": "ws", "signature": "ws(path, [opts], handler)", "description": "Register a WebSocket upgrade handler. See Real-Time → WebSocket." },
57
+ { "method": "set", "signature": "set(key, value)", "description": "Set an application setting (also used as get(key) to retrieve)." },
58
+ { "method": "enable", "signature": "enable(key)", "description": "Set a boolean setting to true." },
59
+ { "method": "disable", "signature": "disable(key)", "description": "Set a boolean setting to false." },
60
+ { "method": "enabled", "signature": "enabled(key)", "description": "Check if a setting is truthy." },
61
+ { "method": "disabled", "signature": "disabled(key)", "description": "Check if a setting is falsy." },
62
+ { "method": "locals", "signature": "locals", "description": "A plain object for storing application-wide data. Merged into req.locals on each request." },
63
+ { "method": "param", "signature": "param(name, handler)", "description": "Register a parameter handler that fires when :name appears in a route." },
64
+ { "method": "group", "signature": "group(prefix, [...middleware], fn)", "description": "Group routes under a prefix with shared middleware." },
65
+ { "method": "chain", "signature": "chain(path)", "description": "Start a route chain for a single path. Returns { get, post, put, delete, ... }." },
66
+ { "method": "routes", "signature": "routes()", "description": "Return the full route table for introspection/debugging." },
67
+ { "method": "onError", "signature": "onError(handler)", "description": "Register a global error handler: (err, req, res, next) => {}." },
68
+ { "method": "listen", "signature": "listen(port, [tlsOpts], [cb])", "description": "Start the HTTP(S) server. Pass TLS options for HTTPS." },
69
+ { "method": "close", "signature": "close()", "description": "Shut down the server." },
70
+ { "method": "handler", "signature": "handler", "description": "The raw (req, res) handler for use with custom HTTP servers." }
71
+ ],
72
+ "example": "const { createApp, json } = require('zero-http')\nconst app = createApp()\n\n// App settings\napp.set('env', 'production')\napp.enable('trust proxy')\nconsole.log(app.get('env')) // 'production'\nconsole.log(app.enabled('trust proxy')) // true\n\n// Shared locals\napp.locals.appName = 'My API'\n\n// Route chaining\napp.get('/', (req, res) => res.json({ app: req.locals.appName }))\n .get('/health', (req, res) => res.sendStatus(200))\n\n// Route groups with shared middleware\napp.group('/api/v1', json(), (router) => {\n\trouter.get('/users', (req, res) => res.json([]))\n\trouter.post('/users', (req, res) => res.status(201).json(req.body))\n})\n\n// Param handlers\napp.param('id', (req, res, next, value) => {\n\tif (!/^\\d+$/.test(value)) return res.status(400).json({ error: 'Invalid ID' })\n\tnext()\n})\n\napp.listen(3000)",
73
+ "tips": [
74
+ "All route methods (get, post, etc.) return the app instance, allowing chaining.",
75
+ "app.set('key', value) and app.get('key') share a dual-purpose API like Express.",
76
+ "The { secure: true } route option rejects non-HTTPS requests with 403."
77
+ ]
78
+ },
79
+ {
80
+ "name": "Router",
81
+ "description": "Creates a standalone modular router instance for organizing routes into sub-apps. Mount routers with app.use(prefix, router). Supports all the same route methods as createApp, plus route chaining and introspection.",
82
+ "methods": [
83
+ { "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register a GET route on this router." },
84
+ { "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register a POST route." },
85
+ { "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register a PUT route." },
86
+ { "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register a DELETE route." },
87
+ { "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register a PATCH route." },
88
+ { "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register an OPTIONS route." },
89
+ { "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register a HEAD route." },
90
+ { "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register a handler for all HTTP methods." },
91
+ { "method": "use", "signature": "use([prefix], handler)", "description": "Mount middleware or a nested router." },
92
+ { "method": "route", "signature": "route(path)", "description": "Create a route chain: router.route('/items').get(fn).post(fn)." },
93
+ { "method": "routes", "signature": "routes()", "description": "Return the route table for this router." }
94
+ ],
95
+ "example": "const { createApp, Router, json } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\nconst users = Router()\nusers.get('/', (req, res) => res.json([]))\nusers.get('/:id', (req, res) => res.json({ id: req.params.id }))\nusers.post('/', (req, res) => res.status(201).json(req.body))\n\nconst posts = Router()\nposts.route('/')\n\t.get((req, res) => res.json([]))\n\t.post((req, res) => res.status(201).json(req.body))\nposts.get('/:id', (req, res) => res.json({ id: req.params.id }))\n\napp.use('/api/users', users)\napp.use('/api/posts', posts)\n\napp.get('/debug/routes', (req, res) => res.json(app.routes()))\n\napp.listen(3000)",
96
+ "tips": [
97
+ "Routers can be nested — mount a Router inside another Router.",
98
+ "Use app.routes() to see the full merged route table for debugging.",
99
+ "The { secure } route option works on router-level routes too."
100
+ ]
101
+ },
102
+ {
103
+ "name": "Request",
104
+ "description": "The request object wraps Node's IncomingMessage with Express-compatible properties and helpers. Available as the first argument to every route handler and middleware.",
105
+ "methods": [
106
+ { "method": "method", "signature": "req.method", "description": "HTTP method string ('GET', 'POST', etc.)." },
107
+ { "method": "url", "signature": "req.url", "description": "Full URL including query string." },
108
+ { "method": "path", "signature": "req.path", "description": "URL path without query string." },
109
+ { "method": "headers", "signature": "req.headers", "description": "Lower-cased request headers object." },
110
+ { "method": "query", "signature": "req.query", "description": "Parsed query-string key/value pairs." },
111
+ { "method": "params", "signature": "req.params", "description": "Route parameters from path segments (e.g. /:id)." },
112
+ { "method": "body", "signature": "req.body", "description": "Request body (populated by body-parsing middleware)." },
113
+ { "method": "cookies", "signature": "req.cookies", "description": "Parsed cookies (populated by cookieParser)." },
114
+ { "method": "signedCookies", "signature": "req.signedCookies", "description": "Verified signed cookies (populated by cookieParser with a secret)." },
115
+ { "method": "secret", "signature": "req.secret", "description": "First signing secret (set by cookieParser). Used by res.cookie({ signed: true })." },
116
+ { "method": "ip", "signature": "req.ip", "description": "Remote IP address." },
117
+ { "method": "secure", "signature": "req.secure", "description": "true if the connection is over TLS." },
118
+ { "method": "protocol", "signature": "req.protocol", "description": "'https' or 'http'." },
119
+ { "method": "hostname", "signature": "req.hostname", "description": "Hostname from the Host header." },
120
+ { "method": "subdomains", "signature": "req.subdomains([offset])", "description": "Array of subdomains (offset defaults to 2)." },
121
+ { "method": "originalUrl", "signature": "req.originalUrl", "description": "Original URL as received — never rewritten by middleware." },
122
+ { "method": "baseUrl", "signature": "req.baseUrl", "description": "The URL prefix on which the current router was mounted." },
123
+ { "method": "locals", "signature": "req.locals", "description": "Request-scoped data store (merged from app.locals)." },
124
+ { "method": "id", "signature": "req.id", "description": "Unique request ID (set by requestId middleware)." },
125
+ { "method": "get", "signature": "req.get(name)", "description": "Get a request header (case-insensitive)." },
126
+ { "method": "is", "signature": "req.is(type)", "description": "Check if Content-Type matches (e.g. req.is('json'))." },
127
+ { "method": "accepts", "signature": "req.accepts(...types)", "description": "Content negotiation — which types the client accepts." },
128
+ { "method": "fresh", "signature": "req.fresh", "description": "true if the client cache is still valid (ETag/Last-Modified)." },
129
+ { "method": "stale", "signature": "req.stale", "description": "Inverse of fresh." },
130
+ { "method": "xhr", "signature": "req.xhr", "description": "true if X-Requested-With is XMLHttpRequest." },
131
+ { "method": "range", "signature": "req.range(size)", "description": "Parse the Range header. Returns object or -1/-2." },
132
+ { "method": "raw", "signature": "req.raw", "description": "The original Node IncomingMessage." }
133
+ ],
134
+ "tips": [
135
+ "req.query is always an object — even if no query string is present, it defaults to {}.",
136
+ "req.body is undefined until a body-parsing middleware populates it.",
137
+ "req.locals persists for the life of the request — use it to pass data between middleware."
138
+ ]
139
+ },
140
+ {
141
+ "name": "Response",
142
+ "description": "The response object wraps Node's ServerResponse with chainable methods for setting status codes, headers, cookies, and sending various response types. Available as the second argument to every route handler.",
143
+ "methods": [
144
+ { "method": "status", "signature": "res.status(code)", "description": "Set the HTTP status code. Chainable." },
145
+ { "method": "set", "signature": "res.set(name, value)", "description": "Set a response header. Chainable." },
146
+ { "method": "get", "signature": "res.get(name)", "description": "Get a previously-set response header." },
147
+ { "method": "append", "signature": "res.append(name, value)", "description": "Append a value to a header." },
148
+ { "method": "vary", "signature": "res.vary(field)", "description": "Add a field to the Vary header." },
149
+ { "method": "type", "signature": "res.type(ct)", "description": "Set Content-Type. Chainable." },
150
+ { "method": "send", "signature": "res.send(body)", "description": "Send a response (string, Buffer, object, or null). Auto-sets Content-Type." },
151
+ { "method": "json", "signature": "res.json(obj)", "description": "Send a JSON response with Content-Type: application/json." },
152
+ { "method": "text", "signature": "res.text(str)", "description": "Send a plain text response." },
153
+ { "method": "html", "signature": "res.html(str)", "description": "Send an HTML response." },
154
+ { "method": "sendStatus", "signature": "res.sendStatus(code)", "description": "Send only the status code with its reason phrase as body." },
155
+ { "method": "sendFile", "signature": "res.sendFile(path, [opts], [cb])", "description": "Stream a file as the response with appropriate Content-Type." },
156
+ { "method": "download", "signature": "res.download(path, [filename], [cb])", "description": "Prompt a file download with Content-Disposition header." },
157
+ { "method": "cookie", "signature": "res.cookie(name, value, [opts])", "description": "Set a cookie. Supports signed (auto-sign via req.secret), priority (Low/Medium/High), partitioned (CHIPS), and auto-serializes objects as JSON cookies (j: prefix). Chainable." },
158
+ { "method": "clearCookie", "signature": "res.clearCookie(name, [opts])", "description": "Clear a cookie by setting it to expire. Chainable." },
159
+ { "method": "redirect", "signature": "res.redirect([status], url)", "description": "Send a redirect response. Default status: 302." },
160
+ { "method": "format", "signature": "res.format(types)", "description": "Content negotiation — respond based on Accept header. Keys are MIME types." },
161
+ { "method": "links", "signature": "res.links(links)", "description": "Set the Link header from { rel: url } pairs. Chainable." },
162
+ { "method": "location", "signature": "res.location(url)", "description": "Set the Location header. Chainable." },
163
+ { "method": "sse", "signature": "res.sse([opts])", "description": "Open a Server-Sent Events stream. See Real-Time → SSE." },
164
+ { "method": "headersSent", "signature": "res.headersSent", "description": "true if headers have already been sent." },
165
+ { "method": "locals", "signature": "res.locals", "description": "Request-scoped data store." },
166
+ { "method": "raw", "signature": "res.raw", "description": "The original Node ServerResponse." }
167
+ ],
168
+ "options": [
169
+ { "option": "domain", "type": "string", "default": "—", "notes": "Cookie domain scope." },
170
+ { "option": "path", "type": "string", "default": "'/'", "notes": "Cookie URL path scope." },
171
+ { "option": "expires", "type": "Date | number", "default": "—", "notes": "Cookie expiration date." },
172
+ { "option": "maxAge", "type": "number", "default": "—", "notes": "Cookie lifetime in seconds." },
173
+ { "option": "httpOnly", "type": "boolean", "default": "false", "notes": "Restrict cookie to HTTP(S) only — no JavaScript access." },
174
+ { "option": "secure", "type": "boolean", "default": "false", "notes": "Send cookie only over HTTPS." },
175
+ { "option": "sameSite", "type": "string", "default": "—", "notes": "'Strict', 'Lax', or 'None'. Controls cross-site cookie behavior." },
176
+ { "option": "signed", "type": "boolean", "default": "false", "notes": "Auto-sign the cookie value using req.secret (set by cookieParser)." },
177
+ { "option": "priority", "type": "string", "default": "—", "notes": "'Low', 'Medium', or 'High'. Browser cookie eviction priority." },
178
+ { "option": "partitioned", "type": "boolean", "default": "false", "notes": "CHIPS partitioned attribute. Requires secure: true. Isolates cookies per top-level site." }
179
+ ],
180
+ "example": "app.get('/data', (req, res) => {\n\t// Content negotiation\n\tres.format({\n\t\t'text/html': () => res.html('<h1>Data</h1>'),\n\t\t'application/json': () => res.json({ data: true }),\n\t\t'text/plain': () => res.text('data'),\n\t\tdefault: () => res.status(406).json({ error: 'Not Acceptable' })\n\t})\n})\n\n// Chaining\nres.status(201).set('X-Custom', 'value').json({ ok: true })\n\n// Cookies with new features\nres.cookie('session', 'abc', { signed: true, httpOnly: true, secure: true })\nres.cookie('prefs', { theme: 'dark' }, { maxAge: 86400 }) // auto JSON\nres.cookie('important', 'val', { priority: 'High', partitioned: true })",
181
+ "tips": [
182
+ "res.cookie() with signed: true requires cookieParser to be configured with a secret.",
183
+ "Object values passed to res.cookie() are auto-serialized with the j: prefix for JSON cookies.",
184
+ "res.send(object) calls res.json() automatically — you can use either interchangeably."
185
+ ]
186
+ }
187
+ ]
188
+ },
189
+ {
190
+ "section": "Body Parsers",
191
+ "icon": "parse",
192
+ "items": [
193
+ {
194
+ "name": "json",
195
+ "description": "Parses JSON request bodies into req.body. Matches Content-Type application/json by default. Supports size limits, strict mode (reject non-object/array roots), and custom reviver functions.",
196
+ "options": [
197
+ { "option": "limit", "type": "string | number", "default": "'1mb'", "notes": "Maximum body size (e.g. '10kb', '5mb', or bytes as number)." },
198
+ { "option": "strict", "type": "boolean", "default": "true", "notes": "Reject payloads whose root is not an object or array." },
199
+ { "option": "reviver", "type": "function", "default": "—", "notes": "JSON.parse reviver function for custom deserialization." },
200
+ { "option": "type", "type": "string | function", "default": "'application/json'", "notes": "Content-Type to match. Can be a function returning boolean." },
201
+ { "option": "requireSecure", "type": "boolean", "default": "false", "notes": "Reject non-HTTPS requests with 403." }
202
+ ],
203
+ "example": "const { createApp, json } = require('zero-http')\nconst app = createApp()\n\napp.use(json({ limit: '10kb', strict: true }))\n\napp.post('/data', (req, res) => {\n\tconsole.log(req.body) // parsed JSON object\n\tres.json({ received: req.body })\n})",
204
+ "tips": [
205
+ "With strict: true (default), primitives like '\"hello\"' or '42' are rejected — only {} and [] are allowed.",
206
+ "The limit option accepts human-readable strings: '100kb', '1mb', '500b'.",
207
+ "Use requireSecure: true on sensitive endpoints to enforce HTTPS-only body submission."
208
+ ]
209
+ },
210
+ {
211
+ "name": "urlencoded",
212
+ "description": "Parses URL-encoded form bodies (application/x-www-form-urlencoded) into req.body. Supports nested object parsing with the extended option.",
213
+ "options": [
214
+ { "option": "extended", "type": "boolean", "default": "false", "notes": "Enable nested bracket parsing (e.g. user[name]=Alice → { user: { name: 'Alice' } })." },
215
+ { "option": "limit", "type": "string | number", "default": "'1mb'", "notes": "Maximum body size." },
216
+ { "option": "type", "type": "string | function", "default": "'application/x-www-form-urlencoded'", "notes": "Content-Type to match." },
217
+ { "option": "requireSecure", "type": "boolean", "default": "false", "notes": "Reject non-HTTPS requests with 403." }
218
+ ],
219
+ "example": "const { createApp, urlencoded } = require('zero-http')\nconst app = createApp()\n\napp.use(urlencoded({ extended: true }))\n\napp.post('/form', (req, res) => {\n\tconsole.log(req.body) // { name: 'Alice', age: '30' }\n\tres.json(req.body)\n})"
220
+ },
221
+ {
222
+ "name": "text",
223
+ "description": "Reads the raw request body as a UTF-8 string into req.body. Matches Content-Type text/* by default.",
224
+ "options": [
225
+ { "option": "type", "type": "string | function", "default": "'text/*'", "notes": "Content-Type to match." },
226
+ { "option": "limit", "type": "string | number", "default": "'1mb'", "notes": "Maximum body size." },
227
+ { "option": "encoding", "type": "string", "default": "'utf8'", "notes": "Character encoding." },
228
+ { "option": "requireSecure", "type": "boolean", "default": "false", "notes": "Reject non-HTTPS requests with 403." }
229
+ ],
230
+ "example": "const { createApp, text } = require('zero-http')\nconst app = createApp()\n\napp.use(text())\n\napp.post('/log', (req, res) => {\n\tconsole.log(typeof req.body) // 'string'\n\tres.text('Received: ' + req.body)\n})"
231
+ },
232
+ {
233
+ "name": "raw",
234
+ "description": "Reads the raw request body as a Buffer into req.body. Useful for binary data, webhooks, or custom protocols.",
235
+ "options": [
236
+ { "option": "type", "type": "string | function", "default": "'application/octet-stream'", "notes": "Content-Type to match." },
237
+ { "option": "limit", "type": "string | number", "default": "'1mb'", "notes": "Maximum body size." },
238
+ { "option": "requireSecure", "type": "boolean", "default": "false", "notes": "Reject non-HTTPS requests with 403." }
239
+ ],
240
+ "example": "const { createApp, raw } = require('zero-http')\nconst app = createApp()\n\napp.use(raw({ type: 'application/octet-stream', limit: '5mb' }))\n\napp.post('/webhook', (req, res) => {\n\tconsole.log(Buffer.isBuffer(req.body)) // true\n\tres.sendStatus(200)\n})"
241
+ },
242
+ {
243
+ "name": "multipart",
244
+ "description": "Streams multipart/form-data file uploads to disk and populates req.body with { fields, files }. Each file entry includes the original filename, stored path, content type, and size.",
245
+ "options": [
246
+ { "option": "dir", "type": "string", "default": "os.tmpdir() + '/zero-http-uploads'", "notes": "Upload directory. Created automatically if it doesn't exist." },
247
+ { "option": "maxFileSize", "type": "number", "default": "—", "notes": "Maximum file size in bytes. Rejects oversized files with 413." },
248
+ { "option": "requireSecure", "type": "boolean", "default": "false", "notes": "Reject non-HTTPS requests with 403." }
249
+ ],
250
+ "example": "const path = require('path')\nconst { createApp, multipart } = require('zero-http')\nconst app = createApp()\n\napp.post('/upload', multipart({\n\tdir: path.join(__dirname, 'uploads'),\n\tmaxFileSize: 10 * 1024 * 1024 // 10 MB\n}), (req, res) => {\n\tres.json({\n\t\tfiles: req.body.files, // [{ originalFilename, storedName, path, contentType, size }]\n\t\tfields: req.body.fields // { description: 'My photo' }\n\t})\n})",
251
+ "tips": [
252
+ "Files are streamed to disk, not buffered in memory — safe for large uploads.",
253
+ "The upload directory is created automatically if it doesn't exist.",
254
+ "Each file object contains: originalFilename, storedName, path, contentType, size."
255
+ ]
256
+ }
257
+ ]
258
+ },
259
+ {
260
+ "section": "Middleware",
261
+ "icon": "layers",
262
+ "items": [
263
+ {
264
+ "name": "cors",
265
+ "description": "CORS middleware with automatic preflight handling. Supports exact origins, subdomain matching with dot-prefix syntax, credentials, and configurable allowed methods/headers.",
266
+ "options": [
267
+ { "option": "origin", "type": "string | string[]", "default": "'*'", "notes": "Allowed origins. Use '.example.com' for subdomain wildcards." },
268
+ { "option": "methods", "type": "string", "default": "'GET,POST,PUT,DELETE,PATCH,OPTIONS'", "notes": "Allowed HTTP methods." },
269
+ { "option": "allowedHeaders", "type": "string", "default": "—", "notes": "Allowed request headers." },
270
+ { "option": "exposedHeaders", "type": "string", "default": "—", "notes": "Headers exposed to the browser." },
271
+ { "option": "credentials", "type": "boolean", "default": "false", "notes": "Allow credentials (cookies, auth headers)." },
272
+ { "option": "maxAge", "type": "number", "default": "—", "notes": "Preflight cache duration in seconds." }
273
+ ],
274
+ "example": "const { createApp, cors, json } = require('zero-http')\nconst app = createApp()\n\napp.use(cors({\n\torigin: [\n\t\t'https://mysite.com', // exact match\n\t\t'.mysite.com' // matches api.mysite.com, www.mysite.com, etc.\n\t],\n\tcredentials: true,\n\tmethods: 'GET,POST,PUT,DELETE'\n}))\napp.use(json())\n\napp.get('/data', (req, res) => res.json({ secure: true }))",
275
+ "tips": [
276
+ "Use the dot-prefix '.example.com' to match all subdomains without listing each one.",
277
+ "When credentials: true, origin cannot be '*' — you must specify explicit origins.",
278
+ "Preflight OPTIONS requests are handled automatically — no extra route needed."
279
+ ]
280
+ },
281
+ {
282
+ "name": "compress",
283
+ "description": "Response compression middleware. Auto-negotiates the best encoding (brotli > gzip > deflate) based on the client's Accept-Encoding header. Brotli preferred when available (Node 11.7+). Automatically skips SSE streams.",
284
+ "options": [
285
+ { "option": "threshold", "type": "number", "default": "1024", "notes": "Minimum body size in bytes to compress." },
286
+ { "option": "level", "type": "number", "default": "-1 (zlib default)", "notes": "Compression level." },
287
+ { "option": "filter", "type": "function", "default": "—", "notes": "Return false to skip compression: (req, res) => boolean." }
288
+ ],
289
+ "example": "const { createApp, compress, json } = require('zero-http')\nconst app = createApp()\n\napp.use(compress({ threshold: 512, level: 6 }))\napp.use(json())\n\napp.get('/big', (req, res) => {\n\tres.json({ data: 'x'.repeat(10000) }) // compressed\n})\n\napp.get('/small', (req, res) => {\n\tres.json({ ok: true }) // below threshold — sent raw\n})",
290
+ "tips": [
291
+ "Brotli typically achieves 15-25% better compression than gzip at similar speeds.",
292
+ "SSE streams are automatically excluded from compression.",
293
+ "Set threshold: 0 to compress everything, but very small responses don't benefit."
294
+ ]
295
+ },
296
+ {
297
+ "name": "helmet",
298
+ "description": "Security headers middleware. Sets Content-Security-Policy, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and 11 more headers. Every header can be individually configured or disabled with false.",
299
+ "options": [
300
+ { "option": "contentSecurityPolicy", "type": "object | false", "default": "default policy", "notes": "CSP directives or false to disable." },
301
+ { "option": "frameguard", "type": "string | false", "default": "'deny'", "notes": "'deny', 'sameorigin', or false." },
302
+ { "option": "hsts", "type": "boolean | false", "default": "true", "notes": "Strict-Transport-Security header." },
303
+ { "option": "hstsMaxAge", "type": "number", "default": "15552000", "notes": "HSTS max-age in seconds (180 days)." },
304
+ { "option": "noSniff", "type": "boolean", "default": "true", "notes": "X-Content-Type-Options: nosniff." },
305
+ { "option": "referrerPolicy", "type": "string | false", "default": "'no-referrer'", "notes": "Referrer-Policy header value." },
306
+ { "option": "hidePoweredBy", "type": "boolean", "default": "true", "notes": "Remove X-Powered-By header." },
307
+ { "option": "xssFilter", "type": "boolean", "default": "false", "notes": "Legacy X-XSS-Protection header." }
308
+ ],
309
+ "example": "const { createApp, helmet, json } = require('zero-http')\nconst app = createApp()\n\n// Sensible defaults\napp.use(helmet())\n\n// Custom CSP + disable HSTS\napp.use(helmet({\n\tcontentSecurityPolicy: {\n\t\tdefaultSrc: [\"'self'\"],\n\t\tscriptSrc: [\"'self'\", 'cdn.example.com'],\n\t\timgSrc: [\"'self'\", 'data:', '*.cloudfront.net']\n\t},\n\thsts: false,\n\tframeguard: 'sameorigin'\n}))",
310
+ "tips": [
311
+ "helmet() with no options gives you a strong security baseline out of the box.",
312
+ "Set any header to false to disable it individually.",
313
+ "CSP headers are critical for preventing XSS — always configure explicitly for production apps."
314
+ ]
315
+ },
316
+ {
317
+ "name": "static",
318
+ "description": "Serves static files from a directory with automatic Content-Type detection (60+ MIME types), cache headers, dotfile protection, and HTML extension fallback.",
319
+ "options": [
320
+ { "option": "index", "type": "string | false", "default": "'index.html'", "notes": "Default file for directory requests. Set false to disable." },
321
+ { "option": "maxAge", "type": "number", "default": "0", "notes": "Cache-Control max-age in milliseconds." },
322
+ { "option": "dotfiles", "type": "string", "default": "'ignore'", "notes": "'allow', 'deny', or 'ignore'. Controls access to dotfiles." },
323
+ { "option": "extensions", "type": "string[]", "default": "—", "notes": "Try these extensions if the file isn't found (e.g. ['html', 'htm'])." },
324
+ { "option": "setHeaders", "type": "function", "default": "—", "notes": "Custom header setter: (res, filePath) => {}." }
325
+ ],
326
+ "example": "const path = require('path')\nconst { createApp, static: serveStatic } = require('zero-http')\nconst app = createApp()\n\napp.use(serveStatic(path.join(__dirname, 'public'), {\n\tindex: 'index.html',\n\tmaxAge: 3600000, // 1 hour\n\tdotfiles: 'ignore',\n\textensions: ['html', 'htm'],\n\tsetHeaders: (res, filePath) => {\n\t\tres.setHeader('X-Served-By', 'zero-http')\n\t}\n}))",
327
+ "tips": [
328
+ "'static' is a reserved word in JavaScript — import as: const { static: serveStatic } = require('zero-http').",
329
+ "Set maxAge to at least 1 hour (3600000ms) in production for cache performance.",
330
+ "dotfiles: 'deny' returns 403 for requests like /.env or /.git — use in production."
331
+ ]
332
+ },
333
+ {
334
+ "name": "rateLimit",
335
+ "description": "Per-IP rate limiter middleware. Tracks request counts in a sliding window and returns 429 Too Many Requests when exceeded. Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers.",
336
+ "options": [
337
+ { "option": "windowMs", "type": "number", "default": "60000", "notes": "Time window in milliseconds (default: 1 minute)." },
338
+ { "option": "max", "type": "number", "default": "100", "notes": "Maximum requests per window per IP." },
339
+ { "option": "message", "type": "string", "default": "—", "notes": "Custom error message for rate-limited responses." },
340
+ { "option": "statusCode", "type": "number", "default": "429", "notes": "HTTP status code for rate-limited responses." },
341
+ { "option": "keyGenerator", "type": "function", "default": "req => req.ip", "notes": "Custom key extraction function for grouping requests." },
342
+ { "option": "skip", "type": "function", "default": "—", "notes": "(req) => boolean. Return true to skip rate limiting for the request (e.g. whitelisted IPs or admin users)." },
343
+ { "option": "handler", "type": "function", "default": "—", "notes": "(req, res) => void. Custom handler for rate-limited requests instead of the default JSON error response." }
344
+ ],
345
+ "example": "const { createApp, json, rateLimit } = require('zero-http')\nconst app = createApp()\n\n// Global: 200 req/min per IP\napp.use(rateLimit({ windowMs: 60000, max: 200 }))\napp.use(json())\n\n// Login: 5 attempts per minute\napp.post('/login', rateLimit({\n\twindowMs: 60000,\n\tmax: 5,\n\tmessage: 'Too many login attempts'\n}), (req, res) => {\n\tres.json({ ok: true })\n})",
346
+ "tips": [
347
+ "Apply strict limits to authentication endpoints (5-10 per minute).",
348
+ "Rate limit headers (X-RateLimit-*) are set automatically for all responses.",
349
+ "Use keyGenerator to rate-limit by API key instead of IP for authenticated APIs.",
350
+ "Use skip to bypass rate limiting for trusted clients: skip: (req) => req.ip === '127.0.0.1'.",
351
+ "Use handler to customize the rate-limited response (e.g. redirect, HTML page, or custom JSON)."
352
+ ]
353
+ },
354
+ {
355
+ "name": "timeout",
356
+ "description": "Request timeout middleware. If a handler doesn't respond within the specified duration, automatically sends a 408 response and sets req.timedOut to true.",
357
+ "options": [
358
+ { "option": "status", "type": "number", "default": "408", "notes": "HTTP status code for timeout responses." },
359
+ { "option": "message", "type": "string", "default": "'Request Timeout'", "notes": "Error message body." }
360
+ ],
361
+ "example": "const { createApp, timeout } = require('zero-http')\nconst app = createApp()\n\napp.use(timeout(10000)) // 10s timeout\n\napp.get('/slow', async (req, res) => {\n\tawait new Promise(r => setTimeout(r, 15000))\n\tif (req.timedOut) return // already sent 408\n\tres.json({ data: 'done' })\n})",
362
+ "tips": [
363
+ "Always check req.timedOut before responding in long-running handlers.",
364
+ "Set different timeouts per route by applying timeout() as route middleware.",
365
+ "30 seconds is a reasonable default for most APIs."
366
+ ]
367
+ },
368
+ {
369
+ "name": "requestId",
370
+ "description": "Generates a unique request ID (UUID v4 by default) for each request. Sets req.id and adds the ID as a response header for log correlation.",
371
+ "options": [
372
+ { "option": "header", "type": "string", "default": "'X-Request-Id'", "notes": "Response header name." },
373
+ { "option": "generator", "type": "function", "default": "crypto.randomUUID", "notes": "Custom ID generator function." },
374
+ { "option": "trustProxy", "type": "boolean", "default": "false", "notes": "Trust incoming X-Request-Id from upstream proxies." }
375
+ ],
376
+ "example": "const { createApp, requestId, logger } = require('zero-http')\nconst app = createApp()\n\napp.use(requestId()) // sets req.id + X-Request-Id header\napp.use(logger()) // logs include the request ID\n\napp.get('/info', (req, res) => {\n\tres.json({ requestId: req.id })\n})"
377
+ },
378
+ {
379
+ "name": "logger",
380
+ "description": "Request logger middleware with response timing. Logs method, URL, status code, and response time in configurable formats with optional colorized output.",
381
+ "options": [
382
+ { "option": "format", "type": "string", "default": "'dev'", "notes": "'dev' (verbose), 'short' (compact), or 'tiny' (minimal)." },
383
+ { "option": "logger", "type": "function", "default": "console.log", "notes": "Custom log function (e.g. write to file)." },
384
+ { "option": "colors", "type": "boolean", "default": "auto (TTY detection)", "notes": "Colorize output." }
385
+ ],
386
+ "example": "const fs = require('fs')\nconst { createApp, logger } = require('zero-http')\nconst app = createApp()\n\n// Colorized dev output to stdout\napp.use(logger({ format: 'dev' }))\n\n// Or: append to a log file\napp.use(logger({\n\tformat: 'short',\n\tcolors: false,\n\tlogger: (msg) => fs.appendFileSync('access.log', msg + '\\n')\n}))"
387
+ }
388
+ ]
389
+ },
390
+ {
391
+ "section": "Cookies & Security",
392
+ "icon": "shield",
393
+ "items": [
394
+ {
395
+ "name": "cookieParser",
396
+ "description": "Cookie parsing middleware with timing-safe HMAC-SHA256 signature verification, JSON cookie support, secret rotation, and static helper methods. Parses the Cookie header into req.cookies and req.signedCookies. When a secret is provided, signed cookies (s: prefix) are verified using crypto.timingSafeEqual to prevent timing attacks. JSON cookies (j: prefix) are automatically parsed into objects. Exposes req.secret and req.secrets for downstream middleware like res.cookie({ signed: true }) and csrf().",
397
+ "methods": [
398
+ { "method": "sign", "signature": "cookieParser.sign(value, secret)", "description": "Sign a value with HMAC-SHA256. Returns 's:<value>.<signature>'." },
399
+ { "method": "unsign", "signature": "cookieParser.unsign(value, secrets)", "description": "Verify and unsign a signed cookie. Tries all provided secrets (rotation support). Returns the original value or false." },
400
+ { "method": "jsonCookie", "signature": "cookieParser.jsonCookie(value)", "description": "Serialize a value as a JSON cookie string: 'j:' + JSON.stringify(value)." },
401
+ { "method": "parseJSON", "signature": "cookieParser.parseJSON(str)", "description": "Parse a JSON cookie string (j: prefix). Returns parsed value or original string." }
402
+ ],
403
+ "options": [
404
+ { "option": "secret", "type": "string | string[]", "default": "—", "notes": "Signing secret. Pass an array for key rotation — newest first. When set, populates req.secret and req.secrets." },
405
+ { "option": "decode", "type": "boolean", "default": "true", "notes": "URI-decode cookie values." }
406
+ ],
407
+ "example": "const { createApp, cookieParser, json } = require('zero-http')\nconst app = createApp()\n\n// Basic setup with signing secret\napp.use(cookieParser('super-secret-key'))\napp.use(json())\n\n// Reading cookies\napp.get('/read', (req, res) => {\n\tres.json({\n\t\tcookies: req.cookies, // { theme: 'dark', prefs: { lang: 'en' } }\n\t\tsigned: req.signedCookies, // { session: 'abc123' }\n\t\tsecret: req.secret // available for downstream use\n\t})\n})\n\n// Setting cookies with new features\napp.get('/set', (req, res) => {\n\t// Auto-sign using req.secret\n\tres.cookie('session', 'abc123', { signed: true, httpOnly: true, secure: true })\n\n\t// JSON cookie — objects auto-serialize with j: prefix\n\tres.cookie('prefs', { lang: 'en', theme: 'dark' }, { maxAge: 86400 })\n\n\t// Priority + Partitioned (CHIPS)\n\tres.cookie('important', 'value', { priority: 'High', partitioned: true, secure: true })\n\n\tres.json({ ok: true })\n})\n\n// Secret rotation — verify with old key, sign with new key\napp.use(cookieParser(['new-secret', 'old-secret']))\n\n// Static helpers for manual use\nconst signed = cookieParser.sign('data', 'secret')\nconst value = cookieParser.unsign(signed, ['secret']) // 'data' or false\nconst parsed = cookieParser.jsonCookie({ a: 1 }) // 'j:{\"a\":1}'",
408
+ "tips": [
409
+ "Always use signed cookies for session tokens and sensitive data.",
410
+ "For key rotation, pass secrets as an array: ['new-key', 'old-key']. Cookies are verified against all secrets but signed with the first.",
411
+ "Signature verification uses crypto.timingSafeEqual() to prevent timing attacks — never downgrade to string comparison.",
412
+ "JSON cookies (j: prefix) are auto-parsed — no need to manually JSON.parse cookie values.",
413
+ "res.cookie() with signed: true requires cookieParser to be configured with a secret. It reads req.secret automatically.",
414
+ "The partitioned attribute (CHIPS) isolates cookies per top-level site — useful for third-party embedded widgets."
415
+ ]
416
+ },
417
+ {
418
+ "name": "csrf",
419
+ "description": "Double-submit cookie CSRF protection. Generates a cryptographically random token, stores it in a cookie, and validates it on state-changing requests. GET, HEAD, and OPTIONS are automatically skipped. Tokens can be sent via header, body (_csrf field), or query parameter.",
420
+ "options": [
421
+ { "option": "cookie", "type": "string", "default": "'_csrf'", "notes": "Cookie name for the CSRF token." },
422
+ { "option": "header", "type": "string", "default": "'x-csrf-token'", "notes": "Request header to check for the token." },
423
+ { "option": "saltLength", "type": "number", "default": "18", "notes": "Token salt length in bytes." },
424
+ { "option": "secret", "type": "string", "default": "auto-generated", "notes": "Secret for token generation. Auto-generated if not provided." },
425
+ { "option": "ignoreMethods", "type": "string[]", "default": "['GET','HEAD','OPTIONS']", "notes": "HTTP methods that skip CSRF validation." },
426
+ { "option": "ignorePaths", "type": "string[]", "default": "[]", "notes": "URL paths to skip (e.g. webhook endpoints)." },
427
+ { "option": "onError", "type": "function", "default": "—", "notes": "Custom error handler: (err, req, res, next) => {}." }
428
+ ],
429
+ "example": "const { createApp, csrf, json, cookieParser } = require('zero-http')\nconst app = createApp()\n\napp.use(cookieParser())\napp.use(json())\napp.use(csrf())\n\n// GET is safe — use it to read the token\napp.get('/api/csrf-token', (req, res) => {\n\tres.json({ token: req.csrfToken })\n})\n\n// POST must include the token\napp.post('/api/transfer', (req, res) => {\n\tres.json({ success: true, amount: req.body.amount })\n})\n\n// Client-side:\n// const { token } = await fetch('/api/csrf-token').then(r => r.json())\n// await fetch('/api/transfer', {\n// method: 'POST',\n// headers: { 'Content-Type': 'application/json', 'x-csrf-token': token },\n// body: JSON.stringify({ amount: 100 })\n// })",
430
+ "tips": [
431
+ "cookieParser() must be registered before csrf() even without a signing secret.",
432
+ "Use ignorePaths for webhook endpoints that receive external POST requests.",
433
+ "The token is available on req.csrfToken after the middleware runs."
434
+ ]
435
+ },
436
+ {
437
+ "name": "validate",
438
+ "description": "Request validation middleware with 11 types and auto-coercion. Validates req.body, req.query, and req.params against a declarative schema. Unknown fields are stripped by default. Returns 422 with structured error messages on failure.",
439
+ "methods": [
440
+ { "method": "validate.field", "signature": "validate.field(value, rules)", "description": "Validate a single value against rules. Returns { valid, errors, value }." },
441
+ { "method": "validate.object", "signature": "validate.object(data, schema)", "description": "Validate an object against a schema. Returns { valid, errors, sanitized }." }
442
+ ],
443
+ "options": [
444
+ { "option": "stripUnknown", "type": "boolean", "default": "true", "notes": "Remove fields not defined in the schema." },
445
+ { "option": "onError", "type": "function", "default": "—", "notes": "Custom error handler. Default sends 422 JSON." }
446
+ ],
447
+ "example": "const { createApp, json, validate } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\n// Validate body\napp.post('/users', validate({\n\tbody: {\n\t\tname: { type: 'string', required: true, minLength: 2, maxLength: 50 },\n\t\temail: { type: 'email', required: true },\n\t\tage: { type: 'integer', min: 13, max: 120 },\n\t\trole: { type: 'string', enum: ['user', 'admin'], default: 'user' },\n\t\ttags: { type: 'array', minItems: 1, maxItems: 5 }\n\t}\n}), (req, res) => {\n\tres.status(201).json(req.body) // sanitized\n})\n\n// Validate query + params\napp.get('/search/:category', validate({\n\tparams: { category: { type: 'string', enum: ['books', 'music', 'films'] } },\n\tquery: {\n\t\tq: { type: 'string', required: true },\n\t\tpage: { type: 'integer', min: 1, default: 1 },\n\t\tlimit: { type: 'integer', min: 1, max: 100, default: 20 }\n\t}\n}), (req, res) => {\n\tres.json({ params: req.params, query: req.query })\n})\n// Validation failure → 422 { errors: ['name is required', ...] }",
448
+ "tips": [
449
+ "Supported types: string, integer, number, float, boolean, array, json, date, uuid, email, url.",
450
+ "Auto-coercion: '42' → 42 (integer), 'true' → true (boolean), etc.",
451
+ "stripUnknown: true (default) prevents mass-assignment attacks by removing unknown fields.",
452
+ "Use validate.field() and validate.object() for programmatic validation outside middleware."
453
+ ]
454
+ }
455
+ ]
456
+ },
457
+ {
458
+ "section": "Environment",
459
+ "icon": "settings",
460
+ "items": [
461
+ {
462
+ "name": "env",
463
+ "description": "Typed environment variable system with .env file loading, schema validation, and type coercion. Access variables via proxy (env.PORT), function call (env('PORT')), or method (env.get('PORT')). Supports 9 types: string, number, integer, port, boolean, array, json, url, enum. Loads .env files in precedence order with process.env always winning.",
464
+ "methods": [
465
+ { "method": "load", "signature": "env.load(schema, [opts])", "description": "Load and validate environment variables from .env files against a typed schema. Throws on validation failure with all errors." },
466
+ { "method": "get", "signature": "env.get(key)", "description": "Get a typed environment variable by key." },
467
+ { "method": "require", "signature": "env.require(key)", "description": "Get a variable or throw if it's not set. Use for critical config." },
468
+ { "method": "has", "signature": "env.has(key)", "description": "Check if a variable is set (not undefined)." },
469
+ { "method": "all", "signature": "env.all()", "description": "Get all loaded values as a plain object." },
470
+ { "method": "reset", "signature": "env.reset()", "description": "Reset the env store. Useful for testing." },
471
+ { "method": "parse", "signature": "env.parse(src)", "description": "Parse a .env file string into key-value pairs. Supports comments, quotes, multiline, interpolation, and export prefix." }
472
+ ],
473
+ "options": [
474
+ { "option": "path", "type": "string", "default": "process.cwd()", "notes": "Custom directory to load .env files from." },
475
+ { "option": "override", "type": "boolean", "default": "false", "notes": "Write file values into process.env (normally process.env takes precedence)." }
476
+ ],
477
+ "example": "const { createApp, env } = require('zero-http')\n\n// Load with typed schema — validates and coerces on startup\nenv.load({\n\tPORT: { type: 'port', default: 3000 },\n\tDATABASE_URL: { type: 'url', required: true },\n\tDEBUG: { type: 'boolean', default: false },\n\tALLOWED_IPS: { type: 'array', separator: ',', default: [] },\n\tNODE_ENV: { type: 'enum', values: ['development', 'production', 'test'], default: 'development' },\n\tMAX_UPLOAD_MB:{ type: 'integer', min: 1, max: 100, default: 10 },\n\tAPP_CONFIG: { type: 'json', default: {} }\n})\n\nconst app = createApp()\n\napp.get('/config', (req, res) => res.json({\n\tport: env.PORT, // number: 3000\n\tdebug: env.DEBUG, // boolean: false\n\tips: env.ALLOWED_IPS, // array: ['10.0.0.1', '10.0.0.2']\n\tenv: env.NODE_ENV // string: 'development'\n}))\n\napp.listen(env.PORT)",
478
+ "tips": [
479
+ "env.load() should be called before createApp() — config errors are caught at startup, not at request time.",
480
+ "process.env always wins over .env file values. Use override: true to reverse this.",
481
+ "Schema types with examples: port (0-65535), boolean ('true'/'yes'/'1'/'on' → true), array ('a,b,c' → ['a','b','c']), json ('{\"key\":1}' → object).",
482
+ "Use env.require('KEY') for critical config that must exist — it throws immediately with a clear error message."
483
+ ]
484
+ },
485
+ {
486
+ "name": ".env File Format",
487
+ "description": "The env module loads .env files in a specific precedence order with full parsing support. Files are loaded from the working directory (or custom path) in this order: .env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local. Later files override earlier ones. process.env always takes final precedence.",
488
+ "example": "# .env — shared defaults\nPORT=3000\nDATABASE_URL=postgres://localhost:5432/mydb\nDEBUG=false\nALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173\nAPP_NAME=My App\n\n# Comments start with #\nexport SECRET_KEY=my-secret # 'export' prefix is stripped\n\n# Quoted values (single, double, backtick)\nGREETING=\"Hello World\"\nMESSAGE='Single quoted'\nTEMPLATE=`Backtick quoted`\n\n# Multiline values\nRSA_KEY=\"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----\"\n\n# Variable interpolation\nAPI_URL=https://api.example.com\nHEALTH_URL=${API_URL}/health\n\n# .env.local — local overrides (gitignore this)\nDATABASE_URL=postgres://localhost:5432/mydb_local\nDEBUG=true\n\n# .env.production — production settings\nDATABASE_URL=postgres://prod-server:5432/mydb\nDEBUG=false\nNODE_ENV=production",
489
+ "exampleLang": "bash",
490
+ "tips": [
491
+ "File load order: .env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local. Each subsequent file overrides previous values.",
492
+ "Always .gitignore .env.local and .env.*.local files — they contain machine-specific secrets.",
493
+ "Supports variable interpolation: API_URL=https://api.com then HEALTH=${API_URL}/health.",
494
+ "The 'export' keyword prefix is stripped automatically — compatible with shell sourceable .env files.",
495
+ "Multiline values are supported with quotes — the value continues until the matching closing quote.",
496
+ "Inline comments are supported in unquoted values: PORT=3000 # web server port."
497
+ ]
498
+ },
499
+ {
500
+ "name": "Schema Types",
501
+ "description": "The env.load() schema supports 9 typed field definitions with validation constraints. Each field can have type, required, default, and type-specific options like min/max, match, separator, and values.",
502
+ "options": [
503
+ { "option": "string", "type": "type", "default": "—", "notes": "Basic string. Supports min (min length), max (max length), match (RegExp pattern)." },
504
+ { "option": "number", "type": "type", "default": "—", "notes": "Floating-point number. Supports min, max range validation." },
505
+ { "option": "integer", "type": "type", "default": "—", "notes": "Whole number (parseInt). Supports min, max range validation." },
506
+ { "option": "port", "type": "type", "default": "—", "notes": "Integer 0-65535. Rejects values outside valid port range." },
507
+ { "option": "boolean", "type": "type", "default": "—", "notes": "Truthy: 'true', '1', 'yes', 'on'. Falsy: 'false', '0', 'no', 'off', ''." },
508
+ { "option": "array", "type": "type", "default": "—", "notes": "Split string by separator (default: ','). Trims items and removes empties." },
509
+ { "option": "json", "type": "type", "default": "—", "notes": "JSON.parse the value. Throws with clear error if invalid JSON." },
510
+ { "option": "url", "type": "type", "default": "—", "notes": "Validated URL string. Throws if not a valid URL (uses URL constructor)." },
511
+ { "option": "enum", "type": "type", "default": "—", "notes": "Must be one of values: []. Throws with allowed values in error message." }
512
+ ],
513
+ "example": "env.load({\n\t// String with pattern matching\n\tAPI_KEY: { type: 'string', required: true, min: 32, match: /^sk_/ },\n\n\t// Number with range\n\tTIMEOUT_MS: { type: 'number', min: 100, max: 60000, default: 5000 },\n\n\t// Integer\n\tWORKERS: { type: 'integer', min: 1, max: 16, default: 4 },\n\n\t// Port (validated 0-65535)\n\tPORT: { type: 'port', default: 3000 },\n\n\t// Boolean (true/false/yes/no/1/0/on/off)\n\tDEBUG: { type: 'boolean', default: false },\n\n\t// Array (split by separator)\n\tALLOWED: { type: 'array', separator: ',', default: [] },\n\n\t// JSON (parsed to object)\n\tCONFIG: { type: 'json', default: {} },\n\n\t// URL (validated)\n\tAPI_URL: { type: 'url', required: true },\n\n\t// Enum (must match one of values)\n\tLOG_LEVEL: { type: 'enum', values: ['debug','info','warn','error'], default: 'info' }\n})",
514
+ "tips": [
515
+ "If a required field is missing and has no default, env.load() throws immediately with all validation errors.",
516
+ "Defaults can be functions: { default: () => crypto.randomUUID() } — they're called lazily.",
517
+ "All type coercion happens at load() time — after that, env.PORT returns a real number, not a string."
518
+ ]
519
+ }
520
+ ]
521
+ },
522
+ {
523
+ "section": "ORM",
524
+ "icon": "database",
525
+ "items": [
526
+ {
527
+ "name": "Database",
528
+ "description": "The ORM entry point. Connect to a database using one of 6 built-in adapters (memory, json, sqlite, mysql, postgres, mongo), register your Model classes, and sync schemas. The Database instance manages the connection lifecycle and provides transaction support. All network-facing adapters (mysql, postgres, mongo) validate credentials on connect — invalid host, port, user, or database values throw immediately.",
529
+ "methods": [
530
+ { "method": "connect", "signature": "Database.connect(type, [opts])", "description": "Static factory method. Creates a Database instance with the specified adapter. Validates credentials for network adapters. Returns the Database instance." },
531
+ { "method": "register", "signature": "db.register(ModelClass)", "description": "Register a Model class with this database. Chainable." },
532
+ { "method": "registerAll", "signature": "db.registerAll(...models)", "description": "Register multiple Model classes. Chainable." },
533
+ { "method": "sync", "signature": "db.sync()", "description": "Create tables for all registered models. Returns Promise." },
534
+ { "method": "drop", "signature": "db.drop()", "description": "Drop tables for all registered models. Returns Promise." },
535
+ { "method": "close", "signature": "db.close()", "description": "Close the database connection. Returns Promise." },
536
+ { "method": "model", "signature": "db.model(name)", "description": "Get a registered model by class name." },
537
+ { "method": "transaction", "signature": "db.transaction(fn)", "description": "Run an async function inside a transaction. Auto-commits on success, rolls back on error. Falls back to direct execution if the adapter doesn't support transactions." }
538
+ ],
539
+ "options": [
540
+ { "option": "memory", "type": "adapter", "default": "—", "notes": "In-memory adapter. No options required. Great for testing and prototyping." },
541
+ { "option": "json", "type": "adapter", "default": "—", "notes": "File-based JSON adapter. Options: { path: 'data.json' }." },
542
+ { "option": "sqlite", "type": "adapter", "default": "—", "notes": "SQLite adapter. Options: { filename, readonly, fileMustExist, createDir, pragmas }. Requires better-sqlite3. Auto-creates parent directories." },
543
+ { "option": "mysql", "type": "adapter", "default": "—", "notes": "MySQL adapter. Options: { host, user, password, database, port }. Requires mysql2. Credentials validated on connect." },
544
+ { "option": "postgres", "type": "adapter", "default": "—", "notes": "PostgreSQL adapter. Options: { host, user, password, database, port, ssl }. Requires pg. Credentials validated on connect." },
545
+ { "option": "mongo", "type": "adapter", "default": "—", "notes": "MongoDB adapter. Options: { url, database, clientOptions }. Requires mongodb. URL and database validated on connect." }
546
+ ],
547
+ "example": "const { Database, Model, TYPES } = require('zero-http')\nconst path = require('path')\n\n// SQLite with file-based persistence (recommended for single-server apps)\nconst db = Database.connect('sqlite', {\n\tfilename: path.join(__dirname, 'Database', 'app.db'),\n\t// createDir: true (default) — auto-creates the Database/ folder\n\t// pragmas are production-tuned by default:\n\t// journal_mode: WAL, foreign_keys: ON, busy_timeout: 5000,\n\t// synchronous: NORMAL, cache_size: -64000 (64 MB),\n\t// temp_store: MEMORY, mmap_size: 268435456 (256 MB)\n})\n\n// Override pragmas as needed\nconst testDb = Database.connect('sqlite', {\n\tfilename: ':memory:',\n\tpragmas: { cache_size: '-32000', wal_autocheckpoint: '500' }\n})\n\n// Network adapters validate credentials\n// Database.connect('mysql', { host: 123 }) → throws: host must be a non-empty string\n// Database.connect('postgres', { port: 99999 }) → throws: port must be 1-65535\n\nclass User extends Model {\n\tstatic table = 'users'\n\tstatic schema = {\n\t\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\t\tname: { type: TYPES.STRING, required: true }\n\t}\n}\n\ndb.register(User)\nawait db.sync()\n\n// Transaction — auto rollback on error\nawait db.transaction(async () => {\n\tconst sender = await User.findById(1)\n\tconst receiver = await User.findById(2)\n\tawait sender.decrement('balance', 100)\n\tawait receiver.increment('balance', 100)\n})",
548
+ "tips": [
549
+ "Use 'memory' for tests and prototyping — zero setup, instant startup.",
550
+ "SQLite with WAL mode handles concurrent reads and writes well for single-server apps.",
551
+ "All SQLite pragmas have production-ready defaults — you only need to set filename.",
552
+ "createDir: true (default) auto-creates parent directories for the database file.",
553
+ "Network adapter credentials (host, port, user, password, database) are type-checked on connect.",
554
+ "db.transaction() wraps your callback in begin/commit/rollback when the adapter supports it.",
555
+ "db.register() returns the Database instance — you can chain: db.register(User).register(Post).",
556
+ "Always call db.sync() after registering models to create the database tables."
557
+ ]
558
+ },
559
+ {
560
+ "name": "SQLite Adapter",
561
+ "description": "The SQLite adapter uses better-sqlite3 for synchronous, high-performance file-based persistence. It auto-creates parent directories, ships with production-tuned PRAGMA defaults (WAL, 64 MB cache, memory-mapped I/O), and exposes utility methods for database maintenance. Ideal for single-server apps, prototyping, and embedded use cases.",
562
+ "methods": [
563
+ { "method": "pragma", "signature": "adapter.pragma(key)", "description": "Read a single PRAGMA value (e.g. 'journal_mode' → 'wal')." },
564
+ { "method": "checkpoint", "signature": "adapter.checkpoint([mode])", "description": "Force a WAL checkpoint. Modes: PASSIVE (default), FULL, RESTART, TRUNCATE." },
565
+ { "method": "integrity", "signature": "adapter.integrity()", "description": "Run PRAGMA integrity_check. Returns 'ok' or a problem description." },
566
+ { "method": "vacuum", "signature": "adapter.vacuum()", "description": "Rebuild the database file, reclaiming unused pages." },
567
+ { "method": "fileSize", "signature": "adapter.fileSize()", "description": "Get the database file size in bytes. Returns 0 for in-memory databases." },
568
+ { "method": "tables", "signature": "adapter.tables()", "description": "List all user-created table names." },
569
+ { "method": "raw", "signature": "adapter.raw(sql, ...params)", "description": "Execute a raw SQL SELECT query with parameters." },
570
+ { "method": "close", "signature": "adapter.close()", "description": "Close the database connection." }
571
+ ],
572
+ "options": [
573
+ { "option": "filename", "type": "string", "default": "':memory:'", "notes": "Path to the SQLite file, or ':memory:' for an in-memory database." },
574
+ { "option": "readonly", "type": "boolean", "default": "false", "notes": "Open the database in read-only mode." },
575
+ { "option": "fileMustExist", "type": "boolean", "default": "false", "notes": "Throw if the database file does not exist." },
576
+ { "option": "createDir", "type": "boolean", "default": "true", "notes": "Auto-create parent directories for the database file." },
577
+ { "option": "pragmas.journal_mode", "type": "string", "default": "'WAL'", "notes": "WAL mode enables concurrent reads/writes. Options: WAL, DELETE, TRUNCATE, MEMORY, OFF." },
578
+ { "option": "pragmas.foreign_keys", "type": "string", "default": "'ON'", "notes": "Enforce foreign key constraints." },
579
+ { "option": "pragmas.busy_timeout", "type": "string", "default": "'5000'", "notes": "Milliseconds to wait when the database is locked." },
580
+ { "option": "pragmas.synchronous", "type": "string", "default": "'NORMAL'", "notes": "Sync mode: OFF, NORMAL, FULL, EXTRA." },
581
+ { "option": "pragmas.cache_size", "type": "string", "default": "'-64000'", "notes": "Page cache size. Negative = KiB (e.g. -64000 = 64 MB)." },
582
+ { "option": "pragmas.temp_store", "type": "string", "default": "'MEMORY'", "notes": "Store temp tables in memory for speed." },
583
+ { "option": "pragmas.mmap_size", "type": "string", "default": "'268435456'", "notes": "Memory-mapped I/O size (256 MB)." },
584
+ { "option": "pragmas.page_size", "type": "string", "default": "—", "notes": "Page size in bytes. Must be set before WAL is enabled." },
585
+ { "option": "pragmas.auto_vacuum", "type": "string", "default": "—", "notes": "Auto-vacuum mode: NONE, FULL, INCREMENTAL." },
586
+ { "option": "pragmas.secure_delete", "type": "string", "default": "—", "notes": "Overwrite deleted content with zeros." },
587
+ { "option": "pragmas.wal_autocheckpoint", "type": "string", "default": "—", "notes": "Number of pages before WAL auto-checkpoint (default 1000)." },
588
+ { "option": "pragmas.locking_mode", "type": "string", "default": "—", "notes": "NORMAL or EXCLUSIVE locking." }
589
+ ],
590
+ "example": "const { Database, Model, TYPES } = require('zero-http')\nconst path = require('path')\n\n// Production SQLite setup with Database/ folder\nconst db = Database.connect('sqlite', {\n\tfilename: path.join(__dirname, 'Database', 'app.db'),\n\t// Pragmas are auto-applied with production defaults\n\t// Override any as needed:\n\tpragmas: {\n\t\twal_autocheckpoint: '500', // checkpoint every 500 pages\n\t\tsecure_delete: 'ON', // zero-fill deleted content\n\t}\n})\n\n// Access adapter utilities\nconst adapter = db.adapter\nconsole.log(adapter.pragma('journal_mode')) // 'wal'\nconsole.log(adapter.tables()) // ['users', 'posts', ...]\nconsole.log(adapter.fileSize()) // 49152 (bytes)\nconsole.log(adapter.integrity()) // 'ok'\n\n// Maintenance\nadapter.checkpoint('TRUNCATE') // reset WAL file\nadapter.vacuum() // reclaim free space\n\n// Read-only replica\nconst replica = Database.connect('sqlite', {\n\tfilename: path.join(__dirname, 'Database', 'app.db'),\n\treadonly: true,\n\tfileMustExist: true\n})",
591
+ "tips": [
592
+ "WAL mode (default) gives you concurrent reads while a single writer commits — ideal for web servers.",
593
+ "cache_size of -64000 means 64 MB of page cache — enough for most applications.",
594
+ "mmap_size of 256 MB uses memory-mapped I/O for fast reads on large databases.",
595
+ "Use checkpoint('TRUNCATE') periodically to reset the WAL file size.",
596
+ "integrity() is useful in health-check endpoints to verify database consistency.",
597
+ "readonly mode is perfect for read replicas — prevents accidental writes.",
598
+ "The adapter auto-creates parent directories — just point filename to 'Database/app.db'."
599
+ ]
600
+ },
601
+ {
602
+ "name": "MySQL Adapter",
603
+ "description": "MySQL / MariaDB adapter using the mysql2 driver with connection pooling, prepared statements, and utility methods for introspection and maintenance. Supports SSL, custom charsets, timezone configuration, and pool health monitoring.",
604
+ "methods": [
605
+ { "method": "raw", "signature": "adapter.raw(sql, ...params)", "description": "Execute a raw SQL SELECT query with parameterized inputs. Returns rows." },
606
+ { "method": "exec", "signature": "adapter.exec(sql, ...params)", "description": "Execute a raw statement (INSERT, UPDATE, DDL). Returns { affectedRows, insertId }." },
607
+ { "method": "tables", "signature": "adapter.tables()", "description": "List all tables in the current database." },
608
+ { "method": "columns", "signature": "adapter.columns(table)", "description": "Get column info for a table (Field, Type, Null, Key, Default, Extra)." },
609
+ { "method": "databaseSize", "signature": "adapter.databaseSize()", "description": "Get total database size in bytes (data + indexes)." },
610
+ { "method": "poolStatus", "signature": "adapter.poolStatus()", "description": "Get pool stats: { total, idle, used, queued }." },
611
+ { "method": "version", "signature": "adapter.version()", "description": "Get MySQL/MariaDB server version string." },
612
+ { "method": "ping", "signature": "adapter.ping()", "description": "Ping the server. Returns true if healthy." },
613
+ { "method": "transaction", "signature": "adapter.transaction(fn)", "description": "Run a function inside a transaction. Receives a connection object. Auto-commits/rollbacks." },
614
+ { "method": "close", "signature": "adapter.close()", "description": "Close the connection pool." }
615
+ ],
616
+ "options": [
617
+ { "option": "host", "type": "string", "default": "'localhost'", "notes": "Server hostname or IP address." },
618
+ { "option": "port", "type": "number", "default": "3306", "notes": "Server port." },
619
+ { "option": "user", "type": "string", "default": "'root'", "notes": "Database user." },
620
+ { "option": "password", "type": "string", "default": "''", "notes": "Database password. Validated as string on connect." },
621
+ { "option": "database", "type": "string", "default": "—", "notes": "Database name (required). Validated as non-empty string." },
622
+ { "option": "connectionLimit", "type": "number", "default": "10", "notes": "Maximum concurrent connections in the pool." },
623
+ { "option": "waitForConnections", "type": "boolean", "default": "true", "notes": "Queue requests when all connections are busy." },
624
+ { "option": "queueLimit", "type": "number", "default": "0", "notes": "Max queued requests (0 = unlimited)." },
625
+ { "option": "connectTimeout", "type": "number", "default": "10000", "notes": "Connection timeout in milliseconds." },
626
+ { "option": "charset", "type": "string", "default": "'utf8mb4'", "notes": "Default character set (utf8mb4 for full emoji support)." },
627
+ { "option": "timezone", "type": "string", "default": "'Z'", "notes": "Session timezone." },
628
+ { "option": "ssl", "type": "string|object", "default": "—", "notes": "SSL profile name or TLS options object." },
629
+ { "option": "multipleStatements", "type": "boolean", "default": "false", "notes": "Allow multiple statements per query (use with caution)." },
630
+ { "option": "decimalNumbers", "type": "boolean", "default": "false", "notes": "Return DECIMAL columns as JavaScript numbers instead of strings." }
631
+ ],
632
+ "example": "const { Database, Model, TYPES } = require('zero-http')\n\nconst db = Database.connect('mysql', {\n\thost: process.env.DB_HOST || 'localhost',\n\tport: Number(process.env.DB_PORT) || 3306,\n\tuser: process.env.DB_USER || 'root',\n\tpassword: process.env.DB_PASS || '',\n\tdatabase: process.env.DB_NAME,\n\tconnectionLimit: 20,\n\tcharset: 'utf8mb4',\n})\n\n// Health check endpoint\napp.get('/health', async (req, res) => {\n\tconst alive = await db.adapter.ping()\n\tconst pool = db.adapter.poolStatus()\n\tconst version = await db.adapter.version()\n\tres.json({ alive, pool, version })\n})\n\n// Introspection\nconst tables = await db.adapter.tables() // ['users', 'posts', ...]\nconst cols = await db.adapter.columns('users') // [{ Field, Type, ... }]\nconst size = await db.adapter.databaseSize() // bytes\n\n// Raw queries with parameterized inputs (safe from injection)\nconst users = await db.adapter.raw(\n\t'SELECT * FROM users WHERE role = ? AND active = ?',\n\t'admin', true\n)\n\n// DDL / write operations\nawait db.adapter.exec(\n\t'ALTER TABLE users ADD COLUMN bio TEXT'\n)",
633
+ "tips": [
634
+ "Always use environment variables for credentials — never hardcode passwords!",
635
+ "connectionLimit of 10-20 is a safe default. Go higher only if you're seeing pool exhaustion.",
636
+ "poolStatus() is perfect for monitoring dashboards — track idle vs used connections.",
637
+ "Use charset: 'utf8mb4' (default) to support emojis and full Unicode.",
638
+ "ping() is ideal for health check endpoints in load balancers.",
639
+ "exec() is for writes that don't return rows — raw() is for SELECTs.",
640
+ "Credentials are validated on Database.connect() — bad host/port/user types throw immediately."
641
+ ]
642
+ },
643
+ {
644
+ "name": "PostgreSQL Adapter",
645
+ "description": "PostgreSQL adapter using the pg driver with connection pooling, $1/$2 parameterized queries, JSONB support, and utility methods for schema introspection, pool monitoring, and real-time LISTEN/NOTIFY subscriptions.",
646
+ "methods": [
647
+ { "method": "raw", "signature": "adapter.raw(sql, ...params)", "description": "Execute a raw SQL SELECT query with $1-style params. Returns rows." },
648
+ { "method": "exec", "signature": "adapter.exec(sql, ...params)", "description": "Execute a raw statement that doesn't return rows. Returns { rowCount }." },
649
+ { "method": "tables", "signature": "adapter.tables([schema])", "description": "List all tables in a schema (default: 'public')." },
650
+ { "method": "columns", "signature": "adapter.columns(table, [schema])", "description": "Get column info: column_name, data_type, is_nullable, column_default." },
651
+ { "method": "databaseSize", "signature": "adapter.databaseSize()", "description": "Get total database size in bytes." },
652
+ { "method": "tableSize", "signature": "adapter.tableSize(table)", "description": "Get total size of a table including indexes, in bytes." },
653
+ { "method": "poolStatus", "signature": "adapter.poolStatus()", "description": "Get pool stats: { total, idle, waiting }." },
654
+ { "method": "version", "signature": "adapter.version()", "description": "Get PostgreSQL server version string." },
655
+ { "method": "ping", "signature": "adapter.ping()", "description": "Ping the server. Returns true if healthy." },
656
+ { "method": "listen", "signature": "adapter.listen(channel, callback)", "description": "Subscribe to PG LISTEN/NOTIFY. Returns an unlisten function." },
657
+ { "method": "transaction", "signature": "adapter.transaction(fn)", "description": "Run a function inside a transaction. Receives a client. Auto-commits/rollbacks." },
658
+ { "method": "close", "signature": "adapter.close()", "description": "Close the connection pool." }
659
+ ],
660
+ "options": [
661
+ { "option": "host", "type": "string", "default": "'localhost'", "notes": "Server hostname." },
662
+ { "option": "port", "type": "number", "default": "5432", "notes": "Server port." },
663
+ { "option": "user", "type": "string", "default": "—", "notes": "Database user. Validated on connect." },
664
+ { "option": "password", "type": "string", "default": "—", "notes": "Database password. Validated on connect." },
665
+ { "option": "database", "type": "string", "default": "—", "notes": "Database name (required). Validated as non-empty." },
666
+ { "option": "connectionString", "type": "string", "default": "—", "notes": "Full connection URI (overrides individual host/port/user/password)." },
667
+ { "option": "max", "type": "number", "default": "10", "notes": "Maximum pool size." },
668
+ { "option": "idleTimeoutMillis", "type": "number", "default": "10000", "notes": "How long idle clients sit before being closed." },
669
+ { "option": "connectionTimeoutMillis", "type": "number", "default": "0", "notes": "Timeout for establishing new connections (0 = no limit)." },
670
+ { "option": "ssl", "type": "boolean|object", "default": "false", "notes": "Enable SSL or pass TLS options ({ rejectUnauthorized: false })." },
671
+ { "option": "application_name", "type": "string", "default": "—", "notes": "Shows up in pg_stat_activity — great for identifying your app." },
672
+ { "option": "statement_timeout", "type": "number", "default": "—", "notes": "Auto-cancel queries running longer than this (ms)." }
673
+ ],
674
+ "example": "const { Database } = require('zero-http')\n\n// Connection string style\nconst db = Database.connect('postgres', {\n\tconnectionString: process.env.DATABASE_URL,\n\tssl: { rejectUnauthorized: false }, // common on Heroku/Render\n\tmax: 20,\n\tapplication_name: 'my-api',\n\tstatement_timeout: 30000,\n})\n\n// Or individual options\nconst db2 = Database.connect('postgres', {\n\thost: 'localhost',\n\tport: 5432,\n\tuser: 'postgres',\n\tpassword: process.env.PG_PASS,\n\tdatabase: 'myapp',\n})\n\n// Health + monitoring\napp.get('/health', async (req, res) => {\n\tconst alive = await db.adapter.ping()\n\tconst pool = db.adapter.poolStatus() // { total, idle, waiting }\n\tconst dbSize = await db.adapter.databaseSize()\n\tres.json({ alive, pool, dbSize })\n})\n\n// Schema introspection\nconst tables = await db.adapter.tables() // ['users', 'posts']\nconst cols = await db.adapter.columns('users') // [{ column_name, data_type, ... }]\nconst size = await db.adapter.tableSize('users') // bytes\n\n// Real-time LISTEN/NOTIFY\nconst unlisten = await db.adapter.listen('new_order', (msg) => {\n\tconsole.log('New order:', msg.payload)\n})\n// Later: await unlisten()\n\n// Raw parameterized query ($1, $2 — safe from injection)\nconst admins = await db.adapter.raw(\n\t'SELECT * FROM users WHERE role = $1 AND active = $2',\n\t'admin', true\n)",
675
+ "tips": [
676
+ "Use connectionString for hosted databases (Heroku, Render, Supabase, Neon).",
677
+ "application_name shows up in pg_stat_activity — makes debugging production queries a breeze.",
678
+ "statement_timeout prevents runaway queries from hogging connections.",
679
+ "LISTEN/NOTIFY is PostgreSQL's built-in pub/sub — great for real-time features without polling.",
680
+ "PostgreSQL uses JSONB natively — the ORM maps TYPES.JSON to JSONB for indexable JSON columns.",
681
+ "poolStatus().waiting > 0 means clients are queued — consider increasing max pool size.",
682
+ "SERIAL PRIMARY KEY is auto-used for integer autoIncrement columns."
683
+ ]
684
+ },
685
+ {
686
+ "name": "MongoDB Adapter",
687
+ "description": "MongoDB adapter using the official mongodb driver with connection pooling, automatic reconnection, index management, and utility methods for collection introspection and database stats. Maps the ORM's relational model to MongoDB documents with auto-increment IDs.",
688
+ "methods": [
689
+ { "method": "raw", "signature": "adapter.raw(command)", "description": "Run a raw MongoDB command document. Returns the command result." },
690
+ { "method": "collections", "signature": "adapter.collections()", "description": "List all collections in the database." },
691
+ { "method": "stats", "signature": "adapter.stats()", "description": "Get database stats: { collections, objects, dataSize, storageSize, indexes, indexSize }." },
692
+ { "method": "collectionStats", "signature": "adapter.collectionStats(name)", "description": "Get stats for a specific collection: { count, size, avgObjSize, storageSize, nindexes }." },
693
+ { "method": "createIndex", "signature": "adapter.createIndex(collection, keys, [opts])", "description": "Create an index. keys: { email: 1 } for ascending. opts: { unique: true }." },
694
+ { "method": "indexes", "signature": "adapter.indexes(collection)", "description": "List all indexes on a collection." },
695
+ { "method": "dropIndex", "signature": "adapter.dropIndex(collection, indexName)", "description": "Drop a specific index by name." },
696
+ { "method": "ping", "signature": "adapter.ping()", "description": "Ping the MongoDB server. Returns true if healthy." },
697
+ { "method": "version", "signature": "adapter.version()", "description": "Get the MongoDB server version." },
698
+ { "method": "isConnected", "signature": "adapter.isConnected", "description": "Property — true if currently connected." },
699
+ { "method": "transaction", "signature": "adapter.transaction(fn)", "description": "Run operations in a transaction (requires replica set). Receives a session object." },
700
+ { "method": "close", "signature": "adapter.close()", "description": "Close the connection." }
701
+ ],
702
+ "options": [
703
+ { "option": "url", "type": "string", "default": "'mongodb://127.0.0.1:27017'", "notes": "MongoDB connection string. Supports SRV: mongodb+srv://..." },
704
+ { "option": "database", "type": "string", "default": "—", "notes": "Database name (required). Validated on connect." },
705
+ { "option": "maxPoolSize", "type": "number", "default": "10", "notes": "Maximum number of connections in the pool." },
706
+ { "option": "minPoolSize", "type": "number", "default": "0", "notes": "Keep this many connections warm." },
707
+ { "option": "connectTimeoutMS", "type": "number", "default": "10000", "notes": "Connection timeout." },
708
+ { "option": "socketTimeoutMS", "type": "number", "default": "0", "notes": "Socket timeout (0 = no limit)." },
709
+ { "option": "serverSelectionTimeoutMS", "type": "number", "default": "30000", "notes": "Timeout for selecting a server from a replica set." },
710
+ { "option": "retryWrites", "type": "boolean", "default": "true", "notes": "Retry write operations on transient errors." },
711
+ { "option": "retryReads", "type": "boolean", "default": "true", "notes": "Retry read operations on transient errors." },
712
+ { "option": "authSource", "type": "string", "default": "—", "notes": "Authentication database (default: the database name)." },
713
+ { "option": "replicaSet", "type": "string", "default": "—", "notes": "Replica set name (required for transactions)." },
714
+ { "option": "clientOptions", "type": "object", "default": "{}", "notes": "Extra MongoClient options passed directly to the driver." }
715
+ ],
716
+ "example": "const { Database, Model, TYPES } = require('zero-http')\n\nconst db = Database.connect('mongo', {\n\turl: process.env.MONGO_URI || 'mongodb://localhost:27017',\n\tdatabase: 'myapp',\n\tmaxPoolSize: 20,\n})\n\n// Health check\napp.get('/health', async (req, res) => {\n\tconst alive = await db.adapter.ping()\n\tconst ver = await db.adapter.version()\n\tconst s = await db.adapter.stats()\n\tres.json({ alive, version: ver, ...s })\n})\n\n// Introspection\nconst cols = await db.adapter.collections() // ['users', 'posts']\nconst s = await db.adapter.collectionStats('users')\n// { count: 1500, size: 245760, avgObjSize: 163, ... }\n\n// Index management\nawait db.adapter.createIndex('users', { email: 1 }, { unique: true })\nawait db.adapter.createIndex('posts', { title: 'text' }) // full-text index\nconst idxs = await db.adapter.indexes('users')\n\n// Transactions (replica set required)\nawait db.transaction(async (session) => {\n\t// Use session for operations that need ACID\n})\n\n// Connection status\nconsole.log(db.adapter.isConnected) // true",
717
+ "tips": [
718
+ "Use mongodb+srv:// connection strings for Atlas — handles DNS seedlists automatically.",
719
+ "The ORM simulates auto-increment IDs for MongoDB — each insert finds the max(id) + 1.",
720
+ "Transactions require a replica set — use 'rs.initiate()' in mongosh for local dev.",
721
+ "createIndex with { unique: true } enforces uniqueness — like SQL UNIQUE constraints.",
722
+ "Full-text search: createIndex(collection, { field: 'text' }) then query with $text.",
723
+ "isConnected is a getter — it's a property, not a method (no parentheses).",
724
+ "collectionStats().avgObjSize tells you the average document size — useful for capacity planning."
725
+ ]
726
+ },
727
+ {
728
+ "name": "Memory Adapter",
729
+ "description": "Zero-dependency in-memory adapter — perfect for tests, prototyping, and ephemeral applications. All data lives in JavaScript Maps and arrays. Supports full CRUD, query builder, and utility methods for introspection, export/import, and cloning.",
730
+ "methods": [
731
+ { "method": "tables", "signature": "adapter.tables()", "description": "List all registered table names." },
732
+ { "method": "totalRows", "signature": "adapter.totalRows()", "description": "Count all rows across all tables." },
733
+ { "method": "stats", "signature": "adapter.stats()", "description": "Get memory stats: { tables, totalRows, estimatedBytes }." },
734
+ { "method": "toJSON", "signature": "adapter.toJSON()", "description": "Export all data as a plain object: { tableName: rows[], ... }." },
735
+ { "method": "fromJSON", "signature": "adapter.fromJSON(data)", "description": "Import data from a plain object. Merges with existing data." },
736
+ { "method": "clone", "signature": "adapter.clone()", "description": "Deep-copy the entire database state into a new MemoryAdapter." },
737
+ { "method": "clear", "signature": "adapter.clear()", "description": "Delete all rows from all tables (tables remain registered)." }
738
+ ],
739
+ "example": "const { Database, Model, TYPES } = require('zero-http')\n\nconst db = Database.connect('memory')\n\nclass User extends Model {\n\tstatic table = 'users'\n\tstatic schema = {\n\t\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\t\tname: { type: TYPES.STRING, required: true },\n\t}\n}\n\ndb.register(User)\nawait db.sync()\n\n// Seed data for tests\nawait User.createMany([\n\t{ name: 'Alice' },\n\t{ name: 'Bob' },\n\t{ name: 'Charlie' },\n])\n\n// Introspection\nconsole.log(db.adapter.tables()) // ['users']\nconsole.log(db.adapter.totalRows()) // 3\nconsole.log(db.adapter.stats()) // { tables: 1, totalRows: 3, estimatedBytes: ... }\n\n// Snapshot & restore\nconst snapshot = db.adapter.toJSON()\n// ... run tests that mutate data ...\nawait db.adapter.clear()\ndb.adapter.fromJSON(snapshot) // restore!\n\n// Independent clone for parallel testing\nconst fork = db.adapter.clone()\n// fork has its own isolated copy of all data",
740
+ "tips": [
741
+ "Memory adapter is the fastest — zero IO overhead, instant operations.",
742
+ "Use toJSON() + fromJSON() to snapshot and restore state between tests.",
743
+ "clone() creates a fully independent copy — mutations in one don't affect the other.",
744
+ "clear() keeps table registrations but empties all rows — great for beforeEach() in tests.",
745
+ "stats().estimatedBytes gives a rough size estimate — useful for memory-conscious apps.",
746
+ "Data doesn't survive process restarts. For persistence, switch to JSON or SQLite."
747
+ ]
748
+ },
749
+ {
750
+ "name": "JSON Adapter",
751
+ "description": "File-backed database adapter that persists data as JSON files on disk — one file per table. Zero-dependency, extends the Memory adapter with atomic writes, auto-flushing, backup support, and file management. Great for prototyping, small apps, and embedded scenarios.",
752
+ "methods": [
753
+ { "method": "flush", "signature": "adapter.flush()", "description": "Immediately write all pending changes to disk." },
754
+ { "method": "fileSize", "signature": "adapter.fileSize()", "description": "Get total size of all JSON files in bytes." },
755
+ { "method": "compact", "signature": "adapter.compact(table)", "description": "Re-serialize and save a table's JSON file." },
756
+ { "method": "backup", "signature": "adapter.backup(destDir)", "description": "Copy all JSON files to a target directory." },
757
+ { "method": "directory", "signature": "adapter.directory", "description": "Property — the resolved path where JSON files are stored." },
758
+ { "method": "hasPendingWrites", "signature": "adapter.hasPendingWrites", "description": "Property — true if there are unflushed writes." },
759
+ { "method": "tables", "signature": "adapter.tables()", "description": "List all registered table names (inherited from Memory)." },
760
+ { "method": "stats", "signature": "adapter.stats()", "description": "Get stats: { tables, totalRows, estimatedBytes } (inherited from Memory)." },
761
+ { "method": "toJSON", "signature": "adapter.toJSON()", "description": "Export all data (inherited from Memory)." }
762
+ ],
763
+ "options": [
764
+ { "option": "dir", "type": "string", "default": "—", "notes": "Directory to store JSON files (required). Created automatically if needed." },
765
+ { "option": "pretty", "type": "boolean", "default": "true", "notes": "Pretty-print JSON with 2-space indentation." },
766
+ { "option": "flushInterval", "type": "number", "default": "50", "notes": "Debounce interval for writes in milliseconds." },
767
+ { "option": "autoFlush", "type": "boolean", "default": "true", "notes": "Auto-write to disk. Set false for manual flush() control." }
768
+ ],
769
+ "example": "const { Database, Model, TYPES } = require('zero-http')\nconst path = require('path')\n\nconst db = Database.connect('json', {\n\tdir: path.join(__dirname, 'data'),\n\tpretty: true, // human-readable files\n\tflushInterval: 100, // debounce writes to 100ms\n})\n\nclass Note extends Model {\n\tstatic table = 'notes'\n\tstatic schema = {\n\t\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\t\ttitle: { type: TYPES.STRING, required: true },\n\t\tbody: { type: TYPES.TEXT },\n\t}\n\tstatic timestamps = true\n}\n\ndb.register(Note)\nawait db.sync()\n\n// CRUD works exactly like any other adapter\nawait Note.create({ title: 'Hello', body: 'World' })\n// Creates data/notes.json on disk\n\n// Backup before deployment\ndb.adapter.backup(path.join(__dirname, 'backups', Date.now().toString()))\n\n// Monitor disk usage\nconsole.log(db.adapter.fileSize()) // total bytes on disk\nconsole.log(db.adapter.directory) // '/path/to/data'\n\n// Force immediate flush (useful before process.exit)\nawait db.adapter.flush()",
770
+ "tips": [
771
+ "JSON files are human-readable — you can edit them manually in an emergency.",
772
+ "Writes are atomic (tmp + rename) so you won't get corrupted files on crash.",
773
+ "flushInterval debounces rapid writes — higher values = fewer disk writes.",
774
+ "backup() is great for creating snapshots before risky operations.",
775
+ "Set autoFlush: false and call flush() manually for batch-heavy operations.",
776
+ "JSON adapter inherits ALL Memory adapter utilities (tables, stats, toJSON, fromJSON, clone).",
777
+ "For more performance, switch to SQLite — it handles concurrent access much better."
778
+ ]
779
+ },
780
+ {
781
+ "name": "Model",
782
+ "description": "The ORM base class — extend it to define your data models. Supports typed schemas with validation, timestamps, soft deletes, lifecycle hooks, hidden fields, reusable scopes, relationships (hasMany, hasOne, belongsTo, belongsToMany), and a full suite of CRUD operations.",
783
+ "methods": [
784
+ { "method": "create", "signature": "Model.create(data)", "description": "Insert a new record. Runs validation and beforeCreate/afterCreate hooks. Returns Promise<Model>." },
785
+ { "method": "createMany", "signature": "Model.createMany([data, ...])", "description": "Insert multiple records. Returns Promise<Model[]>." },
786
+ { "method": "find", "signature": "Model.find([conditions])", "description": "Find all records matching conditions. Returns Promise<Model[]>." },
787
+ { "method": "findOne", "signature": "Model.findOne(conditions)", "description": "Find a single record. Returns Promise<Model|null>." },
788
+ { "method": "findById", "signature": "Model.findById(id)", "description": "Find by primary key. Returns Promise<Model|null>." },
789
+ { "method": "findOrCreate", "signature": "Model.findOrCreate(conditions, [defaults])", "description": "Find or insert. Returns Promise<{ instance, created }>." },
790
+ { "method": "exists", "signature": "Model.exists([conditions])", "description": "Check if any matching records exist. Returns Promise<boolean>." },
791
+ { "method": "upsert", "signature": "Model.upsert(conditions, data)", "description": "Insert or update. Finds by conditions, creates with merged data if not found, updates if found. Returns Promise<{ instance, created }>." },
792
+ { "method": "updateWhere", "signature": "Model.updateWhere(conditions, data)", "description": "Update all matching records. Returns Promise<number> (affected count)." },
793
+ { "method": "deleteWhere", "signature": "Model.deleteWhere(conditions)", "description": "Delete all matching records (respects softDelete). Returns Promise<number>." },
794
+ { "method": "count", "signature": "Model.count([conditions])", "description": "Count matching records. Returns Promise<number>." },
795
+ { "method": "query", "signature": "Model.query()", "description": "Start a fluent Query builder. See ORM → Query." },
796
+ { "method": "scope", "signature": "Model.scope(name, [...args])", "description": "Start a query with a named scope applied. Returns Query." },
797
+ { "method": "save", "signature": "instance.save()", "description": "Insert (if new) or update dirty fields (if persisted). Returns Promise<Model>." },
798
+ { "method": "update", "signature": "instance.update(data)", "description": "Update specific fields on the instance. Returns Promise<Model>." },
799
+ { "method": "delete", "signature": "instance.delete()", "description": "Delete the instance (soft or hard depending on softDelete setting)." },
800
+ { "method": "restore", "signature": "instance.restore()", "description": "Restore a soft-deleted instance (sets deletedAt to null)." },
801
+ { "method": "reload", "signature": "instance.reload()", "description": "Re-fetch the instance from the database. Returns Promise<Model>." },
802
+ { "method": "toJSON", "signature": "instance.toJSON()", "description": "Return a plain object, excluding fields listed in static hidden." },
803
+ { "method": "load", "signature": "instance.load(relationName)", "description": "Eagerly load a relationship. Sets instance[relationName]. Returns Promise." },
804
+ { "method": "increment", "signature": "instance.increment(field, [by])", "description": "Increment a numeric field by amount (default 1). Saves immediately." },
805
+ { "method": "decrement", "signature": "instance.decrement(field, [by])", "description": "Decrement a numeric field by amount (default 1). Saves immediately." },
806
+ { "method": "hasMany", "signature": "Model.hasMany(Related, foreignKey, [localKey])", "description": "Define a one-to-many relationship." },
807
+ { "method": "hasOne", "signature": "Model.hasOne(Related, foreignKey, [localKey])", "description": "Define a one-to-one relationship." },
808
+ { "method": "belongsTo", "signature": "Model.belongsTo(Related, foreignKey, [otherKey])", "description": "Define an inverse belongs-to relationship." },
809
+ { "method": "belongsToMany", "signature": "Model.belongsToMany(Related, opts)", "description": "Define a many-to-many relationship through a junction table. Options: { through, foreignKey, otherKey, localKey, relatedKey }." },
810
+ { "method": "first", "signature": "Model.first([conditions])", "description": "Find the first record. Returns Promise<Model|null>." },
811
+ { "method": "last", "signature": "Model.last([conditions])", "description": "Find the last record (by PK descending). Returns Promise<Model|null>." },
812
+ { "method": "all", "signature": "Model.all([conditions])", "description": "Get all records (alias for find). Returns Promise<Model[]>." },
813
+ { "method": "paginate", "signature": "Model.paginate(page, [perPage], [conditions])", "description": "Rich pagination: returns { data, total, page, perPage, pages, hasNext, hasPrev }." },
814
+ { "method": "chunk", "signature": "Model.chunk(size, fn, [conditions])", "description": "Process all records in batches. fn(batch, batchIndex) — supports async." },
815
+ { "method": "random", "signature": "Model.random([conditions])", "description": "Get a random record. Returns Promise<Model|null>." },
816
+ { "method": "pluck", "signature": "Model.pluck(field, [conditions])", "description": "Pluck values for a single column. Returns Promise<Array>." }
817
+ ],
818
+ "options": [
819
+ { "option": "table", "type": "string", "default": "—", "notes": "Database table name (required)." },
820
+ { "option": "schema", "type": "object", "default": "—", "notes": "Column definitions with type, required, default, unique, primaryKey, autoIncrement, enum, min, max, minLength, maxLength, match, guarded." },
821
+ { "option": "timestamps", "type": "boolean", "default": "false", "notes": "Auto-manage createdAt and updatedAt columns." },
822
+ { "option": "softDelete", "type": "boolean", "default": "false", "notes": "Mark records as deleted (deletedAt) instead of removing them. Use .restore() to undelete." },
823
+ { "option": "hooks", "type": "object", "default": "{}", "notes": "Lifecycle hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete." },
824
+ { "option": "hidden", "type": "string[]", "default": "[]", "notes": "Fields excluded from toJSON() output. Use for passwords, tokens, internal IDs." },
825
+ { "option": "scopes", "type": "object", "default": "{}", "notes": "Named reusable query conditions. Each scope is a function: (query, ...args) => query.where(...)." }
826
+ ],
827
+ "example": "const { Model, TYPES } = require('zero-http')\n\nclass User extends Model {\n\tstatic table = 'users'\n\tstatic timestamps = true\n\tstatic softDelete = true\n\tstatic hidden = ['password', 'resetToken']\n\tstatic scopes = {\n\t\tactive: (q) => q.where('active', true),\n\t\trole: (q, role) => q.where('role', role),\n\t\trecent: (q) => q.orderBy('createdAt', 'desc').limit(10)\n\t}\n\tstatic schema = {\n\t\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\t\tname: { type: TYPES.STRING, required: true, minLength: 2 },\n\t\temail: { type: TYPES.STRING, required: true, unique: true },\n\t\tpassword: { type: TYPES.STRING, guarded: true },\n\t\trole: { type: TYPES.STRING, enum: ['user', 'admin'], default: 'user' },\n\t\tlogins: { type: TYPES.INTEGER, default: 0 },\n\t\tactive: { type: TYPES.BOOLEAN, default: true }\n\t}\n\tstatic hooks = {\n\t\tbeforeCreate: (data) => { data.email = data.email.toLowerCase() }\n\t}\n}\n\n// Static shortcuts — no query builder needed\nconst first = await User.first({ role: 'admin' }) // first admin\nconst last = await User.last() // last user by PK\nconst all = await User.all({ active: true }) // alias for find()\nconst pick = await User.random() // random user\nconst names = await User.pluck('name') // ['Alice','Bob',...]\n\n// Pagination with rich metadata\nconst page = await User.paginate(2, 10, { active: true })\n// { data: [...], total: 42, page: 2, perPage: 10, pages: 5, hasNext: true, hasPrev: true }\n\n// Batch processing — never loads all records at once\nawait User.chunk(100, async (batch, i) => {\n\tconsole.log(`Processing batch ${i}: ${batch.length} users`)\n})\n\n// Scoped queries\nconst activeAdmins = await User.scope('active')\nconst recentAdmins = await User.scope('role', 'admin')\n\n// Upsert — insert or update\nconst { instance, created } = await User.upsert(\n\t{ email: 'alice@example.com' },\n\t{ name: 'Alice', role: 'admin' }\n)\n\n// Increment/decrement\nconst user = await User.findById(1)\nawait user.increment('logins') // logins + 1\nawait user.decrement('logins', 5) // logins - 5\n\n// toJSON respects hidden — no password leak\nconsole.log(user.toJSON())",
828
+ "tips": [
829
+ "Model.first/last/all/random/pluck are shortcuts — they build queries internally so you don't have to.",
830
+ "Model.paginate() returns metadata (total, pages, hasNext, hasPrev) — perfect for REST API pagination endpoints.",
831
+ "Model.chunk() processes large tables in batches — avoids loading millions of records into memory at once.",
832
+ "static hidden = ['password'] prevents accidental password leaks in API responses.",
833
+ "Scopes are the most powerful feature for DRY queries — define once, reuse everywhere.",
834
+ "upsert() is atomic find-or-create-or-update — avoids race conditions in concurrent environments.",
835
+ "increment/decrement save immediately — no need to call instance.save() after.",
836
+ "guarded: true on a schema field prevents it from being set via mass-assignment (create/update with object).",
837
+ "Hooks are great for data normalization (lowercase emails) and audit logging.",
838
+ "belongsToMany requires a junction table name and the foreign keys for both sides."
839
+ ]
840
+ },
841
+ {
842
+ "name": "Query",
843
+ "description": "Fluent query builder returned by Model.query(). All filter/sort/limit methods are chainable. Execute with .exec(), .first(), .count(), .exists(), .pluck(), or aggregates (sum/avg/min/max). Also thenable — you can await the query directly without calling .exec().",
844
+ "methods": [
845
+ { "method": "select", "signature": "select(...fields)", "description": "Select specific columns. Chainable." },
846
+ { "method": "distinct", "signature": "distinct()", "description": "Return unique rows only. Chainable." },
847
+ { "method": "where", "signature": "where(field, [op], value)", "description": "Add a WHERE condition. Supports object form, (field, value), or (field, op, value). Operators: =, !=, >, <, >=, <=, LIKE, IN, NOT IN, BETWEEN, IS NULL, IS NOT NULL." },
848
+ { "method": "orWhere", "signature": "orWhere(field, [op], value)", "description": "Add an OR WHERE condition. Chainable." },
849
+ { "method": "whereNull", "signature": "whereNull(field)", "description": "WHERE field IS NULL. Chainable." },
850
+ { "method": "whereNotNull", "signature": "whereNotNull(field)", "description": "WHERE field IS NOT NULL. Chainable." },
851
+ { "method": "whereIn", "signature": "whereIn(field, values)", "description": "WHERE field IN (...values). Chainable." },
852
+ { "method": "whereNotIn", "signature": "whereNotIn(field, values)", "description": "WHERE field NOT IN (...values). Chainable." },
853
+ { "method": "whereBetween", "signature": "whereBetween(field, low, high)", "description": "WHERE field BETWEEN low AND high. Chainable." },
854
+ { "method": "whereNotBetween", "signature": "whereNotBetween(field, low, high)", "description": "WHERE field NOT BETWEEN low AND high. Chainable." },
855
+ { "method": "whereLike", "signature": "whereLike(field, pattern)", "description": "WHERE field LIKE pattern (% and _ wildcards). Chainable." },
856
+ { "method": "orderBy", "signature": "orderBy(field, [dir])", "description": "Sort results. dir: 'asc' (default) or 'desc'. Chainable." },
857
+ { "method": "limit", "signature": "limit(n)", "description": "Maximum number of results. Chainable." },
858
+ { "method": "offset", "signature": "offset(n)", "description": "Skip n records. Chainable." },
859
+ { "method": "page", "signature": "page(pageNum, [perPage])", "description": "Pagination helper. 1-indexed. perPage defaults to 20. Chainable." },
860
+ { "method": "groupBy", "signature": "groupBy(...fields)", "description": "Group results by columns. Chainable." },
861
+ { "method": "having", "signature": "having(field, [op], value)", "description": "Add a HAVING condition (use with groupBy). Chainable." },
862
+ { "method": "join", "signature": "join(table, localKey, foreignKey)", "description": "INNER JOIN. Chainable." },
863
+ { "method": "leftJoin", "signature": "leftJoin(table, localKey, foreignKey)", "description": "LEFT JOIN. Chainable." },
864
+ { "method": "rightJoin", "signature": "rightJoin(table, localKey, foreignKey)", "description": "RIGHT JOIN. Chainable." },
865
+ { "method": "withDeleted", "signature": "withDeleted()", "description": "Include soft-deleted records in results. Chainable." },
866
+ { "method": "scope", "signature": "scope(name, [...args])", "description": "Apply a named scope from the model's static scopes. Chainable." },
867
+ { "method": "exec", "signature": "exec()", "description": "Execute the query. Returns Promise<Model[]>." },
868
+ { "method": "first", "signature": "first()", "description": "Execute and return the first result. Returns Promise<Model|null>." },
869
+ { "method": "count", "signature": "count()", "description": "Execute and return the count. Returns Promise<number>." },
870
+ { "method": "exists", "signature": "exists()", "description": "Returns Promise<boolean> — true if any matching records exist." },
871
+ { "method": "pluck", "signature": "pluck(field)", "description": "Returns Promise<Array> of values for a single column." },
872
+ { "method": "sum", "signature": "sum(field)", "description": "Returns Promise<number> — sum of a numeric column." },
873
+ { "method": "avg", "signature": "avg(field)", "description": "Returns Promise<number> — average of a numeric column." },
874
+ { "method": "min", "signature": "min(field)", "description": "Returns Promise<*> — minimum value of a column." },
875
+ { "method": "max", "signature": "max(field)", "description": "Returns Promise<*> — maximum value of a column." },
876
+ { "method": "take", "signature": "take(n)", "description": "Alias for limit() — LINQ naming. Chainable." },
877
+ { "method": "skip", "signature": "skip(n)", "description": "Alias for offset() — LINQ naming. Chainable." },
878
+ { "method": "toArray", "signature": "toArray()", "description": "Alias for exec() — returns Promise<Model[]>." },
879
+ { "method": "orderByDesc", "signature": "orderByDesc(field)", "description": "Shorthand for orderBy(field, 'desc'). Chainable." },
880
+ { "method": "last", "signature": "last()", "description": "Execute and return the last result. Returns Promise<Model|null>." },
881
+ { "method": "when", "signature": "when(condition, fn)", "description": "Conditionally apply query logic if condition is truthy. Chainable." },
882
+ { "method": "unless", "signature": "unless(condition, fn)", "description": "Conditionally apply query logic if condition is falsy. Chainable." },
883
+ { "method": "tap", "signature": "tap(fn)", "description": "Inspect the query for debugging without breaking the chain. Chainable." },
884
+ { "method": "chunk", "signature": "chunk(size, fn)", "description": "Process results in batches. Calls fn(batch, batchIndex) for each chunk." },
885
+ { "method": "each", "signature": "each(fn)", "description": "Execute and iterate each result. fn(item, index) — supports async." },
886
+ { "method": "map", "signature": "map(fn)", "description": "Execute, transform each result. Returns Promise<Array>." },
887
+ { "method": "filter", "signature": "filter(fn)", "description": "Execute, post-filter results in JS. Returns Promise<Model[]>." },
888
+ { "method": "reduce", "signature": "reduce(fn, initial)", "description": "Execute and reduce results to a single value." },
889
+ { "method": "paginate", "signature": "paginate(page, [perPage])", "description": "Rich pagination: returns { data, total, page, perPage, pages, hasNext, hasPrev }." },
890
+ { "method": "whereRaw", "signature": "whereRaw(sql, ...params)", "description": "Inject raw SQL WHERE clause (SQL adapters only). Parameterized. Chainable." }
891
+ ],
892
+ "example": "// Filtering with negation\nconst results = await User.query()\n\t.whereNotIn('role', ['banned', 'suspended'])\n\t.whereNotBetween('age', 0, 12)\n\t.whereLike('email', '%@gmail.com')\n\t.orderBy('name')\n\t.exec()\n\n// Scoped queries (chainable)\nconst activeAdmins = await User.query()\n\t.scope('active')\n\t.scope('role', 'admin')\n\t.orderBy('name')\n\t.exec()\n\n// Aggregates\nconst avgAge = await User.query().avg('age')\nconst revenue = await Order.query().where('status', 'paid').sum('total')\nconst maxPrice = await Product.query().max('price')\nconst count = await User.query().where('role', 'admin').count()\n\n// LINQ-style aliases\nconst top5 = await User.query().orderByDesc('score').take(5)\nconst page3 = await User.query().skip(40).take(20)\nconst oldest = await User.query().orderBy('age').last()\n\n// Conditional query building (perfect for API endpoints)\nconst users = await User.query()\n\t.when(req.query.role, q => q.where('role', req.query.role))\n\t.when(req.query.minAge, q => q.where('age', '>=', req.query.minAge))\n\t.unless(req.query.showAll, q => q.limit(50))\n\t.tap(q => console.log('Query:', q.build()))\n\n// Rich pagination with metadata\nconst result = await User.query()\n\t.where('active', true)\n\t.paginate(2, 10)\n// { data: [...], total: 53, page: 2, perPage: 10,\n// pages: 6, hasNext: true, hasPrev: true }\n\n// Batch processing for large datasets\nawait User.query().chunk(100, async (batch, i) => {\n\tconsole.log(`Batch ${i}: ${batch.length} users`)\n\tfor (const u of batch) await u.update({ migrated: true })\n})\n\n// Functional transforms\nconst names = await User.query().map(u => u.name)\nconst admins = await User.query().filter(u => u.role === 'admin')\nconst totalAge = await User.query().reduce((sum, u) => sum + u.age, 0)",
893
+ "tips": [
894
+ "Queries are thenable — 'await User.query().where(...)' works without calling .exec().",
895
+ "take() and skip() are LINQ aliases for limit() and offset() — use whichever feels natural.",
896
+ "when() is a game-changer for API endpoints — conditionally apply filters based on request params.",
897
+ "tap() is perfect for debugging — inspect the query state without breaking the chain.",
898
+ "paginate() returns everything you need for pagination UI: total, pages, hasNext, hasPrev.",
899
+ "chunk() processes large datasets in batches — no memory explosion on million-row tables.",
900
+ "map/filter/reduce work like Array methods but on query results — great for transformations.",
901
+ "last() reverses the first orderBy direction — or defaults to PK DESC if no order is set.",
902
+ "whereRaw() lets power users inject raw SQL — always parameterized for safety.",
903
+ "Scopes chain: .scope('active').scope('role', 'admin') applies both conditions."
904
+ ]
905
+ },
906
+ {
907
+ "name": "TYPES",
908
+ "description": "Column type constants for defining model schemas. Each type maps to the appropriate native type in the target database adapter.",
909
+ "options": [
910
+ { "option": "STRING", "type": "const", "default": "'string'", "notes": "Variable-length string (VARCHAR)." },
911
+ { "option": "INTEGER", "type": "const", "default": "'integer'", "notes": "Whole number (INT)." },
912
+ { "option": "FLOAT", "type": "const", "default": "'float'", "notes": "Floating-point number (REAL/DOUBLE)." },
913
+ { "option": "BOOLEAN", "type": "const", "default": "'boolean'", "notes": "True/false (BOOLEAN/TINYINT)." },
914
+ { "option": "DATE", "type": "const", "default": "'date'", "notes": "Date without time." },
915
+ { "option": "DATETIME", "type": "const", "default": "'datetime'", "notes": "Date with time (TIMESTAMP)." },
916
+ { "option": "JSON", "type": "const", "default": "'json'", "notes": "JSON column (auto-serialize/deserialize)." },
917
+ { "option": "TEXT", "type": "const", "default": "'text'", "notes": "Large text field (TEXT/CLOB)." },
918
+ { "option": "BLOB", "type": "const", "default": "'blob'", "notes": "Binary data (BLOB/BYTEA)." },
919
+ { "option": "UUID", "type": "const", "default": "'uuid'", "notes": "UUID string. Can auto-generate with default: () => crypto.randomUUID()." }
920
+ ],
921
+ "example": "const { TYPES } = require('zero-http')\n\nstatic schema = {\n\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\tuuid: { type: TYPES.UUID, default: () => crypto.randomUUID() },\n\tname: { type: TYPES.STRING, required: true, minLength: 2, maxLength: 100 },\n\tbio: { type: TYPES.TEXT },\n\tage: { type: TYPES.INTEGER, min: 0, max: 150 },\n\tscore: { type: TYPES.FLOAT, default: 0.0 },\n\tactive: { type: TYPES.BOOLEAN, default: true },\n\tbirthday: { type: TYPES.DATE },\n\tloginAt: { type: TYPES.DATETIME },\n\tsettings: { type: TYPES.JSON, default: {} },\n\tavatar: { type: TYPES.BLOB }\n}"
922
+ }
923
+ ]
924
+ },
925
+ {
926
+ "section": "Real-Time",
927
+ "icon": "zap",
928
+ "items": [
929
+ {
930
+ "name": "WebSocket",
931
+ "description": "Built-in RFC 6455 WebSocket server. Register handlers with app.ws(path, handler). Each connection receives a WebSocketConnection instance with send/receive, ping/pong, binary support, and event emitter methods. Configure max payload size, ping intervals, and client verification.",
932
+ "methods": [
933
+ { "method": "send", "signature": "ws.send(data)", "description": "Send a text or binary message." },
934
+ { "method": "sendJSON", "signature": "ws.sendJSON(obj)", "description": "Send a JSON-serialized message." },
935
+ { "method": "ping", "signature": "ws.ping([data])", "description": "Send a ping frame." },
936
+ { "method": "pong", "signature": "ws.pong([data])", "description": "Send a pong frame." },
937
+ { "method": "close", "signature": "ws.close([code], [reason])", "description": "Graceful close with optional status code and reason." },
938
+ { "method": "terminate", "signature": "ws.terminate()", "description": "Forcefully close the connection." },
939
+ { "method": "on", "signature": "ws.on(event, handler)", "description": "Listen for events: message, close, error, ping, pong, drain." },
940
+ { "method": "once", "signature": "ws.once(event, handler)", "description": "Listen for an event once." },
941
+ { "method": "off", "signature": "ws.off(event, handler)", "description": "Remove an event listener." }
942
+ ],
943
+ "options": [
944
+ { "option": "maxPayload", "type": "number", "default": "1048576 (1MB)", "notes": "Maximum incoming message size in bytes." },
945
+ { "option": "pingInterval", "type": "number", "default": "30000", "notes": "Automatic ping interval in ms (0 to disable)." },
946
+ { "option": "verifyClient", "type": "function", "default": "—", "notes": "Verification function: (req) => boolean. Return false to reject." }
947
+ ],
948
+ "example": "const { createApp } = require('zero-http')\nconst app = createApp()\n\nconst clients = new Set()\n\napp.ws('/chat', {\n\tmaxPayload: 64 * 1024,\n\tpingInterval: 20000\n}, (ws, req) => {\n\tws.data.name = req.query.name || 'anon'\n\tclients.add(ws)\n\tws.send('Welcome, ' + ws.data.name + '!')\n\n\tws.on('message', msg => {\n\t\tfor (const c of clients) {\n\t\t\tif (c !== ws && c.readyState === 1)\n\t\t\t\tc.send(ws.data.name + ': ' + msg)\n\t\t}\n\t})\n\n\tws.on('close', () => clients.delete(ws))\n})\n\napp.listen(3000)",
949
+ "tips": [
950
+ "ws.data is a plain object for storing per-connection user data (name, auth, etc.).",
951
+ "Check ws.readyState === 1 (OPEN) before sending to avoid errors.",
952
+ "Use verifyClient to enforce authentication before accepting the upgrade.",
953
+ "Properties: id, readyState, protocol, extensions, headers, ip, query, url, secure, connectedAt, uptime, bufferedAmount."
954
+ ]
955
+ },
956
+ {
957
+ "name": "WebSocketPool",
958
+ "description": "Connection and room manager for WebSocket apps. Automatically tracks connections, manages room membership, supports broadcast/targeted messaging, and cleans up on disconnect.",
959
+ "methods": [
960
+ { "method": "add", "signature": "pool.add(ws)", "description": "Add a connection to the pool. Auto-removed on close." },
961
+ { "method": "remove", "signature": "pool.remove(ws)", "description": "Remove a connection from the pool and all rooms." },
962
+ { "method": "join", "signature": "pool.join(ws, room)", "description": "Join a connection to a named room." },
963
+ { "method": "leave", "signature": "pool.leave(ws, room)", "description": "Leave a room." },
964
+ { "method": "broadcast", "signature": "pool.broadcast(msg, [exclude])", "description": "Send to all connections." },
965
+ { "method": "broadcastJSON", "signature": "pool.broadcastJSON(obj, [exclude])", "description": "Send JSON to all connections." },
966
+ { "method": "toRoom", "signature": "pool.toRoom(room, msg, [exclude])", "description": "Send to all in a room." },
967
+ { "method": "toRoomJSON", "signature": "pool.toRoomJSON(room, obj, [exclude])", "description": "Send JSON to all in a room." },
968
+ { "method": "in", "signature": "pool.in(room)", "description": "Get all connections in a room." },
969
+ { "method": "roomsOf", "signature": "pool.roomsOf(ws)", "description": "Get all rooms a connection is in." },
970
+ { "method": "closeAll", "signature": "pool.closeAll()", "description": "Close all connections." },
971
+ { "method": "size", "signature": "pool.size", "description": "Total connection count." },
972
+ { "method": "clients", "signature": "pool.clients", "description": "All connections as an iterable." },
973
+ { "method": "rooms", "signature": "pool.rooms", "description": "Array of all room names." },
974
+ { "method": "roomSize", "signature": "pool.roomSize(room)", "description": "Number of connections in a room." }
975
+ ],
976
+ "example": "const { createApp, WebSocketPool } = require('zero-http')\nconst app = createApp()\nconst pool = new WebSocketPool()\n\napp.ws('/chat', (ws, req) => {\n\tconst room = req.query.room || 'general'\n\tpool.add(ws) // auto-removed on close\n\tpool.join(ws, room)\n\n\tws.data.name = req.query.name || 'anon'\n\tpool.toRoomJSON(room, { type: 'join', user: ws.data.name }, ws)\n\n\tws.on('message', msg => {\n\t\tpool.toRoom(room, ws.data.name + ': ' + msg, ws)\n\t})\n\n\tws.on('close', () => {\n\t\tpool.toRoomJSON(room, { type: 'leave', user: ws.data.name })\n\t})\n})\n\napp.get('/pool/status', (req, res) => res.json({\n\tconnections: pool.size,\n\trooms: pool.rooms,\n\troomSizes: pool.rooms.reduce((o, r) => (o[r] = pool.roomSize(r), o), {})\n}))"
977
+ },
978
+ {
979
+ "name": "SSE (Server-Sent Events)",
980
+ "description": "Push real-time events to browser clients via res.sse(). Returns an SSEStream instance with auto-IDs, named events, keep-alive pings, and graceful disconnect handling. The browser connects with new EventSource(url).",
981
+ "methods": [
982
+ { "method": "send", "signature": "sse.send(data)", "description": "Send a data-only event." },
983
+ { "method": "sendJSON", "signature": "sse.sendJSON(obj)", "description": "Send a JSON-serialized event." },
984
+ { "method": "event", "signature": "sse.event(name, data)", "description": "Send a named event. Browser listens with es.addEventListener(name, ...)." },
985
+ { "method": "comment", "signature": "sse.comment(text)", "description": "Send a comment line (not received by EventSource)." },
986
+ { "method": "retry", "signature": "sse.retry(ms)", "description": "Set the reconnection interval for the client." },
987
+ { "method": "keepAlive", "signature": "sse.keepAlive(ms)", "description": "Start sending periodic comment pings." },
988
+ { "method": "flush", "signature": "sse.flush()", "description": "Flush the response stream." },
989
+ { "method": "close", "signature": "sse.close()", "description": "Close the SSE stream." },
990
+ { "method": "on", "signature": "sse.on(event, handler)", "description": "Listen for events: close, error." }
991
+ ],
992
+ "options": [
993
+ { "option": "status", "type": "number", "default": "200", "notes": "HTTP status code." },
994
+ { "option": "retry", "type": "number", "default": "—", "notes": "Initial reconnection interval in ms." },
995
+ { "option": "keepAlive", "type": "number", "default": "0", "notes": "Keep-alive ping interval in ms (0 = disabled)." },
996
+ { "option": "keepAliveComment", "type": "string", "default": "'ping'", "notes": "Comment text for keep-alive pings." },
997
+ { "option": "autoId", "type": "boolean", "default": "false", "notes": "Auto-generate incrementing event IDs." },
998
+ { "option": "startId", "type": "number", "default": "1", "notes": "Starting ID for autoId." },
999
+ { "option": "pad", "type": "number", "default": "0", "notes": "Pad the response with whitespace for proxy compatibility." },
1000
+ { "option": "headers", "type": "object", "default": "{}", "notes": "Extra response headers." }
1001
+ ],
1002
+ "example": "const { createApp } = require('zero-http')\nconst app = createApp()\n\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({\n\t\tretry: 5000,\n\t\tautoId: true,\n\t\tkeepAlive: 30000\n\t})\n\n\tsse.send('connected')\n\n\tconst iv = setInterval(() => {\n\t\tsse.event('tick', { time: Date.now() })\n\t}, 1000)\n\n\tsse.on('close', () => {\n\t\tclearInterval(iv)\n\t\tconsole.log('client disconnected')\n\t})\n})\n\n// Browser:\n// const es = new EventSource('/events')\n// es.addEventListener('tick', e => console.log(JSON.parse(e.data)))",
1003
+ "tips": [
1004
+ "SSE is automatically excluded from response compression.",
1005
+ "Use keepAlive to prevent proxy/load-balancer timeouts on idle connections.",
1006
+ "Properties: connected, eventCount, bytesSent, connectedAt, uptime, lastEventId, data (user store)."
1007
+ ]
1008
+ }
1009
+ ]
1010
+ },
1011
+ {
1012
+ "section": "Networking",
1013
+ "icon": "globe",
1014
+ "items": [
1015
+ {
1016
+ "name": "fetch",
1017
+ "description": "Built-in HTTP/HTTPS client for server-side requests. Returns a Response-like object with ok, status, json(), text(), arrayBuffer(). Supports timeouts, abort signals, upload/download progress tracking, automatic JSON body serialization, and custom TLS options for HTTPS URLs.",
1018
+ "methods": [
1019
+ { "method": "ok", "signature": "res.ok", "description": "true if status is 200-299." },
1020
+ { "method": "status", "signature": "res.status", "description": "HTTP status code." },
1021
+ { "method": "statusText", "signature": "res.statusText", "description": "HTTP status text." },
1022
+ { "method": "secure", "signature": "res.secure", "description": "true if the request was over HTTPS." },
1023
+ { "method": "url", "signature": "res.url", "description": "Final URL after redirects." },
1024
+ { "method": "headers.get", "signature": "res.headers.get(name)", "description": "Get a response header value." },
1025
+ { "method": "text", "signature": "res.text()", "description": "Read body as text. Returns Promise<string>." },
1026
+ { "method": "json", "signature": "res.json()", "description": "Read body as parsed JSON. Returns Promise<any>." },
1027
+ { "method": "arrayBuffer", "signature": "res.arrayBuffer()", "description": "Read body as ArrayBuffer. Returns Promise<ArrayBuffer>." }
1028
+ ],
1029
+ "options": [
1030
+ { "option": "method", "type": "string", "default": "'GET'", "notes": "HTTP method." },
1031
+ { "option": "headers", "type": "object", "default": "{}", "notes": "Request headers." },
1032
+ { "option": "body", "type": "string | Buffer | object", "default": "—", "notes": "Request body. Objects are auto-serialized to JSON." },
1033
+ { "option": "timeout", "type": "number", "default": "—", "notes": "Request timeout in milliseconds." },
1034
+ { "option": "signal", "type": "AbortSignal", "default": "—", "notes": "AbortController signal for cancellation." },
1035
+ { "option": "onDownloadProgress", "type": "function", "default": "—", "notes": "Progress callback: ({ loaded, total }) => {}." },
1036
+ { "option": "onUploadProgress", "type": "function", "default": "—", "notes": "Upload progress callback." },
1037
+ { "option": "rejectUnauthorized", "type": "boolean", "default": "true", "notes": "TLS: reject self-signed certs." },
1038
+ { "option": "ca", "type": "string | Buffer", "default": "—", "notes": "TLS: custom CA certificate." },
1039
+ { "option": "cert", "type": "string | Buffer", "default": "—", "notes": "TLS: client certificate." },
1040
+ { "option": "key", "type": "string | Buffer", "default": "—", "notes": "TLS: client private key." }
1041
+ ],
1042
+ "example": "const { fetch } = require('zero-http')\n\nasync function download(url) {\n\tconst controller = new AbortController()\n\tsetTimeout(() => controller.abort(), 10000)\n\n\tconst res = await fetch(url, {\n\t\ttimeout: 5000,\n\t\tsignal: controller.signal,\n\t\trejectUnauthorized: false, // accept self-signed certs\n\t\tonDownloadProgress: ({ loaded, total }) =>\n\t\t\tconsole.log(`${loaded}/${total || '?'} bytes`)\n\t})\n\n\tconsole.log(res.ok, res.status, res.secure, res.url)\n\tconst data = await res.json()\n\treturn data\n}\n\ndownload('https://jsonplaceholder.typicode.com/todos/1')\n\t.then(console.log)",
1043
+ "tips": [
1044
+ "Object bodies are auto-serialized to JSON and Content-Type is set automatically.",
1045
+ "Use rejectUnauthorized: false only for development — never in production.",
1046
+ "Timeout and AbortSignal can be used together — whichever fires first cancels the request."
1047
+ ]
1048
+ },
1049
+ {
1050
+ "name": "Error Classes",
1051
+ "description": "Built-in HTTP error classes with status codes, machine-readable codes, and optional details. Every error extends HttpError which carries statusCode, code, and details. Throw them in route handlers — the router catches and sends the correct HTTP response automatically.",
1052
+ "methods": [
1053
+ { "method": "HttpError", "signature": "new HttpError(statusCode, [message], [opts])", "description": "Base class. opts: { code, details }." },
1054
+ { "method": "BadRequestError", "signature": "new BadRequestError([message], [opts])", "description": "400 Bad Request." },
1055
+ { "method": "UnauthorizedError", "signature": "new UnauthorizedError([message], [opts])", "description": "401 Unauthorized." },
1056
+ { "method": "ForbiddenError", "signature": "new ForbiddenError([message], [opts])", "description": "403 Forbidden." },
1057
+ { "method": "NotFoundError", "signature": "new NotFoundError([message], [opts])", "description": "404 Not Found." },
1058
+ { "method": "MethodNotAllowedError", "signature": "new MethodNotAllowedError([message], [opts])", "description": "405 Method Not Allowed." },
1059
+ { "method": "ConflictError", "signature": "new ConflictError([message], [opts])", "description": "409 Conflict." },
1060
+ { "method": "GoneError", "signature": "new GoneError([message], [opts])", "description": "410 Gone." },
1061
+ { "method": "PayloadTooLargeError", "signature": "new PayloadTooLargeError([message], [opts])", "description": "413 Payload Too Large." },
1062
+ { "method": "UnprocessableEntityError", "signature": "new UnprocessableEntityError([message], [opts])", "description": "422 Unprocessable Entity." },
1063
+ { "method": "ValidationError", "signature": "new ValidationError([message], [errors], [opts])", "description": "422 with field-level errors. errors: object or array stored in .errors and .details." },
1064
+ { "method": "TooManyRequestsError", "signature": "new TooManyRequestsError([message], [opts])", "description": "429 Too Many Requests." },
1065
+ { "method": "InternalError", "signature": "new InternalError([message], [opts])", "description": "500 Internal Server Error." },
1066
+ { "method": "NotImplementedError", "signature": "new NotImplementedError([message], [opts])", "description": "501 Not Implemented." },
1067
+ { "method": "BadGatewayError", "signature": "new BadGatewayError([message], [opts])", "description": "502 Bad Gateway." },
1068
+ { "method": "ServiceUnavailableError", "signature": "new ServiceUnavailableError([message], [opts])", "description": "503 Service Unavailable." },
1069
+ { "method": "createError", "signature": "createError(statusCode, [message], [opts])", "description": "Factory — creates the correct error class for any status code." },
1070
+ { "method": "isHttpError", "signature": "isHttpError(err)", "description": "Returns true if err is an HttpError or has a statusCode. Type guard." },
1071
+ { "method": "toJSON", "signature": "err.toJSON()", "description": "Serialize: { error, code, statusCode, details? }." }
1072
+ ],
1073
+ "example": "const { NotFoundError, ValidationError, createError, isHttpError } = require('zero-http')\n\n// Throw in route handlers — automatically caught!\napp.get('/users/:id', async (req, res) => {\n\tconst user = await User.findById(req.params.id)\n\tif (!user) throw new NotFoundError('User not found')\n\tres.json(user)\n})\n\n// Validation with field-level errors\napp.post('/users', async (req, res) => {\n\tconst errors = {}\n\tif (!req.body.email) errors.email = 'required'\n\tif (!req.body.name) errors.name = 'required'\n\tif (Object.keys(errors).length > 0) {\n\t\tthrow new ValidationError('Invalid input', errors)\n\t\t// Response: { error: 'Invalid input', code: 'VALIDATION_FAILED',\n\t\t// statusCode: 422, details: { email: 'required', name: 'required' } }\n\t}\n\tres.json(await User.create(req.body))\n})\n\n// Factory — create by status code\nthrow createError(409, 'Duplicate entry', { details: { id: 42 } })\n\n// Custom error code\nthrow new HttpError(503, 'Database offline', { code: 'DB_DOWN' })\n\n// Type checking in error handlers\napp.onError((err, req, res, next) => {\n\tif (isHttpError(err)) {\n\t\tres.status(err.statusCode).json(err.toJSON())\n\t} else {\n\t\tres.status(500).json({ error: 'Unexpected error' })\n\t}\n})",
1074
+ "tips": [
1075
+ "Just throw — the router wraps all handlers in try/catch and catches async rejections too.",
1076
+ "Every error auto-generates a code from the status text: 404 → NOT_FOUND, 422 → UNPROCESSABLE_ENTITY.",
1077
+ "ValidationError.errors stores field-level details — perfect for form validation feedback.",
1078
+ "createError(statusCode) returns the correct typed class: createError(404) returns NotFoundError.",
1079
+ "Use opts.details to attach any structured data (IDs, field names, debug info) to the error.",
1080
+ "isHttpError() works as a TypeScript type guard — narrows the type to HttpError."
1081
+ ]
1082
+ },
1083
+ {
1084
+ "name": "errorHandler",
1085
+ "description": "Configurable error-handling middleware. Formats error responses based on environment (dev vs production), includes stack traces in dev, hides internal details in production, and supports custom formatters and logging. Use with app.onError().",
1086
+ "methods": [],
1087
+ "options": [
1088
+ { "option": "stack", "type": "boolean", "default": "true (non-production)", "notes": "Include stack traces in responses. Auto-detected from NODE_ENV." },
1089
+ { "option": "log", "type": "boolean", "default": "true", "notes": "Log errors to console." },
1090
+ { "option": "logger", "type": "function", "default": "console.error", "notes": "Custom log function." },
1091
+ { "option": "formatter", "type": "function", "default": "—", "notes": "Custom response formatter: (err, req, isDev) => responseBody." },
1092
+ { "option": "onError", "type": "function", "default": "—", "notes": "Callback on every error: (err, req, res) => void." }
1093
+ ],
1094
+ "example": "const { createApp, errorHandler } = require('zero-http')\nconst app = createApp()\n\n// Basic — dev mode with stack traces\napp.onError(errorHandler())\n\n// Production — hide internals\napp.onError(errorHandler({ stack: false }))\n\n// Custom formatter\napp.onError(errorHandler({\n\tformatter: (err, req, isDev) => ({\n\t\tsuccess: false,\n\t\tmessage: err.message,\n\t\t...(isDev && { stack: err.stack })\n\t})\n}))\n\n// Error monitoring callback\napp.onError(errorHandler({\n\tonError: (err, req) => {\n\t\t// Send to Sentry, DataDog, etc.\n\t\terrorTracker.capture(err, { url: req.url, method: req.method })\n\t}\n}))",
1095
+ "tips": [
1096
+ "In production (NODE_ENV=production), 5xx errors show 'Internal Server Error' instead of the real message.",
1097
+ "4xx errors always show the real message — they're client errors, not secrets.",
1098
+ "The formatter option gives you full control over the response shape for your API style.",
1099
+ "onError is perfect for error monitoring (Sentry, DataDog) without changing response format.",
1100
+ "Headers-already-sent errors are silently skipped — no double-response crashes."
1101
+ ]
1102
+ },
1103
+ {
1104
+ "name": "debug",
1105
+ "description": "Lightweight namespaced debug logger with levels, colors, and timestamps. Enable via DEBUG env var or programmatically. Each namespace gets a unique color. Supports text and structured JSON output.",
1106
+ "methods": [
1107
+ { "method": "debug(namespace)", "signature": "debug('app:routes')", "description": "Create a namespaced logger. Returns a function with level methods." },
1108
+ { "method": "log()", "signature": "log(...args)", "description": "Log at debug level (default call). Supports %s, %d, %j format specifiers." },
1109
+ { "method": "log.trace()", "signature": "log.trace(...args)", "description": "Log at trace level (most verbose)." },
1110
+ { "method": "log.info()", "signature": "log.info(...args)", "description": "Log at info level." },
1111
+ { "method": "log.warn()", "signature": "log.warn(...args)", "description": "Log at warn level." },
1112
+ { "method": "log.error()", "signature": "log.error(...args)", "description": "Log at error level." },
1113
+ { "method": "log.fatal()", "signature": "log.fatal(...args)", "description": "Log at fatal level (most severe)." },
1114
+ { "method": "debug.level()", "signature": "debug.level('info')", "description": "Set minimum log level globally. Levels: trace, debug, info, warn, error, fatal, silent." },
1115
+ { "method": "debug.enable()", "signature": "debug.enable('app:*')", "description": "Enable namespaces by pattern. Same syntax as DEBUG env var." },
1116
+ { "method": "debug.disable()", "signature": "debug.disable()", "description": "Disable all debug output." },
1117
+ { "method": "debug.json()", "signature": "debug.json(true)", "description": "Enable structured JSON output (for log aggregators)." },
1118
+ { "method": "debug.timestamps()", "signature": "debug.timestamps(false)", "description": "Toggle timestamps." },
1119
+ { "method": "debug.colors()", "signature": "debug.colors(false)", "description": "Toggle ANSI colors." },
1120
+ { "method": "debug.output()", "signature": "debug.output(stream)", "description": "Set custom output stream (default: stderr)." },
1121
+ { "method": "debug.reset()", "signature": "debug.reset()", "description": "Reset all settings to defaults." }
1122
+ ],
1123
+ "options": [
1124
+ { "option": "DEBUG", "type": "env var", "default": "—", "notes": "Comma-separated namespace patterns. Supports * glob and -prefix to exclude." },
1125
+ { "option": "DEBUG_LEVEL", "type": "env var", "default": "debug", "notes": "Minimum log level: trace, debug, info, warn, error, fatal, silent." }
1126
+ ],
1127
+ "example": "const { debug } = require('zero-http')\n\nconst log = debug('app:routes')\nconst dbLog = debug('db:queries')\n\n// Log at different levels\nlog.info('server started on port %d', 3000)\nlog.warn('deprecated endpoint hit: %s', req.url)\nlog.error('request failed', err)\nlog.trace('entering handler for %s %s', req.method, req.url)\n\n// Conditional logging\nif (log.enabled) {\n\tlog('expensive debug data: %j', buildDebugPayload())\n}\n\n// JSON mode for production log aggregators\ndebug.json(true)\ndebug.level('info')\n// Output: {\"timestamp\":\"...\",\"level\":\"INFO\",\"namespace\":\"app:routes\",\"message\":\"server started on port 3000\"}\n\n// Enable specific namespaces\ndebug.enable('app:*,db:*') // only app and db\ndebug.enable('*,-db:queries') // everything except db:queries\n\n// Environment variables\n// DEBUG=app:* node server.js\n// DEBUG_LEVEL=warn node server.js",
1128
+ "tips": [
1129
+ "Set DEBUG=* to see all debug output, or DEBUG=app:* to filter by namespace.",
1130
+ "Use debug.json(true) in production for structured JSON logs — pipe to ELK, Datadog, etc.",
1131
+ "Format specifiers: %s (string), %d (number), %j (JSON), %o (object).",
1132
+ "Each namespace gets a unique color — makes it easy to scan logs visually.",
1133
+ "debug.level('warn') filters out trace/debug/info — only show warnings and above.",
1134
+ "log.enabled is a boolean — use it to skip expensive debug payload construction."
1135
+ ]
1136
+ }
1137
+ ]
1138
+ }
1139
+ ]