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
@@ -1,24 +1,33 @@
1
1
  [
2
2
  {
3
3
  "name": "createApp()",
4
- "description": "Returns an application instance with Express-like routing, middleware support, WebSocket handling, error handling, and an HTTP/HTTPS server.",
5
- "example": "const { createApp, json, cors, logger, compress,\n\tstatic: serveStatic } = require('zero-http')\nconst app = createApp()\n\napp.use(logger({ format: 'dev' }))\napp.use(cors())\napp.use(compress())\napp.use(json({ limit: '10kb' }))\napp.use(serveStatic(path.join(__dirname, 'public')))\n\napp.get('/', (req, res) => res.json({ hello: 'world' }))\n\napp.ws('/chat', (ws, req) => {\n\tws.on('message', msg => ws.send('echo: ' + msg))\n})\n\napp.onError((err, req, res, next) => {\n\tconsole.error(err)\n\tres.status(500).json({ error: err.message })\n})\n\n// HTTP\napp.listen(3000, () => console.log('listening on :3000'))\n\n// HTTPS\napp.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') })",
4
+ "description": "Returns an application instance with Express-like routing, middleware support, WebSocket handling, settings management, error handling, and an HTTP/HTTPS server. All HTTP method shortcuts return the App instance for chaining.",
5
+ "example": "const { createApp, json, cors, logger, compress, helmet, timeout,\n\trequestId, cookieParser, static: serveStatic } = require('zero-http')\nconst app = createApp()\n\n// 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 across all requests\napp.locals.appName = 'My API'\n\n// Middleware stack\napp.use(logger({ format: 'dev' }))\napp.use(helmet())\napp.use(cors())\napp.use(compress())\napp.use(timeout(30000))\napp.use(requestId())\napp.use(cookieParser('my-secret'))\napp.use(json({ limit: '10kb' }))\napp.use(serveStatic(path.join(__dirname, 'public')))\n\n// Chained route registration\napp.get('/', (req, res) => res.json({ hello: 'world' }))\n .get('/health', (req, res) => res.json({ status: 'ok' }))\n\n// Route grouping\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 handler\napp.param('id', (req, res, next, value) => {\n\treq.item = items.find(i => i.id === value)\n\tif (!req.item) return res.status(404).json({ error: 'Not found' })\n\tnext()\n})\n\n// WebSocket\napp.ws('/chat', (ws, req) => {\n\tws.on('message', msg => ws.send('echo: ' + msg))\n})\n\n// Error handler\napp.onError((err, req, res, next) => {\n\tconsole.error(err)\n\tres.status(500).json({ error: err.message })\n})\n\n// HTTP\napp.listen(3000, () => console.log('listening on :3000'))\n\n// HTTPS\napp.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') })",
6
6
  "methods": [
7
7
  { "method": "use", "signature": "use(fn)", "description": "Register global middleware; fn(req, res, next)." },
8
8
  { "method": "use", "signature": "use(prefix, fn|router)", "description": "Register path-scoped middleware or mount a Router sub-app. The prefix is stripped from req.url." },
9
- { "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register GET route handlers. Optional opts object for { secure } matching." },
10
- { "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register POST route handlers." },
11
- { "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register PUT route handlers." },
12
- { "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register DELETE route handlers." },
13
- { "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register PATCH route handlers." },
14
- { "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register OPTIONS route handlers." },
15
- { "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register HEAD route handlers." },
16
- { "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register handlers for ALL HTTP methods." },
9
+ { "method": "get", "signature": "get(path, [opts], ...handlers)", "description": "Register GET route handlers. Returns App (chainable). With 1 arg, acts as settings getter — see set()." },
10
+ { "method": "post", "signature": "post(path, [opts], ...handlers)", "description": "Register POST route handlers. Returns App (chainable)." },
11
+ { "method": "put", "signature": "put(path, [opts], ...handlers)", "description": "Register PUT route handlers. Returns App (chainable)." },
12
+ { "method": "delete", "signature": "delete(path, [opts], ...handlers)", "description": "Register DELETE route handlers. Returns App (chainable)." },
13
+ { "method": "patch", "signature": "patch(path, [opts], ...handlers)", "description": "Register PATCH route handlers. Returns App (chainable)." },
14
+ { "method": "options", "signature": "options(path, [opts], ...handlers)", "description": "Register OPTIONS route handlers. Returns App (chainable)." },
15
+ { "method": "head", "signature": "head(path, [opts], ...handlers)", "description": "Register HEAD route handlers. Returns App (chainable)." },
16
+ { "method": "all", "signature": "all(path, [opts], ...handlers)", "description": "Register handlers for ALL HTTP methods. Returns App (chainable)." },
17
+ { "method": "set", "signature": "set(key) | set(key, value)", "description": "Dual-purpose: 1 arg = get setting value, 2 args = set setting value. Setter returns App (chainable)." },
18
+ { "method": "enable", "signature": "enable(key)", "description": "Set a boolean setting to true. Returns App (chainable)." },
19
+ { "method": "disable", "signature": "disable(key)", "description": "Set a boolean setting to false. Returns App (chainable)." },
20
+ { "method": "enabled", "signature": "enabled(key)", "description": "Check if a setting is truthy. Returns boolean." },
21
+ { "method": "disabled", "signature": "disabled(key)", "description": "Check if a setting is falsy. Returns boolean." },
22
+ { "method": "locals", "signature": "(object)", "description": "Shared key-value store merged into every req.locals and res.locals per request." },
23
+ { "method": "param", "signature": "param(name, fn)", "description": "Register a handler for a named route parameter. fn(req, res, next, value). Returns App (chainable)." },
24
+ { "method": "group", "signature": "group(prefix, ...middleware, cb)", "description": "Create a route group under a prefix with shared middleware. Last arg is (router) => {} callback. Returns App." },
25
+ { "method": "chain", "signature": "chain(path)", "description": "Returns a chainable route object with .get(), .post(), etc. for a single path. Alias for router.route()." },
17
26
  { "method": "ws", "signature": "ws(path, [opts], handler)", "description": "Register a WebSocket upgrade handler. handler receives (ws, req)." },
18
27
  { "method": "onError", "signature": "onError(fn)", "description": "Register a global error handler: fn(err, req, res, next). Only one at a time." },
19
- { "method": "listen", "signature": "listen(port, [tlsOpts], [cb])", "description": "Start HTTP server, or HTTPS if tlsOpts includes { key, cert }. Returns the server instance." },
28
+ { "method": "listen", "signature": "listen(port, [tlsOpts], [cb])", "description": "Start HTTP server (default port 3000), or HTTPS if tlsOpts includes { key, cert }. Returns the server instance." },
20
29
  { "method": "close", "signature": "close([cb])", "description": "Gracefully close the server and invoke the optional callback." },
21
- { "method": "routes", "signature": "routes()", "description": "Return a flat array of all registered routes for introspection." },
30
+ { "method": "routes", "signature": "routes()", "description": "Return a flat array of all registered routes (including WS) for introspection." },
22
31
  { "method": "handler", "signature": "(property)", "description": "Bound request handler suitable for http.createServer(app.handler)." }
23
32
  ],
24
33
  "options": []
@@ -44,11 +53,14 @@
44
53
  },
45
54
  {
46
55
  "name": "Request (req)",
47
- "description": "Wraps the incoming Node http.IncomingMessage with convenient properties and helpers. Includes HTTPS detection via req.secure and req.protocol.",
48
- "example": "app.get('/info', (req, res) => {\n\tconsole.log(req.method) // 'GET'\n\tconsole.log(req.url) // '/info?foo=bar'\n\tconsole.log(req.query) // { foo: 'bar' }\n\tconsole.log(req.ip) // '127.0.0.1'\n\tconsole.log(req.secure) // true/false\n\tconsole.log(req.protocol) // 'https' or 'http'\n\tconsole.log(req.get('host')) // 'localhost:3000'\n\tconsole.log(req.is('json')) // false\n\tres.json({ ok: true })\n})",
56
+ "description": "Wraps the incoming Node http.IncomingMessage with convenient properties and helpers. Includes HTTPS detection, cookie access, content negotiation, and Express-compatible properties like originalUrl, baseUrl, hostname, and xhr.",
57
+ "example": "app.get('/info', (req, res) => {\n\tconsole.log(req.method) // 'GET'\n\tconsole.log(req.url) // '/info?foo=bar'\n\tconsole.log(req.originalUrl) // '/api/info?foo=bar' (full original)\n\tconsole.log(req.baseUrl) // '/api' (set by mounted routers)\n\tconsole.log(req.path) // '/info'\n\tconsole.log(req.query) // { foo: 'bar' }\n\tconsole.log(req.params) // { id: '42' } (from route pattern)\n\tconsole.log(req.ip) // '127.0.0.1'\n\tconsole.log(req.secure) // true/false\n\tconsole.log(req.protocol) // 'https' or 'http'\n\tconsole.log(req.hostname) // 'example.com'\n\tconsole.log(req.xhr) // true if X-Requested-With: XMLHttpRequest\n\tconsole.log(req.fresh) // conditional GET freshness\n\tconsole.log(req.stale) // !req.fresh\n\tconsole.log(req.app) // the App instance\n\tconsole.log(req.cookies) // {} (populated by cookieParser)\n\tconsole.log(req.locals) // {} request-scoped store\n\tconsole.log(req.get('host')) // 'localhost:3000'\n\tconsole.log(req.is('json')) // false\n\tconsole.log(req.accepts('json', 'html')) // 'json'\n\tconsole.log(req.subdomains()) // ['api'] for api.example.com\n\tconsole.log(req.range(1000)) // { type: 'bytes', ranges: [{start:0,end:499}] }\n\tres.json({ ok: true })\n})",
49
58
  "methods": [
50
59
  { "method": "method", "signature": "(string)", "description": "HTTP method (GET, POST, etc.)." },
51
- { "method": "url", "signature": "(string)", "description": "Request URL path + query string." },
60
+ { "method": "url", "signature": "(string)", "description": "Request URL path + query string (may be rewritten by mounted routers)." },
61
+ { "method": "originalUrl", "signature": "(string)", "description": "The original request URL before any router rewrites. Never changes." },
62
+ { "method": "baseUrl", "signature": "(string)", "description": "The prefix path where the router was mounted. Empty string for root app." },
63
+ { "method": "path", "signature": "(string)", "description": "URL pathname without query string." },
52
64
  { "method": "headers", "signature": "(object)", "description": "Raw request headers (lowercased keys)." },
53
65
  { "method": "query", "signature": "(object)", "description": "Parsed query string via URLSearchParams." },
54
66
  { "method": "params", "signature": "(object)", "description": "Route parameters populated by the router (e.g. { id: '42' })." },
@@ -56,26 +68,50 @@
56
68
  { "method": "ip", "signature": "(string|null)", "description": "Remote IP address from req.socket.remoteAddress." },
57
69
  { "method": "secure", "signature": "(boolean)", "description": "true when the connection is over TLS (HTTPS)." },
58
70
  { "method": "protocol", "signature": "(string)", "description": "'https' or 'http' based on connection type." },
71
+ { "method": "hostname", "signature": "(string|undefined)", "description": "Hostname from x-forwarded-host or host header, port stripped." },
72
+ { "method": "fresh", "signature": "(boolean)", "description": "true for GET/HEAD when If-None-Match / If-Modified-Since match cached version." },
73
+ { "method": "stale", "signature": "(boolean)", "description": "Inverse of fresh — true when the response needs to be sent in full." },
74
+ { "method": "xhr", "signature": "(boolean)", "description": "true when X-Requested-With header equals 'XMLHttpRequest'." },
75
+ { "method": "cookies", "signature": "(object)", "description": "Parsed cookies (populated by cookieParser middleware)." },
76
+ { "method": "signedCookies", "signature": "(object)", "description": "Verified signed cookies (populated by cookieParser with a secret)." },
77
+ { "method": "locals", "signature": "(object)", "description": "Request-scoped store. Merged with app.locals on each request." },
78
+ { "method": "app", "signature": "(App|null)", "description": "Reference to the App instance handling this request." },
59
79
  { "method": "raw", "signature": "(object)", "description": "The underlying http.IncomingMessage." },
60
80
  { "method": "get", "signature": "get(name)", "description": "Get a request header (case-insensitive)." },
61
- { "method": "is", "signature": "is(type)", "description": "Check if Content-Type contains the given type string (e.g. 'json', 'text/html')." }
81
+ { "method": "is", "signature": "is(type)", "description": "Check if Content-Type contains the given type string (e.g. 'json', 'text/html')." },
82
+ { "method": "accepts", "signature": "accepts(...types)", "description": "Content negotiation — returns the best match from the given types based on Accept header, or false. Supports shorthands: 'json', 'html', 'text', 'xml', 'css', 'js'." },
83
+ { "method": "subdomains", "signature": "subdomains([offset])", "description": "Returns subdomain array. Default offset=2 strips TLD+domain. E.g. 'api.blog.example.com' → ['blog', 'api']." },
84
+ { "method": "range", "signature": "range(size)", "description": "Parse Range header. Returns { type, ranges: [{start, end}] }, -1 (unsatisfiable), or -2 (malformed/missing)." }
62
85
  ],
63
86
  "options": []
64
87
  },
65
88
  {
66
89
  "name": "Response (res)",
67
- "description": "Wraps the outgoing Node http.ServerResponse with chainable helpers for setting status, headers, sending responses, and opening an SSE stream.",
68
- "example": "app.get('/demo', (req, res) => {\n\tres.status(200)\n\t .set('X-Custom', 'hello')\n\t .type('json')\n\t .json({ message: 'ok' })\n})\n\napp.get('/page', (req, res) => res.html('<h1>Hello</h1>'))\napp.get('/go', (req, res) => res.redirect(301, '/new-location'))\n\n// Open an SSE stream\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({ retry: 5000, autoId: true })\n\tsse.send('hello')\n})",
90
+ "description": "Wraps the outgoing Node http.ServerResponse with chainable helpers for setting status, headers, cookies, sending responses, content negotiation, file streaming, and opening an SSE stream.",
91
+ "example": "app.get('/demo', (req, res) => {\n\tres.status(200)\n\t .set('X-Custom', 'hello')\n\t .type('json')\n\t .json({ message: 'ok' })\n})\n\napp.get('/page', (req, res) => res.html('<h1>Hello</h1>'))\napp.get('/go', (req, res) => res.redirect(301, '/new-location'))\n\n// Content negotiation\napp.get('/data', (req, res) => {\n\tres.format({\n\t\t'text/html': () => res.html('<p>data</p>'),\n\t\t'application/json': () => res.json({ data: true }),\n\t\tdefault: () => res.text('data')\n\t})\n})\n\n// Cookies\napp.get('/setcookie', (req, res) => {\n\tres.cookie('session', 'abc123', { maxAge: 3600, secure: true })\n\t .json({ ok: true })\n})\n\n// File download\napp.get('/download', (req, res) => {\n\tres.download('/path/to/report.pdf', 'report.pdf')\n})\n\n// SSE\napp.get('/events', (req, res) => {\n\tconst sse = res.sse({ retry: 5000, autoId: true })\n\tsse.send('hello')\n})",
69
92
  "methods": [
70
93
  { "method": "status", "signature": "status(code)", "description": "Set HTTP status code. Returns this (chainable)." },
71
- { "method": "set", "signature": "set(name, value)", "description": "Set a response header. Returns this (chainable)." },
94
+ { "method": "sendStatus", "signature": "sendStatus(code)", "description": "Set status code and send the status text as the response body (e.g. 404 'Not Found')." },
95
+ { "method": "set", "signature": "set(name, value)", "description": "Set a response header. CRLF injection protection built-in. Returns this (chainable)." },
72
96
  { "method": "get", "signature": "get(name)", "description": "Get a previously-set response header (case-insensitive)." },
97
+ { "method": "append", "signature": "append(name, value)", "description": "Append to a response header (comma-separated). CRLF injection protection. Returns this (chainable)." },
98
+ { "method": "vary", "signature": "vary(field)", "description": "Add a field to the Vary header. Deduplicates and handles '*'. Returns this (chainable)." },
73
99
  { "method": "type", "signature": "type(ct)", "description": "Set Content-Type. Accepts shorthands: 'json', 'html', 'text', 'xml', 'form', 'bin'. Chainable." },
100
+ { "method": "links", "signature": "links(links)", "description": "Set Link header from an object. E.g. links({ next: '/p/2', last: '/p/5' }) → '<\\/p/2>; rel=\"next\", <\\/p/5>; rel=\"last\"'. Chainable." },
101
+ { "method": "location", "signature": "location(url)", "description": "Set the Location header. Returns this (chainable)." },
102
+ { "method": "headersSent", "signature": "(boolean)", "description": "true once headers have been flushed to the client (delegates to raw response)." },
103
+ { "method": "app", "signature": "(App|null)", "description": "Reference to the App instance handling this request." },
104
+ { "method": "locals", "signature": "(object)", "description": "Response-scoped store. Merged with app.locals on each request." },
74
105
  { "method": "send", "signature": "send(body)", "description": "Send the response. Auto-detects Content-Type: Buffer→octet-stream, '<…'→html, string→text, object→json." },
75
106
  { "method": "json", "signature": "json(obj)", "description": "Set Content-Type to application/json and send the object." },
76
107
  { "method": "text", "signature": "text(str)", "description": "Set Content-Type to text/plain and send the string." },
77
108
  { "method": "html", "signature": "html(str)", "description": "Set Content-Type to text/html and send the string." },
78
109
  { "method": "redirect", "signature": "redirect([status], url)", "description": "Redirect to URL. Default status is 302." },
110
+ { "method": "format", "signature": "format(types)", "description": "Content negotiation — takes an object mapping MIME types to handler functions. Supports 'default' key. Returns 406 if no match." },
111
+ { "method": "sendFile", "signature": "sendFile(path, [opts], [cb])", "description": "Stream a file as the response. Auto-detects Content-Type. opts: { root, headers }. Path traversal protection built-in." },
112
+ { "method": "download", "signature": "download(path, [filename], [cb])", "description": "Set Content-Disposition: attachment and stream the file. Optional filename overrides the download name." },
113
+ { "method": "cookie", "signature": "cookie(name, value, [opts])", "description": "Set a cookie. Object values are auto-serialised as JSON cookies (j: prefix). opts: path('/'), httpOnly(true), sameSite('Lax'), domain, maxAge(seconds), expires, secure, signed(false), priority, partitioned. Returns this (chainable)." },
114
+ { "method": "clearCookie", "signature": "clearCookie(name, [opts])", "description": "Clear a cookie by setting expires to epoch. Returns this (chainable)." },
79
115
  { "method": "sse", "signature": "sse([opts])", "description": "Open a Server-Sent Events stream. Returns an SSEStream controller with send(), event(), comment(), close(), etc." }
80
116
  ],
81
117
  "options": []
@@ -105,8 +141,8 @@
105
141
  },
106
142
  {
107
143
  "name": "WebSocketConnection",
108
- "description": "Represents a single WebSocket connection. Created automatically when a client upgrades to WebSocket at a registered ws() path. Provides an event-driven API with per-connection data store, connection metadata, and binary/text messaging.",
109
- "example": "// Properties available on each ws connection:\napp.ws('/demo', (ws, req) => {\n\tconsole.log(ws.id) // 'ws_1_a3x9k'\n\tconsole.log(ws.readyState) // 1 (OPEN)\n\tconsole.log(ws.protocol) // negotiated sub-protocol\n\tconsole.log(ws.headers) // upgrade request headers\n\tconsole.log(ws.ip) // remote IP\n\tconsole.log(ws.query) // parsed query params\n\tconsole.log(ws.url) // upgrade URL\n\tconsole.log(ws.secure) // true for wss://\n\tconsole.log(ws.connectedAt) // timestamp of connection\n\tconsole.log(ws.uptime) // ms since connected\n\tconsole.log(ws.bufferedAmount) // pending bytes\n\n\t// Per-connection data store\n\tws.data.role = 'admin'\n\tif (ws.data.role === 'admin') ws.send('admin tools unlocked')\n})",
144
+ "description": "Represents a single WebSocket connection. Created automatically when a client upgrades to WebSocket at a registered ws() path. Provides an event-driven API with per-connection data store, connection metadata, and binary/text messaging. Exposes static state constants: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3).",
145
+ "example": "// Properties available on each ws connection:\napp.ws('/demo', (ws, req) => {\n\tconsole.log(ws.id) // 'ws_1_a3x9k'\n\tconsole.log(ws.readyState) // 1 (OPEN)\n\tconsole.log(ws.protocol) // negotiated sub-protocol\n\tconsole.log(ws.headers) // upgrade request headers\n\tconsole.log(ws.ip) // remote IP\n\tconsole.log(ws.query) // parsed query params\n\tconsole.log(ws.url) // upgrade URL\n\tconsole.log(ws.secure) // true for wss://\n\tconsole.log(ws.connectedAt) // timestamp of connection\n\tconsole.log(ws.uptime) // ms since connected\n\tconsole.log(ws.bufferedAmount) // pending bytes\n\n\t// Per-connection data store\n\tws.data.role = 'admin'\n\tif (ws.data.role === 'admin') ws.send('admin tools unlocked')\n\n\t// State constants\n\tif (ws.readyState === WebSocketConnection.OPEN) { /* connected */ }\n})",
110
146
  "methods": [
111
147
  { "method": "id", "signature": "(string)", "description": "Unique connection identifier (e.g. ws_1_l8x3k)." },
112
148
  { "method": "readyState", "signature": "(number)", "description": "0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED (mirrors the WebSocket spec)." },
@@ -125,6 +161,29 @@
125
161
  ],
126
162
  "options": []
127
163
  },
164
+ {
165
+ "name": "WebSocketPool",
166
+ "description": "Connection and room manager for WebSocket connections. Provides broadcast, room-based messaging, and connection lifecycle management. Auto-removes connections on close. All mutating methods return the pool for chaining.",
167
+ "example": "const { createApp, WebSocketPool } = require('zero-http')\nconst app = createApp()\nconst pool = new WebSocketPool()\n\napp.ws('/chat', (ws, req) => {\n\tpool.add(ws) // auto-removed on close\n\tpool.join(ws, req.query.room || 'general')\n\n\tws.data.name = req.query.name || 'anon'\n\tpool.toRoomJSON('general', { type: 'join', user: ws.data.name }, ws)\n\n\tws.on('message', msg => {\n\t\tconst room = pool.roomsOf(ws)[0]\n\t\tpool.toRoom(room, ws.data.name + ': ' + msg, ws)\n\t})\n\n\tws.on('close', () => {\n\t\tconsole.log('pool size:', pool.size)\n\t\tconsole.log('rooms:', pool.rooms)\n\t})\n})\n\n// Broadcast to everyone\napp.get('/announce', (req, res) => {\n\tpool.broadcastJSON({ announcement: req.query.msg })\n\tres.json({ sent: pool.size })\n})\n\napp.listen(3000)",
168
+ "methods": [
169
+ { "method": "add", "signature": "add(ws)", "description": "Add a connection to the pool. Auto-removes on close. Returns this." },
170
+ { "method": "remove", "signature": "remove(ws)", "description": "Remove a connection from the pool and all its rooms. Returns this." },
171
+ { "method": "join", "signature": "join(ws, room)", "description": "Join a connection to a named room. Returns this." },
172
+ { "method": "leave", "signature": "leave(ws, room)", "description": "Remove a connection from a room. Cleans up empty rooms. Returns this." },
173
+ { "method": "broadcast", "signature": "broadcast(data, [exclude])", "description": "Send a message to ALL connected clients. Optional exclude connection (e.g. the sender)." },
174
+ { "method": "broadcastJSON", "signature": "broadcastJSON(obj, [exclude])", "description": "JSON-serialize and broadcast to all clients." },
175
+ { "method": "toRoom", "signature": "toRoom(room, data, [exclude])", "description": "Send a message to all connections in a specific room." },
176
+ { "method": "toRoomJSON", "signature": "toRoomJSON(room, obj, [exclude])", "description": "JSON-serialize and send to all connections in a room." },
177
+ { "method": "in", "signature": "in(room)", "description": "Get an array of all connections in a room." },
178
+ { "method": "roomsOf", "signature": "roomsOf(ws)", "description": "Get all room names a connection belongs to." },
179
+ { "method": "size", "signature": "(number)", "description": "Total number of active connections (getter)." },
180
+ { "method": "roomSize", "signature": "roomSize(room)", "description": "Number of connections in a specific room." },
181
+ { "method": "rooms", "signature": "(string[])", "description": "List of all active room names (getter)." },
182
+ { "method": "clients", "signature": "(WebSocketConnection[])", "description": "Array of all active connections (getter)." },
183
+ { "method": "closeAll", "signature": "closeAll([code], [reason])", "description": "Close all connections gracefully. Default code=1001, reason='Server shutdown'." }
184
+ ],
185
+ "options": []
186
+ },
128
187
  {
129
188
  "name": "res.sse([opts]) / SSEStream",
130
189
  "description": "Opens a Server-Sent Events stream from a route handler. Returns an SSEStream controller with methods for sending events, comments, and controlling the stream lifecycle. Supports auto-incrementing IDs, keep-alive, retry hints, and per-stream data store.",
@@ -155,10 +214,263 @@
155
214
  { "option": "headers", "type": "object", "default": "—", "notes": "Additional response headers merged into the SSE response." }
156
215
  ]
157
216
  },
