dx-server 0.13.0-alpha.1 → 0.13.0-alpha.2

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 (46) hide show
  1. package/README.md +397 -265
  2. package/lib/body.js +1 -1
  3. package/lib/body.js.map +1 -0
  4. package/lib/bodyHelpers.js +22 -9
  5. package/lib/bodyHelpers.js.map +1 -0
  6. package/lib/dx.d.ts +1 -1
  7. package/lib/dx.js +12 -6
  8. package/lib/dx.js.map +1 -0
  9. package/lib/dxHelpers.d.ts +2 -1
  10. package/lib/dxHelpers.js +105 -87
  11. package/lib/dxHelpers.js.map +1 -0
  12. package/lib/helpers.js.map +1 -0
  13. package/lib/index.d.ts +1 -1
  14. package/lib/index.js +1 -1
  15. package/lib/index.js.map +1 -0
  16. package/lib/logger.d.ts +3 -2
  17. package/lib/logger.js +54 -46
  18. package/lib/logger.js.map +1 -0
  19. package/lib/router.js +5 -5
  20. package/lib/router.js.map +1 -0
  21. package/lib/static.js +4 -3
  22. package/lib/static.js.map +1 -0
  23. package/lib/staticHelpers.d.ts +5 -1
  24. package/lib/staticHelpers.js +150 -134
  25. package/lib/staticHelpers.js.map +1 -0
  26. package/lib/stream.d.ts +1 -1
  27. package/lib/stream.js +11 -5
  28. package/lib/stream.js.map +1 -0
  29. package/lib/vendors/contentType.js +7 -30
  30. package/lib/vendors/contentType.js.map +1 -0
  31. package/lib/vendors/etag.d.ts +2 -2
  32. package/lib/vendors/etag.js +15 -25
  33. package/lib/vendors/etag.js.map +1 -0
  34. package/lib/vendors/fresh.js +10 -17
  35. package/lib/vendors/fresh.js.map +1 -0
  36. package/lib/vendors/mime.js +4 -4
  37. package/lib/vendors/mime.js.map +1 -0
  38. package/lib/vendors/mimeDb.d.ts +2544 -2544
  39. package/lib/vendors/mimeDb.js +7100 -7079
  40. package/lib/vendors/mimeDb.js.map +1 -0
  41. package/lib/vendors/mimeScore.js +10 -11
  42. package/lib/vendors/mimeScore.js.map +1 -0
  43. package/lib/vendors/rangeParser.d.ts +2 -10
  44. package/lib/vendors/rangeParser.js +16 -29
  45. package/lib/vendors/rangeParser.js.map +1 -0
  46. package/package.json +32 -27
