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