217
+ {
218
+ "name": "helmet([opts])",
219
+ "description": "Security headers middleware that sets sensible defaults for Content-Security-Policy, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and more. All 16 options can be individually overridden or disabled by setting them to false.",
220
+ "example": "const { createApp, helmet } = require('zero-http')\nconst app = createApp()\n\n// Sensible defaults — just use it!\napp.use(helmet())\n\n// Customize specific headers\napp.use(helmet({\n\tcontentSecurityPolicy: {\n\t\tdefaultSrc: [\"'self'\"],\n\t\tscriptSrc: [\"'self'\", 'cdn.example.com'],\n\t\tstyleSrc: [\"'self'\", \"'unsafe-inline'\"]\n\t},\n\tframeguard: 'sameorigin', // allow same-origin iframes\n\thstsMaxAge: 31536000, // 1 year\n\thstsPreload: true, // opt-in to HSTS preload list\n\tcrossOriginEmbedderPolicy: true // enable COEP\n}))\n\n// Disable specific protections\napp.use(helmet({\n\thsts: false, // disable HSTS\n\tcontentSecurityPolicy: false // disable CSP\n}))",
221
+ "methods": [],
222
+ "options": [
223
+ { "option": "contentSecurityPolicy", "type": "object|false", "default": "built-in directives", "notes": "CSP directive object. Keys: defaultSrc, scriptSrc, styleSrc, imgSrc, fontSrc, etc. Set false to disable." },
224
+ { "option": "crossOriginEmbedderPolicy", "type": "boolean", "default": "false", "notes": "Set true to send Cross-Origin-Embedder-Policy: require-corp." },
225
+ { "option": "crossOriginOpenerPolicy", "type": "string|false", "default": "same-origin", "notes": "Cross-Origin-Opener-Policy value. Set false to disable." },
226
+ { "option": "crossOriginResourcePolicy", "type": "string|false", "default": "same-origin", "notes": "Cross-Origin-Resource-Policy value. Set false to disable." },
227
+ { "option": "dnsPrefetchControl", "type": "boolean", "default": "true", "notes": "Set X-DNS-Prefetch-Control: off. Set false to skip." },
228
+ { "option": "frameguard", "type": "string|false", "default": "deny", "notes": "X-Frame-Options value: 'deny' or 'sameorigin'. Set false to disable." },
229
+ { "option": "hidePoweredBy", "type": "boolean", "default": "true", "notes": "Removes the X-Powered-By header. Set false to keep it." },
230
+ { "option": "hsts", "type": "boolean|number", "default": "true", "notes": "Enable Strict-Transport-Security. Set false to disable." },
231
+ { "option": "hstsMaxAge", "type": "number", "default": "15552000 (~180 days)", "notes": "HSTS max-age in seconds." },
232
+ { "option": "hstsIncludeSubDomains", "type": "boolean", "default": "true", "notes": "Include subdomains in HSTS policy." },
233
+ { "option": "hstsPreload", "type": "boolean", "default": "false", "notes": "Add preload flag for HSTS preload list eligibility." },
234
+ { "option": "ieNoOpen", "type": "boolean", "default": "true", "notes": "Set X-Download-Options: noopen. Set false to skip." },
235
+ { "option": "noSniff", "type": "boolean", "default": "true", "notes": "Set X-Content-Type-Options: nosniff. Set false to skip." },
236
+ { "option": "permittedCrossDomainPolicies", "type": "string|false", "default": "none", "notes": "X-Permitted-Cross-Domain-Policies value. Set false to disable." },
237
+ { "option": "referrerPolicy", "type": "string|false", "default": "no-referrer", "notes": "Referrer-Policy value. Set false to disable." },
238
+ { "option": "xssFilter", "type": "boolean", "default": "false", "notes": "Set true to enable X-XSS-Protection: 1; mode=block." }
239
+ ]
240
+ },
241
+ {
242
+ "name": "timeout(ms, [opts])",
243
+ "description": "Request timeout middleware. If the handler doesn't respond within the given time, the request is terminated with a configurable status code and message. Sets req.timedOut boolean for downstream checks.",
244
+ "example": "const { createApp, timeout, json } = require('zero-http')\nconst app = createApp()\n\n// Global 30-second timeout\napp.use(timeout())\n\n// Custom timeout with options\napp.use(timeout(5000, {\n\tstatus: 504,\n\tmessage: 'Gateway Timeout — the upstream took too long'\n}))\n\n// Per-route timeout\napp.post('/slow-task', timeout(60000), json(), async (req, res) => {\n\tconst result = await longRunningTask()\n\tif (req.timedOut) return // guard against sending after timeout\n\tres.json(result)\n})",
245
+ "methods": [],
246
+ "options": [
247
+ { "option": "ms", "type": "number", "default": "30000", "notes": "Timeout duration in milliseconds. Can also be passed as the first argument." },
248
+ { "option": "status", "type": "number", "default": "408", "notes": "HTTP status code sent when the timeout fires." },
249
+ { "option": "message", "type": "string", "default": "Request Timeout", "notes": "Response body text sent on timeout." }
250
+ ]
251
+ },
252
+ {
253
+ "name": "requestId([opts])",
254
+ "description": "Generates a unique request ID for every incoming request. Sets req.id and mirrors it as a response header for request tracing and log correlation. Uses crypto.randomUUID() by default.",
255
+ "example": "const { createApp, requestId, logger } = require('zero-http')\nconst app = createApp()\n\napp.use(requestId())\napp.use(logger({ format: 'dev' }))\n\napp.get('/', (req, res) => {\n\tconsole.log('Request ID:', req.id) // e.g. '550e8400-e29b-41d4-a716-446655440000'\n\tres.json({ requestId: req.id })\n})\n// Response header: X-Request-Id: 550e8400-e29b-41d4-a716-446655440000\n\n// Trust proxy-forwarded IDs\napp.use(requestId({\n\theader: 'X-Trace-Id',\n\ttrustProxy: true\n}))\n\n// Custom generator\napp.use(requestId({\n\tgenerator: () => 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8)\n}))",
256
+ "methods": [],
257
+ "options": [
258
+ { "option": "header", "type": "string", "default": "X-Request-Id", "notes": "Header name used to read (if trustProxy) and write the request ID." },
259
+ { "option": "generator", "type": "function", "default": "crypto.randomUUID()", "notes": "Custom function returning a unique ID string." },
260
+ { "option": "trustProxy", "type": "boolean", "default": "false", "notes": "When true, trusts the incoming header value (max 128 chars) instead of generating a new one." }
261
+ ]
262
+ },
263
+ {
264
+ "name": "cookieParser([secret], [opts])",
265
+ "description": "Parses the Cookie header and populates req.cookies. When a secret is provided, signed cookies (prefixed with 's:') are verified with timing-safe HMAC-SHA256 and placed in req.signedCookies. JSON cookies (prefixed with 'j:') are auto-parsed. Exposes req.secret and req.secrets for downstream middleware. Static helpers: sign, unsign, jsonCookie, parseJSON.",
266
+ "example": "const { createApp, cookieParser } = require('zero-http')\nconst app = createApp()\n\n// With signing + secret rotation\napp.use(cookieParser(['new-secret', 'old-secret']))\n\napp.get('/read', (req, res) => {\n\tconsole.log(req.cookies) // { theme: 'dark' }\n\tconsole.log(req.signedCookies) // { session: 'abc123' } (verified)\n\tconsole.log(req.secret) // 'new-secret' (first secret)\n\tres.json({ cookies: req.cookies, signed: req.signedCookies })\n})\n\napp.get('/set', (req, res) => {\n\t// Auto-sign with signed:true (uses req.secret)\n\tres.cookie('session', 'abc123', { signed: true, httpOnly: true, maxAge: 3600 })\n\t// Auto-serialise object as JSON cookie (j: prefix)\n\t .cookie('prefs', { theme: 'dark', lang: 'en' })\n\t// Priority + Partitioned (CHIPS)\n\t .cookie('tracking', 'x', { priority: 'High', partitioned: true, secure: true, sameSite: 'None' })\n\t .json({ ok: true })\n})\n\n// Static helpers\nconst signed = cookieParser.sign('value', 'secret')\nconst orig = cookieParser.unsign(signed, 'secret') // => 'value'\nconst jval = cookieParser.jsonCookie({ cart: [1,2] }) // => 'j:{\"cart\":[1,2]}'\nconst parsed = cookieParser.parseJSON(jval) // => { cart: [1,2] }",
267
+ "methods": [
268
+ { "method": "cookieParser.sign", "signature": "cookieParser.sign(value, secret)", "description": "Static method. Signs a value with HMAC-SHA256. Returns 's:<value>.<signature>'." },
269
+ { "method": "cookieParser.unsign", "signature": "cookieParser.unsign(value, secret|secrets)", "description": "Static method. Verifies and unsigns a signed cookie value. Secret can be a string or array (rotation). Returns the original value or false." },
270
+ { "method": "cookieParser.jsonCookie", "signature": "cookieParser.jsonCookie(value)", "description": "Static method. Serialises any value as a JSON cookie string prefixed with 'j:'." },
271
+ { "method": "cookieParser.parseJSON", "signature": "cookieParser.parseJSON(str)", "description": "Static method. Parses a 'j:'-prefixed JSON cookie string. Returns parsed value or original string." }
272
+ ],
273
+ "options": [
274
+ { "option": "secret", "type": "string|string[]", "default": "undefined", "notes": "Signing secret(s) for cookie verification. Array enables key rotation — first key signs, all keys verify. Exposed as req.secret and req.secrets." },
275
+ { "option": "decode", "type": "boolean", "default": "true", "notes": "When true, decodeURIComponent is applied to cookie values." }
276
+ ]
277
+ },
278
+ {
279
+ "name": "csrf([opts])",
280
+ "description": "CSRF protection middleware using the double-submit cookie pattern. Generates a cryptographic token on every state-changing request and validates it on subsequent mutations. Token is set as an HttpOnly cookie and must be sent back via header, body field, or query parameter.",
281
+ "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 requests are ignored by default — read the token\napp.get('/form', (req, res) => {\n\tres.json({ csrfToken: req.csrfToken })\n})\n\n// POST/PUT/DELETE must include the token\napp.post('/submit', (req, res) => {\n\t// Token validated automatically by csrf() middleware\n\tres.json({ success: true })\n})\n// Client sends token via:\n// Header: x-csrf-token: <token>\n// Body: { _csrf: '<token>' }\n// Query: ?_csrf=<token>\n\n// Custom options\napp.use(csrf({\n\tcookie: '_xsrf',\n\theader: 'x-xsrf-token',\n\tsaltLength: 24,\n\tignorePaths: ['/api/webhooks'],\n\tonError: (err, req, res) => {\n\t\tres.status(403).json({ error: 'Invalid CSRF token' })\n\t}\n}))",
282
+ "methods": [],
283
+ "options": [
284
+ { "option": "cookie", "type": "string", "default": "_csrf", "notes": "Name of the cookie used to store the CSRF secret." },
285
+ { "option": "header", "type": "string", "default": "x-csrf-token", "notes": "Header name checked for the CSRF token." },
286
+ { "option": "saltLength", "type": "number", "default": "18", "notes": "Length of the random salt used in token generation." },
287
+ { "option": "secret", "type": "string", "default": "auto (32-byte hex)", "notes": "HMAC secret for token signing. Auto-generated per process if not provided." },
288
+ { "option": "ignoreMethods", "type": "string[]", "default": "['GET', 'HEAD', 'OPTIONS']", "notes": "HTTP methods that skip CSRF validation." },
289
+ { "option": "ignorePaths", "type": "string[]", "default": "[]", "notes": "URL paths that skip CSRF validation (e.g. webhook endpoints)." },
290
+ { "option": "onError", "type": "function", "default": "403 JSON response", "notes": "Custom error handler: fn(err, req, res). Called when token is missing or invalid." }
291
+ ]
292
+ },
293
+ {
294
+ "name": "validate(schema, [opts])",
295
+ "description": "Request validation middleware. Validates and coerces req.body, req.query, and req.params against a schema. Supports 11 data types with automatic type coercion, pattern matching, range checks, enum validation, and custom validators. Unknown fields are stripped by default (mass-assignment protection).",
296
+ "example": "const { createApp, json, validate } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\n// Validate request 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: 10 }\n\t}\n}), (req, res) => {\n\t// req.body is sanitized and coerced\n\tres.status(201).json(req.body)\n})\n\n// Validate query params\napp.get('/search', validate({\n\tquery: {\n\t\tq: { type: 'string', required: true, minLength: 1 },\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({ query: req.query })\n})\n\n// Validate route params\napp.get('/users/:id', validate({\n\tparams: { id: { type: 'uuid', required: true } }\n}), (req, res) => {\n\tres.json({ id: req.params.id })\n})\n\n// Custom validator function\napp.post('/register', validate({\n\tbody: {\n\t\tpassword: {\n\t\t\ttype: 'string', required: true, minLength: 8,\n\t\t\tvalidate: (v) => /[A-Z]/.test(v) && /[0-9]/.test(v)\n\t\t\t\t? undefined : 'Must contain uppercase and number'\n\t\t}\n\t}\n}), (req, res) => res.json({ ok: true }))\n\n// Static helpers — validate outside middleware\nconst { value, error } = validate.field('hello@test.com', { type: 'email' }, 'email')\nconst { sanitized, errors } = validate.object(data, schema)",
297
+ "methods": [
298
+ { "method": "validate.field", "signature": "validate.field(value, rule, fieldName)", "description": "Static helper. Validate and coerce a single value. Returns { value, error }." },
299
+ { "method": "validate.object", "signature": "validate.object(data, schema, [opts])", "description": "Static helper. Validate an entire object against a schema. Returns { sanitized, errors }." }
300
+ ],
301
+ "options": [
302
+ { "option": "stripUnknown", "type": "boolean", "default": "true", "notes": "Remove fields not defined in the schema (prevents mass-assignment attacks)." },
303
+ { "option": "onError", "type": "function", "default": "422 JSON response", "notes": "Custom error handler: fn(errors, req, res, next). errors is an array of error strings." }
304
+ ]
305
+ },
306
+ {
307
+ "name": "env",
308
+ "description": "Typed environment configuration. Loads and validates environment variables from .env files with schema enforcement, type coercion, and sensible defaults. Proxy-based — access variables as env.KEY, env('KEY'), or env.get('KEY'). Supports multi-file loading with environment-specific overrides.",
309
+ "example": "const { env } = require('zero-http')\n\n// Simple access (no schema) — reads from process.env\nconsole.log(env.NODE_ENV) // 'development'\nconsole.log(env('PORT')) // '3000' (string from env)\nconsole.log(env.get('API_KEY')) // value or undefined\nconsole.log(env.has('DATABASE_URL')) // true/false\n\n// Load with schema validation and type coercion\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\tSECRET_KEY: { type: 'string', required: true, min: 32 },\n\tMAX_UPLOAD: { type: 'integer', min: 1, max: 100, default: 10 },\n\tAPP_CONFIG: { type: 'json', default: {} }\n})\n\n// After loading, values are typed:\nconsole.log(typeof env.PORT) // 'number' (3000)\nconsole.log(typeof env.DEBUG) // 'boolean' (false)\nconsole.log(Array.isArray(env.ALLOWED_IPS)) // true\n\n// File load order (.env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local)\n// process.env always takes priority\n\n// Require a variable (throws if missing)\nconst dbUrl = env.require('DATABASE_URL')\n\n// Get all loaded values\nconsole.log(env.all())\n\n// Parse a .env string manually\nconst parsed = env.parse('KEY=value\\nFOO=bar')\n\n// Reset loaded state\nenv.reset()",
310
+ "methods": [
311
+ { "method": "env(key)", "signature": "env(key) | env.KEY", "description": "Get an environment variable. Proxy-based — use either function call or property access." },
312
+ { "method": "env.load", "signature": "env.load([schema], [opts])", "description": "Load .env files, apply schema validation & type coercion. opts: { path, override }." },
313
+ { "method": "env.get", "signature": "env.get(key)", "description": "Get a variable from the loaded store or process.env." },
314
+ { "method": "env.require", "signature": "env.require(key)", "description": "Get a variable; throws if missing or empty." },
315
+ { "method": "env.has", "signature": "env.has(key)", "description": "Check if a variable exists. Returns boolean." },
316
+ { "method": "env.all", "signature": "env.all()", "description": "Return a shallow copy of all loaded variables." },
317
+ { "method": "env.reset", "signature": "env.reset()", "description": "Clear the loaded store and schema. Useful for testing." },
318
+ { "method": "env.parse", "signature": "env.parse(src)", "description": "Parse a .env-format string into a key-value object." }
319
+ ],
320
+ "options": [
321
+ { "option": "path", "type": "string", "default": "process.cwd()", "notes": "Directory to look for .env files." },
322
+ { "option": "override", "type": "boolean", "default": "false", "notes": "When true, .env values override existing process.env values." }
323
+ ]
324
+ },
325
+ {
326
+ "name": "Database",
327
+ "description": "Lightweight ORM entry point. Connect to a database, register models, sync schemas, and run queries. Supports 6 adapters: memory (in-process), json (file-based), sqlite, mysql, postgres, and mongo. All CRUD operations are async and adapter-agnostic.",
328
+ "example": "const { Database, Model, TYPES } = require('zero-http')\n\n// Define a model\nclass User extends Model {\n\tstatic table = 'users'\n\tstatic timestamps = true // adds createdAt, updatedAt\n\tstatic softDelete = true // adds deletedAt, enables restore()\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\tage: { type: TYPES.INTEGER, min: 0, max: 150 },\n\t\trole: { type: TYPES.STRING, enum: ['user', 'admin'], default: 'user' },\n\t\tmeta: { type: TYPES.JSON, default: {} }\n\t}\n\tstatic hooks = {\n\t\tbeforeCreate: (data) => { data.email = data.email.toLowerCase() },\n\t\tafterCreate: (instance) => console.log('Created:', instance.id)\n\t}\n}\n\n// Connect and sync\nconst db = Database.connect('memory')\ndb.register(User)\nawait db.sync()\n\n// CRUD\nconst user = await User.create({ name: 'Alice', email: 'alice@test.com', age: 30 })\nconst found = await User.findById(user.id)\nawait found.update({ age: 31 })\nawait found.delete() // soft-delete (sets deletedAt)\nawait found.restore() // undo soft-delete\n\n// Query builder\nconst admins = await User.query()\n\t.where('role', 'admin')\n\t.where('age', '>=', 18)\n\t.orderBy('name')\n\t.limit(10)\n\t.exec()\n\nconst count = await User.count({ role: 'user' })\n\n// Cleanup\nawait db.drop()\nawait db.close()",
329
+ "methods": [
330
+ { "method": "Database.connect", "signature": "Database.connect(type, [opts])", "description": "Static. Connect to a database. type: 'memory', 'json', 'sqlite', 'mysql', 'postgres', 'mongo'. Returns Database instance." },
331
+ { "method": "db.register", "signature": "db.register(ModelClass)", "description": "Register a Model class with this database. Returns this (chainable)." },
332
+ { "method": "db.registerAll", "signature": "db.registerAll(...models)", "description": "Register multiple Model classes at once. Returns this (chainable)." },
333
+ { "method": "db.sync", "signature": "db.sync()", "description": "Async. Create/update tables for all registered models." },
334
+ { "method": "db.drop", "signature": "db.drop()", "description": "Async. Drop tables for all registered models (reverse order)." },
335
+ { "method": "db.close", "signature": "db.close()", "description": "Async. Close the database connection." },
336
+ { "method": "db.model", "signature": "db.model(name)", "description": "Retrieve a registered model class by table name." },
337
+ { "method": "db.transaction", "signature": "db.transaction(fn)", "description": "Async. Execute callback within a transaction — rolls back on error, commits on success. Adapters without native transactions run the callback directly." }
338
+ ],
339
+ "options": [
340
+ { "option": "type", "type": "string", "default": "—", "notes": "Adapter type: 'memory' | 'json' | 'sqlite' | 'mysql' | 'postgres' | 'mongo'." },
341
+ { "option": "path", "type": "string", "default": "—", "notes": "File path for 'json' adapter." },
342
+ { "option": "filename", "type": "string", "default": "—", "notes": "Database file for 'sqlite' adapter." },
343
+ { "option": "host", "type": "string", "default": "—", "notes": "Hostname for mysql/postgres/mongo." },
344
+ { "option": "port", "type": "number", "default": "—", "notes": "Port for mysql/postgres/mongo." },
345
+ { "option": "database", "type": "string", "default": "—", "notes": "Database name for mysql/postgres/mongo." },
346
+ { "option": "user / password", "type": "string", "default": "—", "notes": "Auth credentials for mysql/postgres/mongo." }
347
+ ]
348
+ },
349
+ {
350
+ "name": "Model",
351
+ "description": "Base class for ORM models. Extend it to define your schema, table, timestamps, soft-deletes, hooks, scopes, hidden fields, and relationships. Provides static CRUD, upsert, exists, scoped queries, instance lifecycle methods, increment/decrement, a fluent query builder, and eager-loading including many-to-many.",
352
+ "example": "const { Model, TYPES } = require('zero-http')\n\nclass Post extends Model {\n\tstatic table = 'posts'\n\tstatic timestamps = true\n\tstatic hidden = ['internalNotes'] // excluded from toJSON()\n\tstatic scopes = {\n\t\tpublished: q => q.where('status', 'published'),\n\t\tbyAuthor: (q, id) => q.where('authorId', id)\n\t}\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\tauthorId:{ type: TYPES.INTEGER, required: true },\n\t\tstatus: { type: TYPES.STRING, enum: ['draft', 'published'], default: 'draft' },\n\t\tviews: { type: TYPES.INTEGER, default: 0 },\n\t\tinternalNotes: { type: TYPES.TEXT }\n\t}\n}\n\n// Relationships\nUser.hasMany(Post, 'authorId')\nPost.belongsTo(User, 'authorId')\nPost.belongsToMany(Tag, { through: 'post_tags', foreignKey: 'postId', otherKey: 'tagId' })\n\n// Scoped queries\nconst published = await Post.scope('published')\nconst mine = await Post.scope('byAuthor', userId).scope('published')\n\n// Upsert + Exists\nconst { instance, created } = await Post.upsert({ title: 'Hello' }, { body: '...' })\nif (await Post.exists({ title: 'Hello' })) { ... }\n\n// Increment / Decrement\nawait post.increment('views')\nawait post.decrement('stock', 5)\n\n// Hidden fields\npost.toJSON() // internalNotes excluded",
353
+ "methods": [
354
+ { "method": "Model.create", "signature": "Model.create(data)", "description": "Async. Insert a record and return a Model instance." },
355
+ { "method": "Model.createMany", "signature": "Model.createMany(dataArray)", "description": "Async. Insert multiple records. Returns array of Model instances." },
356
+ { "method": "Model.find", "signature": "Model.find([conditions])", "description": "Async. Find all matching records. No conditions = find all." },
357
+ { "method": "Model.findOne", "signature": "Model.findOne(conditions)", "description": "Async. Find the first matching record or null." },
358
+ { "method": "Model.findById", "signature": "Model.findById(id)", "description": "Async. Find by primary key or null." },
359
+ { "method": "Model.findOrCreate", "signature": "Model.findOrCreate(conditions, [defaults])", "description": "Async. Find or create. Returns { instance, created: boolean }." },
360
+ { "method": "Model.upsert", "signature": "Model.upsert(conditions, data)", "description": "Async. Insert or update — finds by conditions, updates if exists, creates otherwise. Returns { instance, created: boolean }." },
361
+ { "method": "Model.exists", "signature": "Model.exists([conditions])", "description": "Async. Returns true if any matching records exist." },
362
+ { "method": "Model.updateWhere", "signature": "Model.updateWhere(conditions, data)", "description": "Async. Bulk update matching records. Returns affected count." },
363
+ { "method": "Model.deleteWhere", "signature": "Model.deleteWhere(conditions)", "description": "Async. Bulk delete matching records. Returns affected count." },
364
+ { "method": "Model.count", "signature": "Model.count([conditions])", "description": "Async. Count matching records." },
365
+ { "method": "Model.query", "signature": "Model.query()", "description": "Returns a Query builder for fluent querying." },
366
+ { "method": "Model.scope", "signature": "Model.scope(name, [...args])", "description": "Start a query with a named scope applied. Scopes are defined in static scopes = {}." },
367
+ { "method": "Model.first", "signature": "Model.first([conditions])", "description": "Async. Find the first record. Returns Promise<Model|null>." },
368
+ { "method": "Model.last", "signature": "Model.last([conditions])", "description": "Async. Find the last record (by PK descending). Returns Promise<Model|null>." },
369
+ { "method": "Model.all", "signature": "Model.all([conditions])", "description": "Async. Get all records (alias for find). Returns Promise<Model[]>." },
370
+ { "method": "Model.paginate", "signature": "Model.paginate(page, [perPage], [conditions])", "description": "Async. Rich pagination: returns { data, total, page, perPage, pages, hasNext, hasPrev }." },
371
+ { "method": "Model.chunk", "signature": "Model.chunk(size, fn, [conditions])", "description": "Async. Process all records in batches. fn(batch, batchIndex) — supports async." },
372
+ { "method": "Model.random", "signature": "Model.random([conditions])", "description": "Async. Get a random record. Returns Promise<Model|null>." },
373
+ { "method": "Model.pluck", "signature": "Model.pluck(field, [conditions])", "description": "Async. Pluck values for a single column. Returns Promise<Array>." },
374
+ { "method": "Model.hasMany", "signature": "Model.hasMany(RelatedModel, foreignKey, [localKey])", "description": "Define a one-to-many relationship." },
375
+ { "method": "Model.hasOne", "signature": "Model.hasOne(RelatedModel, foreignKey, [localKey])", "description": "Define a one-to-one relationship." },
376
+ { "method": "Model.belongsTo", "signature": "Model.belongsTo(RelatedModel, foreignKey, [otherKey])", "description": "Define an inverse one-to-one/many relationship." },
377
+ { "method": "Model.belongsToMany", "signature": "Model.belongsToMany(RelatedModel, opts)", "description": "Define a many-to-many relationship via junction table. opts: { through, foreignKey, otherKey, localKey, relatedKey }." },
378
+ { "method": "instance.save", "signature": "instance.save()", "description": "Async. Insert or update the record (upsert)." },
379
+ { "method": "instance.update", "signature": "instance.update(data)", "description": "Async. Update specific fields. Returns this." },
380
+ { "method": "instance.delete", "signature": "instance.delete()", "description": "Async. Delete the record (soft-delete if enabled)." },
381
+ { "method": "instance.restore", "signature": "instance.restore()", "description": "Async. Restore a soft-deleted record. Returns this." },
382
+ { "method": "instance.reload", "signature": "instance.reload()", "description": "Async. Re-fetch the record from the database. Returns this." },
383
+ { "method": "instance.increment", "signature": "instance.increment(field, [by=1])", "description": "Async. Increment a numeric field. Updates DB and instance." },
384
+ { "method": "instance.decrement", "signature": "instance.decrement(field, [by=1])", "description": "Async. Decrement a numeric field. Updates DB and instance." },
385
+ { "method": "instance.toJSON", "signature": "instance.toJSON()", "description": "Return a plain object copy of the record. Respects static hidden = [] to exclude sensitive fields." },
386
+ { "method": "instance.load", "signature": "instance.load(relationName)", "description": "Async. Eager-load a related model or collection. Supports hasMany, hasOne, belongsTo, and belongsToMany." }
387
+ ],
388
+ "options": [
389
+ { "option": "table", "type": "string", "default": "''", "notes": "Static property. Database table name." },
390
+ { "option": "schema", "type": "object", "default": "{}", "notes": "Static property. Column definitions with type, constraints, and defaults." },
391
+ { "option": "timestamps", "type": "boolean", "default": "false", "notes": "Static property. Automatically add createdAt / updatedAt columns." },
392
+ { "option": "softDelete", "type": "boolean", "default": "false", "notes": "Static property. Add deletedAt column; delete() sets it instead of removing." },
393
+ { "option": "hooks", "type": "object", "default": "{}", "notes": "Static property. Lifecycle hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete." },
394
+ { "option": "hidden", "type": "string[]", "default": "[]", "notes": "Static property. Fields excluded from toJSON() — e.g. ['password', 'resetToken']." },
395
+ { "option": "scopes", "type": "object", "default": "{}", "notes": "Static property. Named query scopes — functions that receive a Query and return it. Called via Model.scope(name, ...args)." }
396
+ ]
397
+ },
398
+ {
399
+ "name": "Query",
400
+ "description": "Fluent query builder returned by Model.query(). All filter/sort/limit methods are chainable. Execute with .exec(), .first(), .count(), .exists(), .pluck(), or aggregates. Also thenable — you can await the query directly.",
401
+ "example": "const { Model, TYPES } = require('zero-http')\n\n// Fluent query building\nconst results = await User.query()\n\t.select('id', 'name', 'email')\n\t.where('role', 'admin')\n\t.where('age', '>=', 21)\n\t.orWhere('role', 'superadmin')\n\t.whereNotNull('email')\n\t.whereIn('status', ['active', 'pending'])\n\t.whereNotIn('role', ['banned'])\n\t.whereLike('name', '%alice%')\n\t.orderBy('name', 'asc')\n\t.limit(25)\n\t.offset(50)\n\t.exec()\n\n// Pagination\nconst page2 = await User.query()\n\t.where('role', 'user')\n\t.orderBy('createdAt', 'desc')\n\t.page(2, 20) // page 2, 20 per page\n\t.exec()\n\n// Aggregation\nconst total = await User.query().where('role', 'admin').count()\nconst avgAge = await User.query().avg('age')\nconst maxPrice = await Product.query().max('price')\nconst minPrice = await Product.query().min('price')\nconst revenue = await Order.query().sum('total')\n\n// Existence + Pluck\nconst hasAdmins = await User.query().where('role', 'admin').exists()\nconst emails = await User.query().pluck('email') // ['a@b.com', ...]\n\n// Scoped queries (chained)\nconst active = await User.query().scope('active').scope('verified').limit(10)\n\n// Get first match\nconst oldest = await User.query().orderBy('age', 'desc').first()\n\n// Include soft-deleted records\nconst all = await User.query().withDeleted().exec()\n\n// Distinct + group by\nconst roles = await User.query().select('role').distinct().groupBy('role').exec()\n\n// Thenable — await directly\nconst users = await User.query().where('active', true)",
402
+ "methods": [
403
+ { "method": "select", "signature": "select(...fields)", "description": "Select specific columns. Chainable." },
404
+ { "method": "distinct", "signature": "distinct()", "description": "Return unique rows only. Chainable." },
405
+ { "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." },
406
+ { "method": "orWhere", "signature": "orWhere(field, [op], value)", "description": "Add an OR WHERE condition. Chainable." },
407
+ { "method": "whereNull", "signature": "whereNull(field)", "description": "WHERE field IS NULL. Chainable." },
408
+ { "method": "whereNotNull", "signature": "whereNotNull(field)", "description": "WHERE field IS NOT NULL. Chainable." },
409
+ { "method": "whereIn", "signature": "whereIn(field, values)", "description": "WHERE field IN (...values). Chainable." },
410
+ { "method": "whereNotIn", "signature": "whereNotIn(field, values)", "description": "WHERE field NOT IN (...values). Chainable." },
411
+ { "method": "whereBetween", "signature": "whereBetween(field, low, high)", "description": "WHERE field BETWEEN low AND high. Chainable." },
412
+ { "method": "whereNotBetween", "signature": "whereNotBetween(field, low, high)", "description": "WHERE field NOT BETWEEN low AND high. Chainable." },
413
+ { "method": "whereLike", "signature": "whereLike(field, pattern)", "description": "WHERE field LIKE pattern (% and _ wildcards). Chainable." },
414
+ { "method": "orderBy", "signature": "orderBy(field, [dir])", "description": "Sort results. dir: 'asc' (default) or 'desc'. Chainable." },
415
+ { "method": "limit", "signature": "limit(n)", "description": "Maximum number of results. Chainable." },
416
+ { "method": "offset", "signature": "offset(n)", "description": "Skip n records. Chainable." },
417
+ { "method": "page", "signature": "page(pageNum, [perPage])", "description": "Pagination helper. 1-indexed. perPage defaults to 20. Chainable." },
418
+ { "method": "groupBy", "signature": "groupBy(...fields)", "description": "Group results by columns. Chainable." },
419
+ { "method": "having", "signature": "having(field, [op], value)", "description": "Add a HAVING condition (use with groupBy). Chainable." },
420
+ { "method": "join", "signature": "join(table, localKey, foreignKey)", "description": "INNER JOIN. Chainable." },
421
+ { "method": "leftJoin", "signature": "leftJoin(table, localKey, foreignKey)", "description": "LEFT JOIN. Chainable." },
422
+ { "method": "rightJoin", "signature": "rightJoin(table, localKey, foreignKey)", "description": "RIGHT JOIN. Chainable." },
423
+ { "method": "withDeleted", "signature": "withDeleted()", "description": "Include soft-deleted records in results. Chainable." },
424
+ { "method": "scope", "signature": "scope(name, [...args])", "description": "Apply a named scope from the model's static scopes. Chainable." },
425
+ { "method": "exec", "signature": "exec()", "description": "Execute the query. Returns Promise<Model[]>." },
426
+ { "method": "first", "signature": "first()", "description": "Execute and return the first result. Returns Promise<Model|null>." },
427
+ { "method": "count", "signature": "count()", "description": "Execute and return the count. Returns Promise<number>." },
428
+ { "method": "exists", "signature": "exists()", "description": "Returns Promise<boolean> — true if any matching records exist." },
429
+ { "method": "pluck", "signature": "pluck(field)", "description": "Returns Promise<Array> of values for a single column." },
430
+ { "method": "sum", "signature": "sum(field)", "description": "Returns Promise<number> — sum of a numeric column." },
431
+ { "method": "avg", "signature": "avg(field)", "description": "Returns Promise<number> — average of a numeric column." },
432
+ { "method": "min", "signature": "min(field)", "description": "Returns Promise<*> — minimum value of a column." },
433
+ { "method": "max", "signature": "max(field)", "description": "Returns Promise<*> — maximum value of a column." },
434
+ { "method": "take", "signature": "take(n)", "description": "Alias for limit() — LINQ naming. Chainable." },
435
+ { "method": "skip", "signature": "skip(n)", "description": "Alias for offset() — LINQ naming. Chainable." },
436
+ { "method": "toArray", "signature": "toArray()", "description": "Alias for exec() — returns Promise<Model[]>." },
437
+ { "method": "orderByDesc", "signature": "orderByDesc(field)", "description": "Shorthand for orderBy(field, 'desc'). Chainable." },
438
+ { "method": "last", "signature": "last()", "description": "Execute and return the last result. Returns Promise<Model|null>." },
439
+ { "method": "when", "signature": "when(condition, fn)", "description": "Conditionally apply query logic if condition is truthy. Chainable." },
440
+ { "method": "unless", "signature": "unless(condition, fn)", "description": "Conditionally apply query logic if condition is falsy. Chainable." },
441
+ { "method": "tap", "signature": "tap(fn)", "description": "Inspect the query for debugging without breaking the chain. Chainable." },
442
+ { "method": "chunk", "signature": "chunk(size, fn)", "description": "Process results in batches. Calls fn(batch, batchIndex) for each chunk." },
443
+ { "method": "each", "signature": "each(fn)", "description": "Execute and iterate each result. fn(item, index) — supports async." },
444
+ { "method": "map", "signature": "map(fn)", "description": "Execute, transform each result. Returns Promise<Array>." },
445
+ { "method": "filter", "signature": "filter(fn)", "description": "Execute, post-filter results in JS. Returns Promise<Model[]>." },
446
+ { "method": "reduce", "signature": "reduce(fn, initial)", "description": "Execute and reduce results to a single value." },
447
+ { "method": "paginate", "signature": "paginate(page, [perPage])", "description": "Rich pagination: returns { data, total, page, perPage, pages, hasNext, hasPrev }." },
448
+ { "method": "whereRaw", "signature": "whereRaw(sql, ...params)", "description": "Inject raw SQL WHERE clause (SQL adapters only). Parameterized. Chainable." }
449
+ ],
450
+ "options": []
451
+ },
452
+ {
453
+ "name": "TYPES",
454
+ "description": "Column type constants for ORM schema definitions. Use these instead of raw strings for type safety and IDE autocomplete.",
455
+ "example": "const { TYPES } = require('zero-http')\n\nconst schema = {\n\tid: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },\n\tname: { type: TYPES.STRING, required: true },\n\tbio: { type: TYPES.TEXT },\n\tprice: { type: TYPES.FLOAT, min: 0 },\n\tactive: { type: TYPES.BOOLEAN, default: true },\n\tcreated: { type: TYPES.DATE },\n\tupdated: { type: TYPES.DATETIME },\n\tmeta: { type: TYPES.JSON, default: {} },\n\tavatar: { type: TYPES.BLOB },\n\texternalId:{ type: TYPES.UUID }\n}",
456
+ "methods": [
457
+ { "method": "TYPES.STRING", "signature": "'string'", "description": "Text column (varchar equivalent)." },
458
+ { "method": "TYPES.INTEGER", "signature": "'integer'", "description": "Integer column." },
459
+ { "method": "TYPES.FLOAT", "signature": "'float'", "description": "Floating-point number column." },
460
+ { "method": "TYPES.BOOLEAN", "signature": "'boolean'", "description": "Boolean column." },
461
+ { "method": "TYPES.DATE", "signature": "'date'", "description": "Date-only column." },
462
+ { "method": "TYPES.DATETIME", "signature": "'datetime'", "description": "Date+time column." },
463
+ { "method": "TYPES.JSON", "signature": "'json'", "description": "JSON column (stored as serialized object)." },
464
+ { "method": "TYPES.TEXT", "signature": "'text'", "description": "Long text column (no length limit)." },
465
+ { "method": "TYPES.BLOB", "signature": "'blob'", "description": "Binary data column." },
466
+ { "method": "TYPES.UUID", "signature": "'uuid'", "description": "UUID string column." }
467
+ ],
468
+ "options": []
469
+ },
158
470
  {
159
471
  "name": "compress([opts])",
160
- "description": "Response compression middleware using brotli, gzip, or deflate based on the client's Accept-Encoding header. Brotli is preferred when available (Node \u2265 11.7). Automatically negotiates encoding, respects threshold, skips SSE streams, and supports a filter function to skip specific requests.",
161
- "example": "const { createApp, compress } = require('zero-http')\nconst app = createApp()\n\n// Compress responses over 512 bytes (brotli > gzip > deflate)\napp.use(compress({ threshold: 512, level: 6 }))\n\n// Skip compression for specific routes\napp.use(compress({\n\tfilter: (req, res) => !req.url.startsWith('/stream')\n}))\n\napp.get('/big', (req, res) => {\n\tres.json({ data: 'x'.repeat(10000) })\n})",
472
+ "description": "Response compression middleware using brotli, gzip, or deflate based on the client's Accept-Encoding header. Brotli is preferred when available (Node 11.7). Automatically negotiates encoding, respects threshold, skips SSE streams, and supports a filter function to skip specific requests.",
473
+ "example": "const { createApp, compress, json } = require('zero-http')\nconst app = createApp()\n\n// Compress responses over 512 bytes (brotli > gzip > deflate)\napp.use(compress({ threshold: 512, level: 6 }))\n\n// Skip compression for specific routes\napp.use(compress({\n\tfilter: (req, res) => !req.url.startsWith('/stream')\n}))\n\napp.get('/big', (req, res) => {\n\tres.json({ data: 'x'.repeat(10000) })\n})",
162
474
  "methods": [],
163
475
  "options": [
164
476
  { "option": "threshold", "type": "number", "default": "1024", "notes": "Minimum response body size in bytes before compression is applied." },
@@ -272,7 +584,9 @@
272
584
  { "option": "max", "type": "number", "default": "100", "notes": "Maximum requests per window per key." },
273
585
  { "option": "statusCode", "type": "number", "default": "429", "notes": "HTTP status code for rate-limited responses." },
274
586
  { "option": "message", "type": "string", "default": "Too many requests, please try again later.", "notes": "Error message returned in the JSON response body when limit is exceeded." },
275
- { "option": "keyGenerator", "type": "function", "default": "(req) => req.ip || 'unknown'", "notes": "Custom function to extract a rate-limit key from the request." }
587
+ { "option": "keyGenerator", "type": "function", "default": "(req) => req.ip || 'unknown'", "notes": "Custom function to extract a rate-limit key from the request." },
588
+ { "option": "skip", "type": "function", "default": "—", "notes": "(req) => boolean. Return true to skip rate limiting for the request." },
589
+ { "option": "handler", "type": "function", "default": "—", "notes": "(req, res) => void. Custom handler for rate-limited requests instead of the default JSON response." }
276
590
  ]
277
591
  },