package/README.md CHANGED
@@ -5,6 +5,13 @@ A modern, unopinionated, and performant Node.js server framework built on AsyncL
5
5
  [![npm version](https://img.shields.io/npm/v/dx-server.svg)](https://www.npmjs.com/package/dx-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ## Important caveats
9
+
10
+ - **ETag is supported.** Static file serving defaults to a weak (mtime/size-based) ETag, while other responses (`setJson`, `setHtml`, `setText`, `setBuffer`, …) use a strong (content-based) ETag. Redirects get no ETag.
11
+ - **Request compression is supported.** Request bodies with `Content-Encoding: gzip`, `deflate`, or `br` are decompressed automatically.
12
+ - **Response compression is NOT supported.** dx-server never sets `Content-Encoding` on responses — handle response compression at the reverse proxy / CDN level.
13
+ - **Static file serving supports Range requests.**
14
+
8
15
  ## Features
9
16
 
10
17
  - 🎯 **Elegant API interface** - No need to pass req/res objects through middleware chains
@@ -46,11 +53,10 @@ To check if your runtime supports URLPattern natively:
46
53
 
47
54
  ```javascript
48
55
  if (typeof URLPattern === 'undefined') {
49
- console.log('URLPattern not supported, polyfill required')
56
+ console.log('URLPattern not supported, polyfill required')
50
57
  }
51
58
  ```
52
59
 
53
-
54
60
  ## Quick Start
55
61
 
56
62
  ### Basic Server
@@ -60,54 +66,60 @@ import {Server} from 'node:http'
60
66
  import chain from 'jchain'
61
67
  import dxServer, {getReq, getRes, router, setHtml, setText} from 'dx-server'
62
68
 
63
- new Server().on('request', (req, res) => chain(
64
- dxServer(req, res),
65
- async next => {
66
- try {
67
- // Access req/res from anywhere - no prop drilling!
68
- getRes().setHeader('Cache-Control', 'no-cache')
69
- console.log(getReq().method, getReq().url)
70
- await next()
71
- } catch (e) {
72
- console.error(e)
73
- setHtml('internal server error', {status: 500})
74
- }
75
- },
76
- router.get({
77
- '/'() {setHtml('hello world')},
78
- '/health'() {setText('ok')}
79
- }),
80
- () => setHtml('not found', {status: 404}),
81
- )()).listen(3000, () => console.log('server is listening at 3000'))
69
+ new Server()
70
+ .on('request', (req, res) =>
71
+ chain(
72
+ dxServer(req, res),
73
+ async next => {
74
+ try {
75
+ // Access req/res from anywhere - no prop drilling!
76
+ getRes().setHeader('Cache-Control', 'no-cache')
77
+ console.log(getReq().method, getReq().url)
78
+ await next()
79
+ } catch (e) {
80
+ console.error(e)
81
+ setHtml('internal server error', {status: 500})
82
+ }
83
+ },
84
+ router.get({
85
+ '/'() {
86
+ setHtml('hello world')
87
+ },
88
+ '/health'() {
89
+ setText('ok')
90
+ },
91
+ }),
92
+ () => setHtml('not found', {status: 404}),
93
+ )(),
94
+ )
95
+ .listen(3000, () => console.log('server is listening at 3000'))
82
96
  ```
83
97
 
84
- ### TypeScript Example
98
+ ### Custom Contexts
85
99
 
86
- ```typescript
87
- import {Server} from 'node:http'
88
- import chain from 'jchain'
89
- import dxServer, {router, setJson, getJson} from 'dx-server'
100
+ Create reusable context objects with `makeDxContext`:
90
101
 
91
- interface User {
92
- id: number
93
- name: string
94
- }
102
+ ```javascript
103
+ import {makeDxContext, getReq} from 'dx-server'
104
+
105
+ // Create auth context
106
+ const authContext = makeDxContext(async () => {
107
+ const token = getReq().headers.authorization
108
+ if (!token) return null
109
+ return await validateToken(token) // Your validation logic
110
+ })
95
111
 
96
- new Server().on('request', (req, res) => chain(
97
- dxServer(req, res),
98
- router.post({
99
- async '/api/users'() {
100
- const body = await getJson<{name: string}>()
101
- if (!body?.name) {
102
- setJson({error: 'Name required'}, {status: 400})
103
- return
104
- }
105
- const user: User = {id: 1, name: body.name}
106
- setJson(user, {status: 201})
107
- }
108
- }),
109
- () => setJson({error: 'Not found'}, {status: 404})
110
- )()).listen(3000)
112
+ // Use in middleware
113
+ chain(
114
+ authContext.chain(), // Initialize for all requests
115
+ next => {
116
+ if (!authContext.value) {
117
+ setJson({error: 'Unauthorized'}, {status: 401})
118
+ return
119
+ }
120
+ return next()
121
+ },
122
+ )
111
123
  ```
112
124
 
113
125
  ### Static File Server
@@ -118,13 +130,17 @@ import chain from 'jchain'
118
130
  import dxServer, {chainStatic, setHtml} from 'dx-server'
119
131
  import {resolve} from 'node:path'
120
132
 
121
- new Server().on('request', (req, res) => chain(
122
- dxServer(req, res),
123
- chainStatic('/*', {
124
- root: resolve(import.meta.dirname, 'public'),
125
- }),
126
- () => setHtml('not found', {status: 404}),
127
- )()).listen(3000)
133
+ new Server()
134
+ .on('request', (req, res) =>
135
+ chain(
136
+ dxServer(req, res),
137
+ chainStatic('/*', {
138
+ root: resolve(import.meta.dirname, 'public'),
139
+ }),
140
+ () => setHtml('not found', {status: 404}),
141
+ )(),
142
+ )
143
+ .listen(3000)
128
144
  ```
129
145
 
130
146
  ### Production-Ready Server
@@ -134,89 +150,124 @@ import {Server} from 'node:http'
134
150
  import {promisify} from 'node:util'
135
151
  import chain from 'jchain'
136
152
  import dxServer, {
137
- getReq, getRes,
138
- getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
139
- setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
140
- router, chainStatic, makeDxContext
153
+ getReq,
154
+ getRes,
155
+ getBuffer,
156
+ getJson,
157
+ getRaw,
158
+ getText,
159
+ getUrlEncoded,
160
+ getQuery,
161
+ setHtml,
162
+ setJson,
163
+ setText,
164
+ setEmpty,
165
+ setBuffer,
166
+ setRedirect,
167
+ setNodeStream,
168
+ setWebStream,
169
+ setFile,
170
+ router,
171
+ chainStatic,
172
+ makeDxContext,
141
173
  } from 'dx-server'
142
174
  import {resolve} from 'node:path'
143
175
 
144
176
  // it is best practice to create custom error class for non-system error
145
177
  class ServerError extends Error {
146
- name = 'ServerError'
178
+ name = 'ServerError'
147
179
 
148
- constructor(message, status = 400, code = 'unknown') {
149
- super(message)
150
- this.status = status
151
- this.code = code
152
- }
180
+ constructor(message, status = 400, code = 'unknown') {
181
+ super(message)
182
+ this.status = status
183
+ this.code = code
184
+ }
153
185
  }
154
186
 
155
187
  const authContext = makeDxContext(async () => {
156
- if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
188
+ if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
157
189
  })
158
190
 
159
191
  function requireAuth() {
160
- if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
192
+ if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
161
193
  }
162
194
 
163
195
  const serverChain = chain(
164
- next => {
165
- // req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
166
- getRes().setHeader('Cache-Control', 'no-cache')
167
- return next() // must return or await
168
- },
169
- async next => {// global error catching for all following middlewares
170
- try {
171
- await next()
172
- } catch (e) {// only app error message should be shown to user
173
- if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
174
- else {// report system error
175
- console.error(e)
176
- setHtml('internal server error (code: internal)', {status: 500})
177
- }
178
- }
179
- },
180
- chainStatic('/public/*', {root: resolve(import.meta.dirname, 'public')}),
181
- authContext.chain(), // chain context will set the context value to authContext.value in every request
182
- router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
183
- try {
184
- await next()
185
- } catch (e) {
186
- if (e instanceof ServerError) setJson({// only app error message should be shown to user
187
- error: e.message,
188
- code: e.code,
189
- }, {status: e.status})
190
- else {// report system error
191
- console.error(e)
192
- setJson({
193
- message: 'internal server error',
194
- code: 'internal'
195
- }, {status: 500})
196
- }
197
- }
198
- }),
199
- router.post({
200
- '/api/sample-public-api'() { // sample POST router
201
- setJson({name: 'joe'})
202
- },
203
- '/api/me'() { // sample private router
204
- requireAuth()
205
- setJson({name: authContext.value.name})
206
- },
207
- }),
208
- router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
209
- router.get('/health', () => setText('ok')),
210
- () => { // not found router
211
- throw new ServerError('Not found', 404, 'NOT_FOUND')
212
- },
196
+ next => {
197
+ // req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
198
+ getRes().setHeader('Cache-Control', 'no-cache')
199
+ return next() // must return or await
200
+ },
201
+ async next => {
202
+ // global error catching for all following middlewares
203
+ try {
204
+ await next()
205
+ } catch (e) {
206
+ // only app error message should be shown to user
207
+ if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
208
+ else {
209
+ // report system error
210
+ console.error(e)
211
+ setHtml('internal server error (code: internal)', {status: 500})
212
+ }
213
+ }
214
+ },
215
+ // pattern '/public/*' maps a request '/public/x.js' to '<root>/public/x.js', so root is the
216
+ // directory that *contains* the public folder (not the public folder itself).
217
+ chainStatic('/public/*', {root: import.meta.dirname}),
218
+ authContext.chain(), // chain context will set the context value to authContext.value in every request
219
+ router.post('/api/*', async ({next}) => {
220
+ // example of catching error for all /api/* routes
221
+ try {
222
+ await next()
223
+ } catch (e) {
224
+ if (e instanceof ServerError)
225
+ setJson(
226
+ {
227
+ // only app error message should be shown to user
228
+ error: e.message,
229
+ code: e.code,
230
+ },
231
+ {status: e.status},
232
+ )
233
+ else {
234
+ // report system error
235
+ console.error(e)
236
+ setJson(
237
+ {
238
+ message: 'internal server error',
239
+ code: 'internal',
240
+ },
241
+ {status: 500},
242
+ )
243
+ }
244
+ }
245
+ }),
246
+ router.post({
247
+ '/api/sample-public-api'() {
248
+ // sample POST router
249
+ setJson({name: 'joe'})
250
+ },
251
+ '/api/me'() {
252
+ // sample private router
253
+ requireAuth()
254
+ setJson({name: authContext.value.name})
255
+ },
256
+ }),
257
+ router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
258
+ router.get('/health', () => setText('ok')),
259
+ () => {
260
+ // not found router
261
+ throw new ServerError('Not found', 404, 'NOT_FOUND')
262
+ },
213
263
  )
214
264
 
215
- const tcpServer = new Server()
216
- .on('request', (req, res) => chain(
217
- dxServer(req, res), // basic dx-server context
218
- serverChain,
219
- )())
265
+ const tcpServer = new Server().on('request', (req, res) =>
266
+ chain(
267
+ dxServer(req, res), // basic dx-server context
268
+ serverChain,
269
+ )(),
270
+ )
220
271
 
221
272
  await promisify(tcpServer.listen.bind(tcpServer))(3000)
222
273
  console.log('server is listening at 3000')
@@ -233,9 +284,9 @@ dx-server uses Node.js AsyncLocalStorage to provide request/response context glo
233
284
  import {getReq, getRes} from 'dx-server'
234
285
 
235
286
  function someDeepFunction() {
236
- const req = getReq() // No need to pass req through multiple layers
237
- const res = getRes()
238
- res.setHeader('X-Custom', 'value')
287
+ const req = getReq() // No need to pass req through multiple layers
288
+ const res = getRes()
289
+ res.setHeader('X-Custom', 'value')
239
290
  }
240
291
  ```
241
292
 
@@ -252,11 +303,11 @@ const text = await getText()
252
303
 
253
304
  // Sync usage (requires chaining)
254
305
  chain(
255
- getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
256
- next => {
257
- console.log(getJson.value) // Access synchronously
258
- return next()
259
- }
306
+ getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
307
+ next => {
308
+ console.log(getJson.value) // Access synchronously
309
+ return next()
310
+ },
260
311
  )
261
312
  ```
262
313
 
@@ -269,23 +320,22 @@ import {makeDxContext, getReq} from 'dx-server'
269
320
 
270
321
  // Create auth context
271
322
  const authContext = makeDxContext(async () => {
272
- const token = getReq().headers.authorization
273
- if (!token) return null
274
- return await validateToken(token) // Your validation logic
323
+ const token = getReq().headers.authorization
324
+ if (!token) return null
325
+ return await validateToken(token) // Your validation logic
275
326
  })
276
327
 
277
328
  // Use in middleware
278
329
  chain(
279
- authContext.chain(), // Initialize for all requests
280
- next => {
281
- if (!authContext.value) {
282
- setJson({error: 'Unauthorized'}, {status: 401})
283
- return
284
- }
285
- return next()
286
- }
330
+ authContext.chain(), // Initialize for all requests
331
+ next => {
332
+ if (!authContext.value) {
333
+ setJson({error: 'Unauthorized'}, {status: 401})
334
+ return
335
+ }
336
+ return next()
337
+ },
287
338
  )
288
-
289
339
  ```
290
340
 
291
341
  ## API Reference
@@ -294,38 +344,60 @@ chain(
294
344
 
295
345
  ```javascript
296
346
  import dxServer, {
297
- // Request/Response access
298
- getReq, getRes,
299
-
300
- // Request body parsers
301
- getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
302
-
303
- // Response setters
304
- setHtml, setJson, setText, setEmpty, setBuffer, setRedirect,
305
- setNodeStream, setWebStream, setFile,
306
-
307
- // Utilities
308
- router, chainStatic, makeDxContext,
309
-
310
- // Logging
311
- logger, logJson,
347
+ // Request/Response access
348
+ getReq,
349
+ getRes,
350
+
351
+ // Request body parsers
352
+ getBuffer,
353
+ getJson,
354
+ getRaw,
355
+ getText,
356
+ getUrlEncoded,
357
+ getQuery,
358
+
359
+ // Response setters
360
+ setHtml,
361
+ setJson,
362
+ setText,
363
+ setEmpty,
364
+ setBuffer,
365
+ setRedirect,
366
+ setNodeStream,
367
+ setWebStream,
368
+ setFile,
369
+
370
+ // Utilities
371
+ router,
372
+ chainStatic,
373
+ makeDxContext,
374
+
375
+ // Logging
376
+ logger,
377
+ logJson,
312
378
  } from 'dx-server'
313
379
 
314
380
  // Low-level helpers
315
381
  import {
316
- setBufferBodyDefaultOptions,
317
- bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
318
- urlEncodedFromReq, queryFromReq,
382
+ setBufferBodyDefaultOptions,
383
+ bufferFromReq,
384
+ jsonFromReq,
385
+ rawFromReq,
386
+ textFromReq,
387
+ urlEncodedFromReq,
388
+ queryFromReq,
319
389
  } from 'dx-server/helpers'
320
390
  ```
321
391
 
322
392
  ### Core Functions
323
393
 
324
394
  #### Request/Response Access
395
+
325
396
  - **`getReq()`** - Get the current request object
326
397
  - **`getRes()`** - Get the current response object
327
398
 
328
399
  #### Body Parsers
400
+
329
401
  All body parsers are async, lazy-loaded, and cached per request:
330
402
 
331
403
  - **`getJson(options?)`** - Parse JSON body (requires `Content-Type: application/json`)
@@ -336,6 +408,7 @@ All body parsers are async, lazy-loaded, and cached per request:
336
408
  - **`getQuery(options?)`** - Parse query string parameters
337
409
 
338
410
  Options:
411
+
339
412
  ```typescript
340
413
  {
341
414
  bodyLimit?: number // Max body size in bytes (default: 100KB)
@@ -345,44 +418,70 @@ Options:
345
418
  ```
346
419
 
347
420
  #### Response Setters
348
- - **`setJson(data, {status?, headers?})`** - Send JSON response
349
- - **`setHtml(html, {status?, headers?})`** - Send HTML response
350
- - **`setText(text, {status?, headers?})`** - Send plain text
351
- - **`setBuffer(buffer, {status?, headers?})`** - Send buffer
352
- - **`setFile(path, options?)`** - Send file
353
- - **`setNodeStream(stream, {status?, headers?})`** - Send Node.js stream
354
- - **`setWebStream(stream, {status?, headers?})`** - Send Web stream
355
- - **`setRedirect(url, {status?, headers?})`** - Redirect response
356
- - **`setEmpty({status?, headers?})`** - Send empty response
421
+
422
+ Setters only take a `{status?}` option (except `setRedirect`/`setFile`). To set response
423
+ headers, use `getRes().setHeader(name, value)` before (or after) calling a setter.
424
+
425
+ - **`setJson(data, {status?})`** - Send JSON response (`application/json; charset=utf-8`)
426
+ - **`setHtml(html, {status?})`** - Send HTML response (`text/html; charset=utf-8`)
427
+ - **`setText(text, {status?})`** - Send plain text (`text/plain; charset=utf-8`)
428
+ - **`setBuffer(buffer, {status?})`** - Send buffer (`application/octet-stream`)
429
+ - **`setFile(path, options?)`** - Send file (see `SendFileOptions`)
430
+ - **`setNodeStream(stream, {status?})`** - Send Node.js stream
431
+ - **`setWebStream(stream, {status?})`** - Send Web stream
432
+ - **`setRedirect(url, status)`** - Redirect response; `status` is `301 | 302` (required, positional)
433
+ - **`setEmpty({status?})`** - Send empty response
357
434
 
358
435
  #### Context Management
436
+
359
437
  - **`makeDxContext(fn)`** - Create a custom context object
438
+
360
439
  ```javascript
361
440
  const ctx = makeDxContext(() => computeValue())
362
-
441
+
363
442
  // Access value
364
- await ctx() // Lazy load
365
- ctx.value // Sync access (after loading)
366
- ctx.get(req) // Get for specific request
367
-
443
+ await ctx() // Lazy load
444
+ ctx.value // Sync access (after loading)
445
+ ctx.get(req) // Get for specific request
446
+
368
447
  // Set value
369
448
  ctx.value = newValue
370
449
  ctx.set(req, newValue)
371
450
  ```
372
451
 
373
452
  #### Middleware Utilities
453
+
374
454
  - **`chainStatic(pattern, options)`** - Serve static files
455
+
375
456
  ```javascript
376
457
  chainStatic('/public/*', {
377
- root: '/path/to/files',
378
- getPathname(matched){return matched.pathname}, // take URLPattern matched object, epects to return the file path
379
- // the returned file path must be run through decodeURIComponent before returning
380
- dotfiles: 'deny',
381
- disableEtag: false,
382
- lastModified: true
458
+ // directory that contains the files. A request '/public/x.js' resolves to '<root>/public/x.js',
459
+ // so root is the parent of the public folder, not the public folder itself.
460
+ root: '/path/to/parent',
461
+ // optional. Receives the URLPattern match and returns the file path to serve. The default
462
+ // serves decodeURIComponent(url.pathname). A custom getPathname must return an
463
+ // already-decoded path. Example: pin a single file regardless of URL:
464
+ // getPathname: () => '/path/to/fixed-file.html'
465
+ getPathname: undefined,
466
+ allowDotfiles: false, // default: dotfiles are denied
467
+ etag: 'weak', // 'weak' (default) | 'strong' | 'disabled'
468
+ disableLastModified: false,
469
+ disableFollowSymlinks: false, // set true to 403 files whose real path escapes root
383
470
  })
384
471
  ```
385
472
 
473
+ - **`logger(log?, options?)`** - Request logging middleware
474
+
475
+ ```javascript
476
+ import {logger, logJson} from 'dx-server'
477
+
478
+ // logs one line on request and one on response completion
479
+ chain(logger()) // defaults: log = logJson (JSON to stdout), timestamps in UTC
480
+
481
+ // custom sink and timezone offset (in hours) for the human-readable timestamp
482
+ chain(logger(logJson, {timezoneOffset: 9})) // +09:00 (JST)
483
+ ```
484
+
386
485
  ### Routing
387
486
 
388
487
  dx-server uses [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for routing, which differs from Express patterns:
@@ -392,15 +491,21 @@ import {router} from 'dx-server'
392
491
 
393
492
  // Single route
394
493
  router.get('/users/:id', ({matched}) => {
395
- const {id} = matched.pathname.groups
396
- setJson({userId: id})
494
+ const {id} = matched.pathname.groups
495
+ setJson({userId: id})
397
496
  })
398
497
 
399
498
  // Multiple routes
400
499
  router.post({
401
- '/api/users'() { /* create user */ },
402
- '/api/users/:id'({matched}) { /* update user */ },
403
- '/api/users/:id/posts'({matched}) { /* get user posts */ }
500
+ '/api/users'() {
501
+ /* create user */
502
+ },
503
+ '/api/users/:id'({matched}) {
504
+ /* update user */
505
+ },
506
+ '/api/users/:id/posts'({matched}) {
507
+ /* get user posts */
508
+ },
404
509
  })
405
510
 
406
511
  // All HTTP methods supported
@@ -411,28 +516,32 @@ router.delete(pattern, handler)
411
516
  router.patch(pattern, handler)
412
517
  router.head(pattern, handler)
413
518
  router.options(pattern, handler)
414
- router.all(pattern, handler) // Any method
519
+ router.all(pattern, handler) // Any method
415
520
 
416
521
  // Custom method
417
522
  router.method('CUSTOM', pattern, handler)
418
523
 
419
524
  // With prefix option
420
- router.get({
421
- '/users': listUsers,
422
- '/users/:id': getUser
423
- }, {prefix: '/api'}) // Routes become /api/users, /api/users/:id
525
+ router.get(
526
+ {
527
+ '/users': listUsers,
528
+ '/users/:id': getUser,
529
+ },
530
+ {prefix: '/api'},
531
+ ) // Routes become /api/users, /api/users/:id
424
532
  ```
425
533
 
426
534
  #### URLPattern vs Express Patterns
427
535
 
428
- | Pattern | URLPattern | Express |
429
- |---------|------------|---------|
430
- | Wildcard | `/api/*` | `/api/*` or `/api/(.*)` |
431
- | Optional trailing slash | `{/}?` | `/path/?` |
432
- | Named params | `/:id` | `/:id` |
433
- | Optional params | `/:id?` | `/:id?` |
536
+ | Pattern | URLPattern | Express |
537
+ | ----------------------- | ---------- | ----------------------- |
538
+ | Wildcard | `/api/*` | `/api/*` or `/api/(.*)` |
539
+ | Optional trailing slash | `{/}?` | `/path/?` |
540
+ | Named params | `/:id` | `/:id` |
541
+ | Optional params | `/:id?` | `/:id?` |
434
542
 
435
543
  **Important differences:**
544
+
436
545
  - `'/foo'` matches `/foo` but NOT `/foo/`
437
546
  - `'/foo/'` matches `/foo/` but NOT `/foo`
438
547
  - Use `'/foo{/}?'` to match both
@@ -452,12 +561,13 @@ import cors from 'cors'
452
561
  // Adapt one Connect/Express-style middleware (or a full Express app, which has the
453
562
  // same (req, res, next) shape) into a jchain step. Always calls next() so the rest of
454
563
  // the chain runs — dx-server handles the case where the response is already ended.
455
- const fromConnect = mw => next => new Promise((resolve, reject) => {
456
- mw(getReq(), getRes(), err => {
457
- if (err) return reject(err)
458
- next().then(resolve, reject)
459
- })
460
- })
564
+ const fromConnect = mw => next =>
565
+ new Promise((resolve, reject) => {
566
+ mw(getReq(), getRes(), err => {
567
+ if (err) return reject(err)
568
+ next().then(resolve, reject)
569
+ })
570
+ })
461
571
 
462
572
  const app = express()
463
573
  app.set('trust proxy', true)
@@ -465,12 +575,12 @@ app.use('/public', express.static('public'))
465
575
  app.get('/legacy', (req, res) => res.json({message: 'Express route'}))
466
576
 
467
577
  chain(
468
- fromConnect(app), // mount the entire Express app first
469
- fromConnect(morgan('common')),
470
- fromConnect(helmet()),
471
- fromConnect(cors()),
472
- // dx-server routes continue here
473
- router.get('/', () => setHtml('ok')),
578
+ fromConnect(app), // mount the entire Express app first
579
+ fromConnect(morgan('common')),
580
+ fromConnect(helmet()),
581
+ fromConnect(cors()),
582
+ // dx-server routes continue here
583
+ router.get('/', () => setHtml('ok')),
474
584
  )
475
585
  ```
476
586
 
@@ -480,15 +590,21 @@ Pure functions for custom implementations:
480
590
 
481
591
  ```javascript
482
592
  import {
483
- setBufferBodyDefaultOptions,
484
- bufferFromReq, jsonFromReq, rawFromReq,
485
- textFromReq, urlEncodedFromReq, queryFromReq
593
+ setBufferBodyDefaultOptions,
594
+ bufferFromReq,
595
+ jsonFromReq,
596
+ rawFromReq,
597
+ textFromReq,
598
+ urlEncodedFromReq,
599
+ queryFromReq,
486
600
  } from 'dx-server/helpers'
487
601
 
488
602
  // Set global defaults
489
603
  setBufferBodyDefaultOptions({
490
- bodyLimit: 10 * 1024 * 1024, // 10MB
491
- queryParser(search){return myCustomParser(search)}
604
+ bodyLimit: 10 * 1024 * 1024, // 10MB
605
+ queryParser(search) {
606
+ return myCustomParser(search)
607
+ },
492
608
  })
493
609
 
494
610
  // Use directly with req/res (no context required)
@@ -499,61 +615,70 @@ const query = queryFromReq(req)
499
615
  ## Security Considerations
500
616
 
501
617
  ### Body Size Limits
618
+
502
619
  Always set appropriate body size limits to prevent DoS attacks:
503
620
 
504
621
  ```javascript
622
+ import {setBufferBodyDefaultOptions} from 'dx-server/helpers'
623
+
624
+ // per-parse limit
505
625
  chain(
506
- getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
507
- // or globally:
508
- dxServer(req, res, {bodyLimit: 5 * 1024 * 1024}) // 5MB
626
+ getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
509
627
  )
628
+
629
+ // or globally, once at startup (default is 100KB):
630
+ setBufferBodyDefaultOptions({bodyLimit: 5 * 1024 * 1024}) // 5MB
510
631
  ```
511
632
 
633
+ Bodies that exceed the limit reject with an error carrying `statusCode: 413`; a body shorter than its
634
+ `Content-Length` rejects with `statusCode: 400`. Map these in your error middleware.
635
+
512
636
  ### Error Handling
637
+
513
638
  Never expose internal errors to clients:
514
639
 
515
640
  ```javascript
516
641
  class AppError extends Error {
517
- constructor(message, status = 400, code = 'ERROR') {
518
- super(message)
519
- this.status = status
520
- this.code = code
521
- }
642
+ constructor(message, status = 400, code = 'ERROR') {
643
+ super(message)
644
+ this.status = status
645
+ this.code = code
646
+ }
522
647
  }
523
648
 
524
- chain(
525
- async next => {
526
- try {
527
- await next()
528
- } catch (error) {
529
- if (error instanceof AppError) {
530
- setJson({error: error.message, code: error.code}, {status: error.status})
531
- } else {
532
- console.error(error) // Log for debugging
533
- setJson({error: 'Internal server error'}, {status: 500})
534
- }
535
- }
536
- }
537
- )
649
+ chain(async next => {
650
+ try {
651
+ await next()
652
+ } catch (error) {
653
+ if (error instanceof AppError) {
654
+ setJson({error: error.message, code: error.code}, {status: error.status})
655
+ } else {
656
+ console.error(error) // Log for debugging
657
+ setJson({error: 'Internal server error'}, {status: 500})
658
+ }
659
+ }
660
+ })
538
661
  ```
539
662
 
540
663
  ### Input Validation
664
+
541
665
  Always validate input data:
542
666
 
543
667
  ```javascript
544
668
  router.post('/api/users', async () => {
545
- const data = await getJson()
546
-
547
- // Validate
548
- if (!data?.email || !isValidEmail(data.email)) {
549
- throw new AppError('Invalid email', 400, 'INVALID_EMAIL')
550
- }
551
-
552
- // Process...
669
+ const data = await getJson()
670
+
671
+ // Validate
672
+ if (!data?.email || !isValidEmail(data.email)) {
673
+ throw new AppError('Invalid email', 400, 'INVALID_EMAIL')
674
+ }
675
+
676
+ // Process...
553
677
  })
554
678
  ```
555
679
 
556
680
  ### Security Headers
681
+
557
682
  Use security middleware via the `fromConnect` adapter shown in [Express Integration](#express-integration):
558
683
 
559
684
  ```javascript
@@ -561,58 +686,65 @@ import helmet from 'helmet'
561
686
  import cors from 'cors'
562
687
 
563
688
  chain(
564
- fromConnect(helmet()),
565
- fromConnect(cors({
566
- origin: process.env.ALLOWED_ORIGINS?.split(','),
567
- credentials: true
568
- })),
689
+ fromConnect(helmet()),
690
+ fromConnect(
691
+ cors({
692
+ origin: process.env.ALLOWED_ORIGINS?.split(','),
693
+ credentials: true,
694
+ }),
695
+ ),
569
696
  )
570
697
  ```
571
698
 
572
699
  ## Advanced Examples
573
700
 
574
701
  ### File Upload with Busboy
702
+
575
703
  ```javascript
576
704
  import busboy from 'busboy'
577
705
 
578
706
  router.post('/upload', () => {
579
- const req = getReq()
580
- const bb = busboy({headers: req.headers, limits: {fileSize: 10 * 1024 * 1024}})
581
-
582
- bb.on('file', (name, file, info) => {
583
- // Handle file stream
584
- })
585
-
586
- req.pipe(bb)
707
+ const req = getReq()
708
+ const bb = busboy({headers: req.headers, limits: {fileSize: 10 * 1024 * 1024}})
709
+
710
+ bb.on('file', (name, file, info) => {
711
+ // Handle file stream
712
+ })
713
+
714
+ req.pipe(bb)
587
715
  })
588
716
  ```
589
717
 
590
718
  ### WebSocket Upgrade
719
+
591
720
  ```javascript
592
721
  import {WebSocketServer} from 'ws'
593
722
 
594
723
  const wss = new WebSocketServer({noServer: true})
595
724
 
596
725
  server.on('upgrade', (request, socket, head) => {
597
- if (request.url === '/ws') {
598
- wss.handleUpgrade(request, socket, head, ws => {
599
- wss.emit('connection', ws, request)
600
- })
601
- }
726
+ if (request.url === '/ws') {
727
+ wss.handleUpgrade(request, socket, head, ws => {
728
+ wss.emit('connection', ws, request)
729
+ })
730
+ }
602
731
  })
603
732
  ```
604
733
 
605
734
  ### Rate Limiting
735
+
606
736
  Using the `fromConnect` adapter from [Express Integration](#express-integration):
607
737
 
608
738
  ```javascript
609
739
  import rateLimit from 'express-rate-limit'
610
740
 
611
741
  chain(
612
- fromConnect(rateLimit({
613
- windowMs: 15 * 60 * 1000, // 15 minutes
614
- max: 100 // limit each IP to 100 requests per windowMs
615
- })),
742
+ fromConnect(
743
+ rateLimit({
744
+ windowMs: 15 * 60 * 1000, // 15 minutes
745
+ max: 100, // limit each IP to 100 requests per windowMs
746
+ }),
747
+ ),
616
748
  )
617
749
  ```
618
750
 
@@ -633,14 +765,14 @@ chain(
633
765
  ```javascript
634
766
  // Express
635
767
  app.get('/users/:id', (req, res) => {
636
- const {id} = req.params
637
- res.json({userId: id})
768
+ const {id} = req.params
769
+ res.json({userId: id})
638
770
  })
639
771
 
640
772
  // dx-server
641
773
  router.get('/users/:id', ({matched}) => {
642
- const {id} = matched.pathname.groups
643
- setJson({userId: id})
774
+ const {id} = matched.pathname.groups
775
+ setJson({userId: id})
644
776
  })
645
777
  ```
646
778