zero-http 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +25 -184
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
"language": "javascript",
|
|
12
12
|
"code": "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\t// Broadcast to all other clients\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)"
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
"title": "WebSocket Rooms with Pool",
|
|
16
|
+
"description": "Use WebSocketPool for room-based messaging instead of manually tracking connections. Auto-removes on disconnect.",
|
|
17
|
+
"language": "javascript",
|
|
18
|
+
"code": "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\n// Admin endpoint to see pool status\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}))\n\napp.listen(3000)"
|
|
19
|
+
},
|
|
14
20
|
{
|
|
15
21
|
"title": "Server-Sent Events (SSE) Stream",
|
|
16
22
|
"description": "Push real-time events to browser clients using res.sse(). Supports auto-IDs, named events, keep-alive, and graceful cleanup on disconnect.",
|
|
@@ -23,12 +29,72 @@
|
|
|
23
29
|
"language": "javascript",
|
|
24
30
|
"code": "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('/').get((req, res) => res.json([])).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)\n// GET /debug/routes returns the full route table"
|
|
25
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"title": "Security Headers with Helmet",
|
|
34
|
+
"description": "Harden your app in one line with helmet(). Sets CSP, HSTS, X-Frame-Options, Referrer-Policy, and more. Override or disable any header individually.",
|
|
35
|
+
"language": "javascript",
|
|
36
|
+
"code": "const { createApp, helmet, json } = require('zero-http')\nconst app = createApp()\n\n// Sensible defaults — just use it\napp.use(helmet())\napp.use(json())\n\napp.get('/api/data', (req, res) => res.json({ safe: true }))\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}))\n\napp.listen(3000)\n// Response headers set automatically:\n// Content-Security-Policy, X-Frame-Options, X-Content-Type-Options,\n// Referrer-Policy, X-DNS-Prefetch-Control, and more"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"title": "Request Timeout & Request ID",
|
|
40
|
+
"description": "Add a global timeout so no request hangs forever, and tag each request with a unique ID for log correlation.",
|
|
41
|
+
"language": "javascript",
|
|
42
|
+
"code": "const { createApp, timeout, requestId, logger, json } = require('zero-http')\nconst app = createApp()\n\napp.use(requestId()) // sets req.id + X-Request-Id header\napp.use(logger()) // logs method, url, status, time\napp.use(timeout(10000)) // 10s timeout → 408 if exceeded\napp.use(json())\n\napp.get('/fast', (req, res) => {\n\tconsole.log('Request:', req.id)\n\tres.json({ requestId: req.id })\n})\n\napp.get('/slow', async (req, res) => {\n\tawait new Promise(r => setTimeout(r, 15000))\n\tif (req.timedOut) return // already responded with 408\n\tres.json({ data: 'done' })\n})\n\napp.listen(3000)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"title": "Cookie Parsing & Signing",
|
|
46
|
+
"description": "Parse cookies with timing-safe HMAC-SHA256 verification, JSON cookie support (j: prefix), auto-signing via signed option, priority, and partitioned (CHIPS) attributes.",
|
|
47
|
+
"language": "javascript",
|
|
48
|
+
"code": "const { createApp, cookieParser, json } = require('zero-http')\nconst app = createApp()\n\n// Enable cookie parsing with a signing secret\napp.use(cookieParser('super-secret-key'))\napp.use(json())\n\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 // signing secret available downstream\n\t})\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, 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) attributes\n\tres.cookie('important', 'value', { priority: 'High', partitioned: true, secure: true })\n\n\tres.json({ ok: true })\n})\n\n// Static helpers for manual use\napp.get('/manual', (req, res) => {\n\tconst signed = cookieParser.sign('data', 'secret')\n\tconst value = cookieParser.unsign(signed, ['secret']) // 'data' or false\n\tconst parsed = cookieParser.jsonCookie('j:{\"a\":1}') // { a: 1 }\n\tres.json({ signed, value, parsed })\n})\n\napp.get('/clear', (req, res) => {\n\tres.clearCookie('session').clearCookie('prefs').json({ cleared: true })\n})\n\napp.listen(3000)"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "CSRF Protection",
|
|
52
|
+
"description": "Protect state-changing requests with double-submit cookie CSRF tokens. GET/HEAD/OPTIONS are automatically skipped.",
|
|
53
|
+
"language": "javascript",
|
|
54
|
+
"code": "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\t// csrf() middleware already validated the token\n\tres.json({ success: true, amount: req.body.amount })\n})\n\n// Client-side fetch example:\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// })\n\napp.listen(3000)"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"title": "Request Validation",
|
|
58
|
+
"description": "Validate and coerce req.body, req.query, and req.params with schema rules. Supports 11 types, pattern matching, ranges, enums, and custom validators.",
|
|
59
|
+
"language": "javascript",
|
|
60
|
+
"code": "const { createApp, json, validate } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\n// Validate body with auto-coercion\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\t// req.body is sanitized — unknown fields stripped\n\tres.status(201).json(req.body)\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 fails → 422 { errors: ['name is required', ...] }\n\napp.listen(3000)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"title": "Environment Configuration",
|
|
64
|
+
"description": "Load and validate environment variables from .env files with type coercion. Supports multi-file loading, schema enforcement, and 9 data types.",
|
|
65
|
+
"language": "javascript",
|
|
66
|
+
"code": "const { createApp, env } = require('zero-http')\n\n// Load with schema — validates and coerces types\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\n// File load order: .env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local\n// process.env always wins over file values\n\napp.listen(env.PORT)"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"title": "ORM — Models & CRUD",
|
|
70
|
+
"description": "Define models with scopes, hidden fields, relationships, and lifecycle hooks. Supports upsert, increment/decrement, belongsToMany, and transactions.",
|
|
71
|
+
"language": "javascript",
|
|
72
|
+
"code": "const { createApp, json, Database, 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'] // excluded from toJSON()\n\tstatic scopes = {\n\t\tactive: (q) => q.where('active', true),\n\t\trole: (q, role) => q.where('role', role)\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 },\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\nconst db = Database.connect('memory')\ndb.register(User)\n\nconst app = createApp()\napp.use(json())\n\n// Scoped queries\napp.get('/users/active', async (req, res) => {\n\tres.json(await User.scope('active'))\n})\n\n// Upsert — insert or update\napp.put('/users', async (req, res) => {\n\tconst { instance, created } = await User.upsert({ email: req.body.email }, req.body)\n\tres.status(created ? 201 : 200).json(instance.toJSON())\n})\n\n// Increment login count\napp.post('/users/:id/login', async (req, res) => {\n\tconst user = await User.findById(+req.params.id)\n\tawait user.increment('logins')\n\tres.json(user.toJSON()) // password excluded by hidden\n})\n\n// Check existence without fetching\napp.get('/users/exists/:email', async (req, res) => {\n\tres.json({ exists: await User.exists({ email: req.params.email }) })\n})\n\n// Transaction\napp.post('/transfer', async (req, res) => {\n\tawait db.transaction(async () => {\n\t\tconst from = await User.findById(req.body.from)\n\t\tconst to = await User.findById(req.body.to)\n\t\tawait from.decrement('balance', req.body.amount)\n\t\tawait to.increment('balance', req.body.amount)\n\t})\n\tres.json({ ok: true })\n})\n\n;(async () => {\n\tawait db.sync()\n\tapp.listen(3000, () => console.log('ORM demo on :3000'))\n})()"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"title": "ORM — Query Builder",
|
|
76
|
+
"description": "Fluent query builder with 30+ chainable methods — filtering, sorting, pagination, aggregates (sum/avg/min/max), scopes, pluck, exists, and more.",
|
|
77
|
+
"language": "javascript",
|
|
78
|
+
"code": "const { Model, TYPES } = require('zero-http')\n\n// Assuming User model with scopes is defined and db.sync() has run:\n\n// 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 totalSales = await Order.query().where('status', 'paid').sum('total')\nconst maxPrice = await Product.query().max('price')\nconst minPrice = await Product.query().min('price')\nconst count = await User.query().where('role', 'admin').count()\n\n// Existence check (returns boolean)\nconst hasAdmins = await User.query().where('role', 'admin').exists()\n\n// Pluck a single column\nconst emails = await User.query()\n\t.where('active', true)\n\t.pluck('email') // ['alice@x.com', 'bob@y.com', ...]\n\n// Pagination\nconst page2 = await User.query()\n\t.where('active', true)\n\t.orderBy('createdAt', 'desc')\n\t.page(2, 20)\n\t.exec()\n\n// Joins (inner, left, right)\nconst withOrders = await User.query()\n\t.leftJoin('orders', 'id', 'userId')\n\t.rightJoin('profiles', 'id', 'userId')\n\t.exec()\n\n// Thenable — just await directly\nconst users = await User.query().where('active', true)"
|
|
79
|
+
},
|
|
26
80
|
{
|
|
27
81
|
"title": "Response Compression",
|
|
28
82
|
"description": "Automatically compress responses with brotli, gzip, or deflate above a size threshold using the compress() middleware.",
|
|
29
83
|
"language": "javascript",
|
|
30
84
|
"code": "const { createApp, compress, json } = require('zero-http')\nconst app = createApp()\n\n// brotli > gzip > deflate, auto-negotiated\napp.use(compress({ threshold: 512, level: 6 }))\napp.use(json())\n\napp.get('/big', (req, res) => {\n\t// This response is large enough to be compressed\n\tres.json({ data: 'x'.repeat(10000) })\n})\n\napp.get('/small', (req, res) => {\n\t// Below threshold — sent uncompressed\n\tres.json({ ok: true })\n})\n\napp.listen(3000)"
|
|
31
85
|
},
|
|
86
|
+
{
|
|
87
|
+
"title": "Content Negotiation & File Downloads",
|
|
88
|
+
"description": "Use res.format() for Accept-header content negotiation, res.links() for pagination headers, and res.download() for file attachments.",
|
|
89
|
+
"language": "javascript",
|
|
90
|
+
"code": "const { createApp, json } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\n// Content negotiation — respond based on Accept header\napp.get('/data', (req, res) => {\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// Pagination links\napp.get('/items', (req, res) => {\n\tres.links({ next: '/items?page=3', last: '/items?page=10' })\n\t .json([]) // Link: </items?page=3>; rel=\"next\", </items?page=10>; rel=\"last\"\n})\n\n// File download\napp.get('/report', (req, res) => {\n\tres.download('/path/to/report.pdf', 'monthly-report.pdf')\n})\n\n// Send status text\napp.get('/health', (req, res) => res.sendStatus(200)) // sends 'OK'\n\napp.listen(3000)"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"title": "App Settings, Groups & Chaining",
|
|
94
|
+
"description": "Use Express-like set/get/enable/disable settings, group routes under a prefix with shared middleware, and chain route registrations.",
|
|
95
|
+
"language": "javascript",
|
|
96
|
+
"code": "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 available in every req.locals\napp.locals.appName = 'My API'\napp.locals.version = '1.0.0'\n\n// Route chaining — all methods return the app\napp.get('/', (req, res) => res.json({ app: req.locals.appName }))\n .get('/health', (req, res) => res.sendStatus(200))\n .get('/version', (req, res) => res.json({ v: res.locals.version }))\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\trouter.get('/users/:id', (req, res) => res.json({ id: req.params.id }))\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\n// Chain helper for a single path\napp.chain('/items')\n\t.get((req, res) => res.json([]))\n\t.post((req, res) => res.status(201).json(req.body))\n\napp.listen(3000)"
|
|
97
|
+
},
|
|
32
98
|
{
|
|
33
99
|
"title": "HTTPS Server",
|
|
34
100
|
"description": "Start an HTTPS server by passing TLS options to listen(). All modules respect req.secure and req.protocol for protocol-aware logic.",
|
|
@@ -65,6 +131,18 @@
|
|
|
65
131
|
"language": "javascript",
|
|
66
132
|
"code": "const { createApp, json } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\napp.get('/fail', (req, res) => {\n\tthrow new Error('Something broke!')\n})\n\napp.onError((err, req, res, next) => {\n\tconsole.error(`[${new Date().toISOString()}]`, err.stack)\n\tres.status(err.statusCode || 500).json({\n\t\terror: err.message,\n\t\tpath: req.url\n\t})\n})\n\napp.listen(3000)"
|
|
67
133
|
},
|
|
134
|
+
{
|
|
135
|
+
"title": "Error Classes & errorHandler",
|
|
136
|
+
"description": "Use built-in HTTP error classes with the errorHandler middleware for structured error responses, field-level validation errors, and production-safe error formatting.",
|
|
137
|
+
"language": "javascript",
|
|
138
|
+
"code": "const { createApp, json, errorHandler,\n\tNotFoundError, ValidationError, ForbiddenError,\n\tcreateError, isHttpError } = require('zero-http')\nconst app = createApp()\napp.use(json())\n\n// Throw typed errors — automatically caught by the router\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 = 'Email is required'\n\tif (!req.body.name) errors.name = 'Name is required'\n\tif (Object.keys(errors).length > 0) {\n\t\tthrow new ValidationError('Invalid input', errors)\n\t\t// → 422 { error: 'Invalid input', code: 'VALIDATION_FAILED',\n\t\t// statusCode: 422, details: { email: '...', name: '...' } }\n\t}\n\tres.status(201).json(req.body)\n})\n\n// Factory — create errors by status code\napp.delete('/items/:id', async (req, res) => {\n\tif (!req.user?.admin) throw createError(403, 'Admin only')\n\tres.json({ deleted: true })\n})\n\n// errorHandler: structured responses, stack in dev, hide 5xx in prod\napp.onError(errorHandler({\n\tformatter: (err, req, isDev) => ({\n\t\tsuccess: false,\n\t\tmessage: err.message,\n\t\tcode: err.code,\n\t\t...(err.details && { details: err.details }),\n\t\t...(isDev && { stack: err.stack })\n\t}),\n\tonError: (err) => console.error('[ERROR]', err.message)\n}))\n\napp.listen(3000)"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"title": "Debug Logger",
|
|
142
|
+
"description": "Use the built-in namespaced debug logger with log levels, JSON output mode for production, and environment variable control.",
|
|
143
|
+
"language": "javascript",
|
|
144
|
+
"code": "const { createApp, debug } = require('zero-http')\n\n// Create namespaced loggers\nconst log = debug('app:server')\nconst dbLog = debug('app:db')\nconst authLog = debug('app:auth')\n\n// Log at different levels\nlog.info('server starting on port %d', 3000)\ndbLog.trace('query executed: %s', 'SELECT * FROM users')\nauthLog.warn('failed login attempt from %s', '192.168.1.1')\nauthLog.error('token verification failed', new Error('expired'))\n\n// Conditional expensive logging\nif (dbLog.enabled) {\n\tdbLog('detailed query plan: %j', buildQueryPlan())\n}\n\n// Production: structured JSON output for log aggregators\nif (process.env.NODE_ENV === 'production') {\n\tdebug.json(true) // { timestamp, level, namespace, message }\n\tdebug.level('info') // only info and above\n\tdebug.colors(false) // no ANSI codes\n}\n\n// Environment variables:\n// DEBUG=app:* node server.js → enable all app:* namespaces\n// DEBUG=*,-app:db node server.js → everything except app:db\n// DEBUG_LEVEL=warn node server.js → only warn, error, fatal\n\nconst app = createApp()\napp.get('/', (req, res) => {\n\tlog.info('handling request: %s %s', req.method, req.url)\n\tres.json({ ok: true })\n})\napp.listen(3000)"
|
|
145
|
+
},
|
|
68
146
|
{
|
|
69
147
|
"title": "Server-Side Fetch with Progress & TLS",
|
|
70
148
|
"description": "Use the built-in fetch client with timeout, abort support, download progress tracking, and custom TLS options for HTTPS URLs.",
|
|
@@ -79,8 +157,8 @@
|
|
|
79
157
|
},
|
|
80
158
|
{
|
|
81
159
|
"title": "Full-Featured Server Skeleton",
|
|
82
|
-
"description": "Combines logging, CORS, compression, body parsing, rate limiting, static serving, WebSocket, SSE, Router sub-apps,
|
|
160
|
+
"description": "Combines security headers, CSRF, cookies, timeouts, request IDs, logging, CORS, compression, body parsing, rate limiting, validation, static serving, WebSocket with room pools, SSE, Router sub-apps, app settings, and error handling.",
|
|
83
161
|
"language": "javascript",
|
|
84
|
-
"code": "const path = require('path')\nconst { createApp, cors, json, urlencoded, text, compress,\n\
|
|
162
|
+
"code": "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\napp.use(requestId())\napp.use(logger({ format: 'dev' }))\napp.use(helmet())\napp.use(cors())\napp.use(compress())\napp.use(timeout(30000))\napp.use(rateLimit({ windowMs: 60000, max: 200 }))\napp.use(cookieParser('my-secret'))\napp.use(json({ limit: '1mb' }))\napp.use(urlencoded({ extended: true }))\napp.use(text())\napp.use(csrf())\napp.use(serveStatic(path.join(__dirname, 'public')))\n\n// API routes with validation\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, () => console.log('Server running on :3000'))"
|
|
85
163
|
}
|
|
86
164
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[
|
|
2
|
-
{ "option": "createApp()", "type": "function", "default": "—", "notes": "Creates an app instance with use(), get(), post(), put(), delete(), patch(), options(), head(), all(), ws(), onError(), listen(port, [tlsOpts], [cb]), close(), routes(),
|
|
2
|
+
{ "option": "createApp()", "type": "function", "default": "—", "notes": "Creates an app instance with use(), get(), post(), put(), delete(), patch(), options(), head(), all(), ws(), onError(), listen(port, [tlsOpts], [cb]), close(), routes(), handler. Also: set/get (dual-purpose), enable/disable, enabled/disabled, locals, param, group, chain." },
|
|
3
3
|
{ "option": "Router()", "type": "function", "default": "—", "notes": "Creates a standalone router. Methods: get/post/put/delete/patch/options/head/all(path, [opts], ...handlers), use(fn|prefix+router), route(path), routes(). Supports { secure } option." },
|
|
4
4
|
{ "option": "json([opts])", "type": "function", "default": "—", "notes": "Parses JSON bodies into req.body. Options: limit, strict (true), reviver, type ('application/json'), requireSecure (false)." },
|
|
5
5
|
{ "option": "urlencoded([opts])", "type": "function", "default": "—", "notes": "Parses URL-encoded bodies. Options: extended (false), limit, type ('application/x-www-form-urlencoded'), requireSecure (false)." },
|
|
@@ -7,13 +7,28 @@
|
|
|
7
7
|
{ "option": "raw([opts])", "type": "function", "default": "—", "notes": "Reads raw bytes into a Buffer on req.body. Options: type ('application/octet-stream'), limit, requireSecure (false)." },
|
|
8
8
|
{ "option": "multipart([opts])", "type": "function", "default": "—", "notes": "Streams file parts to disk; sets req.body to { fields, files }. Options: dir (os.tmpdir()/zero-http-uploads), maxFileSize, requireSecure (false)." },
|
|
9
9
|
{ "option": "static(rootPath, [opts])", "type": "function", "default": "—", "notes": "Serves static files with 60+ MIME types. Options: index ('index.html'), maxAge (0), dotfiles ('ignore'), extensions, setHeaders." },
|
|
10
|
-
{ "option": "cors([opts])", "type": "function", "default": "—", "notes": "CORS middleware with preflight handling. Options: origin ('*'), methods, allowedHeaders, credentials (false)." },
|
|
11
|
-
{ "option": "compress([opts])", "type": "function", "default": "
|
|
10
|
+
{ "option": "cors([opts])", "type": "function", "default": "—", "notes": "CORS middleware with preflight handling. Options: origin ('*'), methods, allowedHeaders, exposedHeaders, credentials (false), maxAge." },
|
|
11
|
+
{ "option": "compress([opts])", "type": "function", "default": "—", "notes": "Response compression middleware (brotli/gzip/deflate, auto-negotiated). Brotli preferred when available (Node ≥ 11.7). Skips SSE streams. Options: threshold (1024), level (-1 zlib default), filter fn." },
|
|
12
|
+
{ "option": "helmet([opts])", "type": "function", "default": "—", "notes": "Security headers middleware. Sets CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and 11 more headers. 16 configurable options — all can be set to false to disable individually." },
|
|
13
|
+
{ "option": "timeout(ms, [opts])", "type": "function", "default": "—", "notes": "Request timeout middleware. Default 30s. Sends 408 if handler doesn't respond in time. Sets req.timedOut. Options: status (408), message ('Request Timeout')." },
|
|
14
|
+
{ "option": "requestId([opts])", "type": "function", "default": "—", "notes": "Generates/extracts a unique request ID (crypto.randomUUID). Sets req.id and response header. Options: header ('X-Request-Id'), generator (UUID v4), trustProxy (false)." },
|
|
15
|
+
{ "option": "cookieParser([secret], [opts])", "type": "function", "default": "—", "notes": "Parses Cookie header into req.cookies and req.signedCookies (timing-safe HMAC-SHA256). Auto-parses JSON cookies (j: prefix). Sets req.secret and req.secrets for downstream use. Static helpers: sign(value, secret), unsign(value, secrets), jsonCookie(str), parseJSON(obj). Options: decode (true). Supports secret rotation via array. res.cookie() enhanced: signed (auto-sign), priority (Low/Medium/High), partitioned (CHIPS)." },
|
|
16
|
+
{ "option": "csrf([opts])", "type": "function", "default": "—", "notes": "Double-submit cookie CSRF protection. Sets req.csrfToken. Token checked via header, body._csrf, or query._csrf. Options: cookie ('_csrf'), header ('x-csrf-token'), saltLength (18), secret (auto), ignoreMethods (['GET','HEAD','OPTIONS']), ignorePaths ([]), onError." },
|
|
17
|
+
{ "option": "validate(schema, [opts])", "type": "function", "default": "—", "notes": "Request validation with 11 types (string, integer, number, float, boolean, array, json, date, uuid, email, url) and auto-coercion. Validates body, query, params. Options: stripUnknown (true), onError. Static: validate.field(), validate.object()." },
|
|
18
|
+
{ "option": "env", "type": "object", "default": "—", "notes": "Typed environment config — proxy-based accessor. env.load(schema, opts) loads .env files with type coercion. 9 schema types: string, number, integer, port, boolean, array, json, url, enum. Methods: load, get, require, has, all, reset, parse." },
|
|
19
|
+
{ "option": "Database", "type": "class", "default": "—", "notes": "ORM entry point. Database.connect(type, opts) supports 6 adapters: memory, json, sqlite, mysql, postgres, mongo. Methods: register, registerAll, sync, drop, close, model, transaction(fn). transaction() wraps callback in begin/commit/rollback when supported." },
|
|
20
|
+
{ "option": "Model", "type": "class", "default": "—", "notes": "ORM base class — extend with table, schema, timestamps, softDelete, hooks, hidden, scopes. Static CRUD: create, createMany, find, findOne, findById, findOrCreate, updateWhere, deleteWhere, count, query, exists, upsert, scope. Shortcuts: first, last, all, paginate, chunk, random, pluck. Instance: save, update, delete, restore, reload, toJSON, load, increment, decrement. Relationships: hasMany, hasOne, belongsTo, belongsToMany (junction table). hidden excludes fields from toJSON(). scopes define reusable named query conditions." },
|
|
21
|
+
{ "option": "Query", "type": "class", "default": "—", "notes": "Fluent query builder from Model.query(). Chainable: select, distinct, where, orWhere, whereNull, whereNotNull, whereIn, whereNotIn, whereBetween, whereNotBetween, whereLike, whereRaw, orderBy, orderByDesc, limit, offset, page, groupBy, having, join, leftJoin, rightJoin, withDeleted, scope, when, unless, tap. Aliases: take (limit), skip (offset), toArray (exec). Execute: exec, first, last, count, exists, pluck. Aggregates: sum, avg, min, max. Functional: each, map, filter, reduce, chunk, paginate. Also thenable." },
|
|
22
|
+
{ "option": "TYPES", "type": "object", "default": "—", "notes": "ORM column type constants: STRING, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME, JSON, TEXT, BLOB, UUID." },
|
|
12
23
|
{ "option": "fetch(url, [opts])", "type": "function", "default": "—", "notes": "Node HTTP/HTTPS client. Response has ok, status, statusText, secure, url, headers.get(), text(), json(), arrayBuffer(). Options: method, headers, body (auto-serialized), timeout, signal, agent, onDownloadProgress, onUploadProgress, plus TLS options (rejectUnauthorized, ca, cert, key, pfx, passphrase, servername, minVersion, maxVersion)." },
|
|
13
|
-
{ "option": "rateLimit([opts])", "type": "function", "default": "—", "notes": "Per-IP rate limiter. Sets X-RateLimit
|
|
24
|
+
{ "option": "rateLimit([opts])", "type": "function", "default": "—", "notes": "Per-IP rate limiter. Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers. Options: windowMs (60000), max (100), statusCode (429), message, keyGenerator, skip (fn to bypass), handler (custom rate-limit response)." },
|
|
14
25
|
{ "option": "logger([opts])", "type": "function", "default": "—", "notes": "Request logger with response timing. Options: format ('dev'|'short'|'tiny'), logger (console.log), colors (auto TTY)." },
|
|
15
|
-
{ "option": "app.ws(path, [opts], handler)", "type": "method", "default": "
|
|
16
|
-
{ "option": "res.sse([opts])", "type": "method", "default": "
|
|
17
|
-
{ "option": "WebSocketConnection", "type": "class", "default": "
|
|
18
|
-
{ "option": "
|
|
26
|
+
{ "option": "app.ws(path, [opts], handler)", "type": "method", "default": "—", "notes": "Register a WebSocket upgrade handler. handler receives (ws, req). Options: maxPayload (1MB), pingInterval (30s), verifyClient fn. ws has send/sendJSON/ping/pong/close/terminate, on/once/off/removeAllListeners/listenerCount. Events: message/close/error/ping/pong/drain." },
|
|
27
|
+
{ "option": "res.sse([opts])", "type": "method", "default": "—", "notes": "Open an SSE stream. Returns SSEStream with send/sendJSON/event/comment/retry/keepAlive/flush/close and on/once/off/removeAllListeners/listenerCount. Options: status (200), retry, keepAlive (0), keepAliveComment ('ping'), autoId (false), startId (1), pad (0), headers." },
|
|
28
|
+
{ "option": "WebSocketConnection", "type": "class", "default": "—", "notes": "WebSocket connection wrapper. Properties: id, readyState, protocol, extensions, headers, ip, query, url, secure, maxPayload, connectedAt, uptime, bufferedAmount, data (user store). Constants: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3)." },
|
|
29
|
+
{ "option": "WebSocketPool", "type": "class", "default": "—", "notes": "Connection & room manager. Methods: add, remove, join, leave, broadcast, broadcastJSON, toRoom, toRoomJSON, in, roomsOf, closeAll. Getters: size, clients, rooms, roomSize(room). Auto-removes on close." },
|
|
30
|
+
{ "option": "SSEStream", "type": "class", "default": "—", "notes": "SSE stream controller. Properties: connected, eventCount, bytesSent, connectedAt, uptime, lastEventId, secure, data (user store). Events: close, error." },
|
|
31
|
+
{ "option": "HttpError", "type": "class", "default": "—", "notes": "Base HTTP error class with statusCode, code, details, toJSON(). 15 subclasses: BadRequestError (400), UnauthorizedError (401), ForbiddenError (403), NotFoundError (404), MethodNotAllowedError (405), ConflictError (409), GoneError (410), PayloadTooLargeError (413), UnprocessableEntityError (422), ValidationError (422 + field errors), TooManyRequestsError (429), InternalError (500), NotImplementedError (501), BadGatewayError (502), ServiceUnavailableError (503). Helpers: createError(statusCode), isHttpError(err)." },
|
|
32
|
+
{ "option": "errorHandler([opts])", "type": "function", "default": "—", "notes": "Configurable error-handling middleware for app.onError(). Options: stack (auto from NODE_ENV), log (true), logger (console.error), formatter (custom response shape), onError (error monitoring callback). Hides 5xx details in production." },
|
|
33
|
+
{ "option": "debug(namespace)", "type": "function", "default": "—", "notes": "Namespaced debug logger with levels. Returns a log function with .trace/.info/.warn/.error/.fatal methods. Global config: debug.enable(pattern), debug.disable(), debug.level(lvl), debug.json(bool), debug.timestamps(bool), debug.colors(bool), debug.output(stream), debug.reset(). Env vars: DEBUG (namespace patterns), DEBUG_LEVEL (min level)." }
|
|
19
34
|
]
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
7
|
<title>zero-http — demo & reference</title>
|
|
8
8
|
<meta name="description"
|
|
9
|
-
content="zero-http is a zero-dependency
|
|
9
|
+
content="zero-http is a zero-dependency backend framework for Node.js with Express-like routing, built-in ORM, WebSocket, SSE, security middleware, body parsers, compression, and more." />
|
|
10
10
|
<meta name="keywords"
|
|
11
11
|
content="nodejs,http,https,server,express,expressjs,middleware,body-parser,multipart,uploads,static files,fetch,http client,websocket,sse,server-sent events,compression,router" />
|
|
12
12
|
<meta name="author" content="Anthony Wiedman" />
|
|
13
|
+
<link rel="icon" type="image/svg+xml" href="/vendor/icons/logo.svg" />
|
|
13
14
|
|
|
14
15
|
<link rel="stylesheet" href="/styles.css" />
|
|
15
16
|
<link rel="stylesheet" href="/vendor/prism-okaidia.css" />
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
<script src="/vendor/prism-toolbar.min.js" defer></script>
|
|
24
25
|
|
|
25
26
|
<script src="/scripts/helpers.js" defer></script>
|
|
27
|
+
<script src="/scripts/custom-select.js" defer></script>
|
|
26
28
|
<script src="/scripts/uploads.js" defer></script>
|
|
27
29
|
<script src="/scripts/playground.js" defer></script>
|
|
28
30
|
<script src="/scripts/proxy.js" defer></script>
|
|
@@ -45,12 +47,12 @@
|
|
|
45
47
|
<img src="/vendor/icons/logo.svg" alt="zero-http logo" class="header-logo" width="50" height="50" />
|
|
46
48
|
<div>
|
|
47
49
|
<div class="logo">zero-http</div>
|
|
48
|
-
<div class="subtitle">Zero-dependency
|
|
50
|
+
<div class="subtitle">Zero-dependency backend framework for Node.js</div>
|
|
49
51
|
</div>
|
|
50
52
|
</a>
|
|
51
53
|
|
|
52
54
|
<div class="repo-buttons" role="navigation" aria-label="Repository links">
|
|
53
|
-
<a class="icon-btn" href="https://github.com/tonywied17/zero-http
|
|
55
|
+
<a class="icon-btn" href="https://github.com/tonywied17/zero-http" target="_blank"
|
|
54
56
|
rel="noopener noreferrer" aria-label="View on GitHub">
|
|
55
57
|
<svg class="icon-svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
56
58
|
<path
|
|
@@ -86,18 +88,17 @@
|
|
|
86
88
|
</button>
|
|
87
89
|
</div>
|
|
88
90
|
<ul>
|
|
89
|
-
<li><a href="#features">Features</a></li>
|
|
90
|
-
|
|
91
|
-
<li><a href="#
|
|
92
|
-
<li class="toc-collapsible"><a href="#api-reference">API Reference</a></li>
|
|
93
|
-
<li class="toc-collapsible"><a href="#simple-examples">Simple Examples</a></li>
|
|
94
|
-
<li class="toc-collapsible"><a href="#playground">Playground</a>
|
|
91
|
+
<li data-static="features"><a href="#features">Features</a></li>
|
|
92
|
+
<!-- Dynamic doc sections inserted here by data-sections.js -->
|
|
93
|
+
<li data-static="playground" class="toc-collapsible"><a href="#playground">Playground</a>
|
|
95
94
|
<ul class="toc-sub">
|
|
96
95
|
<li class="toc-sub-item"><a href="#upload-testing">Upload multipart data</a></li>
|
|
97
96
|
<li class="toc-sub-item"><a href="#parser-tests">Parser tests</a></li>
|
|
98
97
|
<li class="toc-sub-item"><a href="#proxy-test">Proxy test</a></li>
|
|
99
98
|
<li class="toc-sub-item"><a href="#ws-chat">WebSocket chat</a></li>
|
|
100
99
|
<li class="toc-sub-item"><a href="#sse-viewer">SSE viewer</a></li>
|
|
100
|
+
<li class="toc-sub-item"><a href="#orm-crud">ORM Task Manager</a></li>
|
|
101
|
+
<li class="toc-sub-item"><a href="#cookie-explorer">Cookie Explorer</a></li>
|
|
101
102
|
</ul>
|
|
102
103
|
</li>
|
|
103
104
|
</ul>
|
|
@@ -178,7 +179,7 @@
|
|
|
178
179
|
width="48" height="48" />
|
|
179
180
|
</div>
|
|
180
181
|
<div class="feature-text"><h5>Compression</h5>
|
|
181
|
-
<p>
|
|
182
|
+
<p>Brotli/gzip/deflate with threshold & filter.</p></div>
|
|
182
183
|
</div>
|
|
183
184
|
<div class="feature-card">
|
|
184
185
|
<div class="feature-icon">
|
|
@@ -194,7 +195,39 @@
|
|
|
194
195
|
width="48" height="48" />
|
|
195
196
|
</div>
|
|
196
197
|
<div class="feature-text"><h5>Modular Router</h5>
|
|
197
|
-
<p>Sub-apps, chaining, introspection.</p></div>
|
|
198
|
+
<p>Sub-apps, groups, chaining, introspection.</p></div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="feature-card">
|
|
201
|
+
<div class="feature-icon">
|
|
202
|
+
<img class="icon-svg" src="/vendor/icons/security.svg" alt="" aria-hidden="true"
|
|
203
|
+
width="48" height="48" />
|
|
204
|
+
</div>
|
|
205
|
+
<div class="feature-text"><h5>Security</h5>
|
|
206
|
+
<p>Helmet headers, CSRF protection, cookies.</p></div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="feature-card">
|
|
209
|
+
<div class="feature-icon">
|
|
210
|
+
<img class="icon-svg" src="/vendor/icons/validate.svg" alt="" aria-hidden="true"
|
|
211
|
+
width="48" height="48" />
|
|
212
|
+
</div>
|
|
213
|
+
<div class="feature-text"><h5>Validation</h5>
|
|
214
|
+
<p>Schema-based request validation & coercion.</p></div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="feature-card">
|
|
217
|
+
<div class="feature-icon">
|
|
218
|
+
<img class="icon-svg" src="/vendor/icons/database.svg" alt="" aria-hidden="true"
|
|
219
|
+
width="48" height="48" />
|
|
220
|
+
</div>
|
|
221
|
+
<div class="feature-text"><h5>ORM & Database</h5>
|
|
222
|
+
<p>Models, query builder, 6 adapters.</p></div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="feature-card">
|
|
225
|
+
<div class="feature-icon">
|
|
226
|
+
<img class="icon-svg" src="/vendor/icons/env.svg" alt="" aria-hidden="true"
|
|
227
|
+
width="48" height="48" />
|
|
228
|
+
</div>
|
|
229
|
+
<div class="feature-text"><h5>Environment Config</h5>
|
|
230
|
+
<p>Typed .env loading with schema validation.</p></div>
|
|
198
231
|
</div>
|
|
199
232
|
</div>
|
|
200
233
|
</div>
|
|
@@ -228,95 +261,11 @@
|
|
|
228
261
|
</div>
|
|
229
262
|
</div>
|
|
230
263
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<p class="muted">Install from npm and create a simple server:</p>
|
|
235
|
-
<pre class="language-bash"><code class="language-bash">npm install zero-http</code></pre>
|
|
236
|
-
|
|
237
|
-
<pre class="language-javascript"><code class="language-javascript">const { createApp, json, static } = require('zero-http')
|
|
238
|
-
const path = require('path')
|
|
239
|
-
const app = createApp()
|
|
240
|
-
|
|
241
|
-
app.use(json({ limit: '10kb' }))
|
|
242
|
-
app.use(static(path.join(__dirname, 'public'), { index: 'index.html' }))
|
|
243
|
-
|
|
244
|
-
app.get('/ping', (req, res) => res.json({ pong: true }))
|
|
245
|
-
|
|
246
|
-
app.listen(3000, () => {
|
|
247
|
-
console.log('Server listening on http://localhost:3000')
|
|
248
|
-
})
|
|
249
|
-
</code></pre>
|
|
250
|
-
|
|
251
|
-
<p class="muted">To run this demo server from the repo root and open
|
|
252
|
-
<code>http://localhost:3000</code>.
|
|
253
|
-
</p>
|
|
254
|
-
<pre class="language-bash"><code class="language-bash">npm install zero-http
|
|
255
|
-
npm run docs
|
|
256
|
-
# open http://localhost:3000
|
|
257
|
-
</code></pre>
|
|
258
|
-
|
|
259
|
-
<h4>API, Options & Examples</h4>
|
|
260
|
-
|
|
261
|
-
<div class="muted">Comprehensive reference for configuration options, runtime behavior, and API
|
|
262
|
-
examples. Expand each section for details and runnable snippets.</div>
|
|
263
|
-
|
|
264
|
-
<details class="acc" id="options">
|
|
265
|
-
<summary>Options (middleware & parser settings)</summary>
|
|
266
|
-
<div class="acc-body">
|
|
267
|
-
<p class="muted">Common option shapes and defaults for parsers and upload middleware.</p>
|
|
268
|
-
<div id="options-items">Loading options…</div>
|
|
269
|
-
</div>
|
|
270
|
-
</details>
|
|
271
|
-
|
|
272
|
-
<details class="acc" id="api-reference">
|
|
273
|
-
<summary>API Reference</summary>
|
|
274
|
-
<div class="acc-body">
|
|
275
|
-
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
|
|
276
|
-
<input id="api-search" placeholder="Search API (name, description)"
|
|
277
|
-
style="flex:1;padding:8px;border-radius:8px;border:1px solid rgba(0,0,0,0.06);background:transparent;color:inherit" />
|
|
278
|
-
<button id="api-clear" class="btn small" type="button">Clear</button>
|
|
279
|
-
</div>
|
|
280
|
-
<div id="api-items">Loading API reference…</div>
|
|
281
|
-
</div>
|
|
282
|
-
</details>
|
|
283
|
-
|
|
284
|
-
<details class="acc" id="simple-examples">
|
|
285
|
-
<summary>Simple Examples</summary>
|
|
286
|
-
<div class="acc-body">
|
|
287
|
-
<p class="muted">Snippets showing how to compose middleware for common
|
|
288
|
-
tasks.</p>
|
|
289
|
-
<div id="examples-items">Loading examples…</div>
|
|
290
|
-
</div>
|
|
291
|
-
</details>
|
|
292
|
-
|
|
293
|
-
<details class="acc">
|
|
294
|
-
<summary>Installation & tests</summary>
|
|
295
|
-
<div class="acc-body">
|
|
296
|
-
|
|
297
|
-
<pre class="language-bash"><code class="language-shell">#clone and install dependencies
|
|
298
|
-
git clone https://github.com/tonywied17/zero-http-npm.git
|
|
299
|
-
npm install
|
|
300
|
-
|
|
301
|
-
# run demo server from repo root
|
|
302
|
-
npm run docs
|
|
303
|
-
|
|
304
|
-
# or directly:
|
|
305
|
-
node documentation/full-server.js
|
|
306
|
-
|
|
307
|
-
# run tests (project-specific)
|
|
308
|
-
node test/test.js
|
|
309
|
-
</code></pre>
|
|
310
|
-
|
|
311
|
-
</div>
|
|
312
|
-
</details>
|
|
313
|
-
|
|
314
|
-
<h4>Extending</h4>
|
|
315
|
-
<p>Use the provided middlewares and routing helpers to build controllers, add auth, or swap in
|
|
316
|
-
different storage backends. The multipart parser exposes file streams and writes each part to
|
|
317
|
-
disk so you can replace disk writes with S3 streams if desired.</p>
|
|
318
|
-
|
|
264
|
+
<!-- Dynamic documentation sections rendered by data-sections.js -->
|
|
265
|
+
<div id="doc-sections" class="doc-sections-container">
|
|
266
|
+
<p class="muted" style="padding:20px">Loading documentation…</p>
|
|
319
267
|
</div>
|
|
268
|
+
|
|
320
269
|
</div>
|
|
321
270
|
|
|
322
271
|
<div class="section-divider constrained" id="playground">
|
|
@@ -489,6 +438,96 @@ node test/test.js
|
|
|
489
438
|
</div>
|
|
490
439
|
</div>
|
|
491
440
|
|
|
441
|
+
<div class="card play-card constrained">
|
|
442
|
+
<div class="card-head">
|
|
443
|
+
<h3 id="orm-crud">ORM Task Manager</h3>
|
|
444
|
+
<div class="small muted">Full CRUD with the built-in ORM — in-memory Task model with search, filters, scopes, soft-delete & restore.</div>
|
|
445
|
+
</div>
|
|
446
|
+
<div class="card-body">
|
|
447
|
+
<div class="orm-stats" id="ormStats"></div>
|
|
448
|
+
<div class="orm-toolbar">
|
|
449
|
+
<form id="taskForm" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
|
|
450
|
+
<label style="flex:2;min-width:160px">Title
|
|
451
|
+
<input id="taskTitle" class="textInput" placeholder="Buy groceries…" required />
|
|
452
|
+
</label>
|
|
453
|
+
<label>Status
|
|
454
|
+
<select id="taskStatus" class="textInput">
|
|
455
|
+
<option value="pending">Pending</option>
|
|
456
|
+
<option value="in-progress">In-progress</option>
|
|
457
|
+
<option value="done">Done</option>
|
|
458
|
+
</select>
|
|
459
|
+
</label>
|
|
460
|
+
<label>Priority
|
|
461
|
+
<select id="taskPriority" class="textInput">
|
|
462
|
+
<option value="0">0</option>
|
|
463
|
+
<option value="1">1</option>
|
|
464
|
+
<option value="2">2</option>
|
|
465
|
+
<option value="3">3</option>
|
|
466
|
+
<option value="4">4</option>
|
|
467
|
+
<option value="5">5</option>
|
|
468
|
+
</select>
|
|
469
|
+
</label>
|
|
470
|
+
<button type="submit" class="btn primary">Add Task</button>
|
|
471
|
+
</form>
|
|
472
|
+
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;align-items:end">
|
|
473
|
+
<label style="flex:1;min-width:120px">Search
|
|
474
|
+
<input id="taskSearch" class="textInput" placeholder="Filter by title…" />
|
|
475
|
+
</label>
|
|
476
|
+
<label>Scope
|
|
477
|
+
<select id="taskScope" class="textInput">
|
|
478
|
+
<option value="">All</option>
|
|
479
|
+
<option value="active">Active</option>
|
|
480
|
+
<option value="done">Done</option>
|
|
481
|
+
<option value="highPriority">High Priority (≥3)</option>
|
|
482
|
+
</select>
|
|
483
|
+
</label>
|
|
484
|
+
<button id="taskDeleteAll" type="button" class="btn warn">Delete All</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
<div id="taskList" class="result mono" style="min-height:100px;margin-top:12px;max-height:380px;overflow-y:auto"></div>
|
|
488
|
+
<pre id="taskResult" class="result mono" style="margin-top:8px;max-height:260px;overflow-y:auto"></pre>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<div class="card play-card constrained">
|
|
493
|
+
<div class="card-head">
|
|
494
|
+
<h3 id="cookie-explorer">Cookie Explorer</h3>
|
|
495
|
+
<div class="small muted">Set, inspect, and delete cookies in real time using the built-in cookieParser middleware.</div>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="card-body">
|
|
498
|
+
<form id="cookieForm" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
|
|
499
|
+
<label style="flex:1;min-width:100px">Name
|
|
500
|
+
<input id="cookieName" class="textInput" placeholder="theme" required />
|
|
501
|
+
</label>
|
|
502
|
+
<label style="flex:1;min-width:100px">Value
|
|
503
|
+
<input id="cookieValue" class="textInput" placeholder="dark" />
|
|
504
|
+
</label>
|
|
505
|
+
<label>HttpOnly
|
|
506
|
+
<select id="cookieHttpOnly" class="textInput">
|
|
507
|
+
<option value="false">No</option>
|
|
508
|
+
<option value="true">Yes</option>
|
|
509
|
+
</select>
|
|
510
|
+
</label>
|
|
511
|
+
<label>SameSite
|
|
512
|
+
<select id="cookieSameSite" class="textInput">
|
|
513
|
+
<option value="Lax">Lax</option>
|
|
514
|
+
<option value="Strict">Strict</option>
|
|
515
|
+
<option value="None">None</option>
|
|
516
|
+
</select>
|
|
517
|
+
</label>
|
|
518
|
+
<label>Max-Age (s)
|
|
519
|
+
<input id="cookieMaxAge" class="textInput" type="number" placeholder="3600" style="width:90px" />
|
|
520
|
+
</label>
|
|
521
|
+
<button type="submit" class="btn primary">Set Cookie</button>
|
|
522
|
+
</form>
|
|
523
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
524
|
+
<button id="cookieRefresh" type="button" class="btn">Refresh Cookies</button>
|
|
525
|
+
</div>
|
|
526
|
+
<div id="cookieJar" class="result mono" style="min-height:80px;margin-top:12px;max-height:320px;overflow-y:auto"></div>
|
|
527
|
+
<pre id="cookieResult" class="result mono" style="margin-top:8px"></pre>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
492
531
|
<div class="card footnote" style="max-width:1300px;margin:18px auto;">
|
|
493
532
|
<p class="muted">This demo doubles as a readable API reference and playground — try uploading files,
|
|
494
533
|
inspect the returned JSON, and use the playground to test parsers.</p>
|