278
592
  {
@@ -284,5 +598,71 @@
284
598
  { "option": "logger", "type": "function", "default": "console.log", "notes": "Custom log output function." },
285
599
  { "option": "colors", "type": "boolean", "default": "auto (TTY detection)", "notes": "Enable or disable ANSI color codes. Auto-detected from process.stdout.isTTY." }
286
600
  ]
601
+ },
602
+ {
603
+ "name": "Error Classes",
604
+ "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.",
605
+ "example": "const { NotFoundError, ValidationError, createError, isHttpError, HttpError } = 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}\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// Type checking\nif (isHttpError(err)) {\n\tres.status(err.statusCode).json(err.toJSON())\n}",
606
+ "methods": [
607
+ { "method": "HttpError", "signature": "new HttpError(statusCode, [message], [opts])", "description": "Base class. opts: { code, details }." },
608
+ { "method": "BadRequestError", "signature": "new BadRequestError([message], [opts])", "description": "400 Bad Request." },
609
+ { "method": "UnauthorizedError", "signature": "new UnauthorizedError([message], [opts])", "description": "401 Unauthorized." },
610
+ { "method": "ForbiddenError", "signature": "new ForbiddenError([message], [opts])", "description": "403 Forbidden." },
611
+ { "method": "NotFoundError", "signature": "new NotFoundError([message], [opts])", "description": "404 Not Found." },
612
+ { "method": "MethodNotAllowedError", "signature": "new MethodNotAllowedError([message], [opts])", "description": "405 Method Not Allowed." },
613
+ { "method": "ConflictError", "signature": "new ConflictError([message], [opts])", "description": "409 Conflict." },
614
+ { "method": "GoneError", "signature": "new GoneError([message], [opts])", "description": "410 Gone." },
615
+ { "method": "PayloadTooLargeError", "signature": "new PayloadTooLargeError([message], [opts])", "description": "413 Payload Too Large." },
616
+ { "method": "UnprocessableEntityError", "signature": "new UnprocessableEntityError([message], [opts])", "description": "422 Unprocessable Entity." },
617
+ { "method": "ValidationError", "signature": "new ValidationError([message], [errors], [opts])", "description": "422 with field-level errors. errors stored in .errors and .details." },
618
+ { "method": "TooManyRequestsError", "signature": "new TooManyRequestsError([message], [opts])", "description": "429 Too Many Requests." },
619
+ { "method": "InternalError", "signature": "new InternalError([message], [opts])", "description": "500 Internal Server Error." },
620
+ { "method": "NotImplementedError", "signature": "new NotImplementedError([message], [opts])", "description": "501 Not Implemented." },
621
+ { "method": "BadGatewayError", "signature": "new BadGatewayError([message], [opts])", "description": "502 Bad Gateway." },
622
+ { "method": "ServiceUnavailableError", "signature": "new ServiceUnavailableError([message], [opts])", "description": "503 Service Unavailable." },
623
+ { "method": "createError", "signature": "createError(statusCode, [message], [opts])", "description": "Factory — creates the correct error class for any status code." },
624
+ { "method": "isHttpError", "signature": "isHttpError(err)", "description": "Returns true if err is an HttpError or has a statusCode. Type guard." },
625
+ { "method": "toJSON", "signature": "err.toJSON()", "description": "Serialize: { error, code, statusCode, details? }." }
626
+ ],
627
+ "options": []
628
+ },
629
+ {
630
+ "name": "errorHandler([opts])",
631
+ "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().",
632
+ "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 + error monitoring\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\tonError: (err, req) => {\n\t\terrorTracker.capture(err, { url: req.url })\n\t}\n}))",
633
+ "methods": [],
634
+ "options": [
635
+ { "option": "stack", "type": "boolean", "default": "true (non-production)", "notes": "Include stack traces in responses. Auto-detected from NODE_ENV." },
636
+ { "option": "log", "type": "boolean", "default": "true", "notes": "Log errors to console." },
637
+ { "option": "logger", "type": "function", "default": "console.error", "notes": "Custom log function." },
638
+ { "option": "formatter", "type": "function", "default": "—", "notes": "Custom response formatter: (err, req, isDev) => responseBody." },
639
+ { "option": "onError", "type": "function", "default": "—", "notes": "Callback on every error: (err, req, res) => void." }
640
+ ]
641
+ },
642
+ {
643
+ "name": "debug(namespace)",
644
+ "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.",
645
+ "example": "const { debug } = require('zero-http')\n\nconst log = debug('app:routes')\nconst dbLog = debug('db:queries')\n\nlog.info('server started on port %d', 3000)\nlog.warn('deprecated endpoint hit: %s', req.url)\nlog.error('request failed', err)\n\n// JSON mode for log aggregators\ndebug.json(true)\ndebug.level('info')\n\n// Enable specific namespaces\ndebug.enable('app:*,db:*')\n// DEBUG=app:* node server.js",
646
+ "methods": [
647
+ { "method": "debug(namespace)", "signature": "debug('app:routes')", "description": "Create a namespaced logger. Returns a function with level methods." },
648
+ { "method": "log()", "signature": "log(...args)", "description": "Log at debug level. Supports %s, %d, %j, %o format specifiers." },
649
+ { "method": "log.trace()", "signature": "log.trace(...args)", "description": "Log at trace level (most verbose)." },
650
+ { "method": "log.info()", "signature": "log.info(...args)", "description": "Log at info level." },
651
+ { "method": "log.warn()", "signature": "log.warn(...args)", "description": "Log at warn level." },
652
+ { "method": "log.error()", "signature": "log.error(...args)", "description": "Log at error level." },
653
+ { "method": "log.fatal()", "signature": "log.fatal(...args)", "description": "Log at fatal level (most severe)." },
654
+ { "method": "debug.level()", "signature": "debug.level('info')", "description": "Set minimum log level globally." },
655
+ { "method": "debug.enable()", "signature": "debug.enable('app:*')", "description": "Enable namespaces by pattern." },
656
+ { "method": "debug.disable()", "signature": "debug.disable()", "description": "Disable all debug output." },
657
+ { "method": "debug.json()", "signature": "debug.json(true)", "description": "Enable structured JSON output." },
658
+ { "method": "debug.timestamps()", "signature": "debug.timestamps(false)", "description": "Toggle timestamps." },
659
+ { "method": "debug.colors()", "signature": "debug.colors(false)", "description": "Toggle ANSI colors." },
660
+ { "method": "debug.output()", "signature": "debug.output(stream)", "description": "Set custom output stream." },
661
+ { "method": "debug.reset()", "signature": "debug.reset()", "description": "Reset all settings to defaults." }
662
+ ],
663
+ "options": [
664
+ { "option": "DEBUG", "type": "env var", "default": "—", "notes": "Comma-separated namespace patterns. Supports * glob and - prefix to exclude." },
665
+ { "option": "DEBUG_LEVEL", "type": "env var", "default": "debug", "notes": "Minimum log level: trace, debug, info, warn, error, fatal, silent." }
666
+ ]
287
667
  }
288
668
  ]