dx-server 0.8.4 → 0.10.0

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 (87) hide show
  1. package/README.md +557 -245
  2. package/cjs/body.d.ts +7 -7
  3. package/cjs/body.js +8 -8
  4. package/cjs/bodyHelpers.js +6 -6
  5. package/cjs/connect.d.ts +2 -2
  6. package/cjs/connect.js +5 -5
  7. package/cjs/dx.d.ts +1 -1
  8. package/cjs/dx.js +24 -15
  9. package/cjs/dxHelpers.d.ts +2 -2
  10. package/cjs/dxHelpers.js +12 -7
  11. package/cjs/express.d.ts +1 -1
  12. package/cjs/express.js +16 -14
  13. package/cjs/helpers.d.ts +2 -2
  14. package/cjs/helpers.js +10 -10
  15. package/cjs/index.d.ts +6 -6
  16. package/cjs/index.js +28 -28
  17. package/cjs/polyfillWithResolvers.js +2 -3
  18. package/cjs/router.js +7 -7
  19. package/cjs/static.d.ts +3 -3
  20. package/cjs/static.js +12 -12
  21. package/cjs/staticHelpers.d.ts +16 -4
  22. package/cjs/staticHelpers.js +191 -9
  23. package/cjs/stream.d.ts +1 -1
  24. package/cjs/stream.js +3 -3
  25. package/{esm → cjs/vendors}/contentType.js +7 -3
  26. package/{esm → cjs/vendors}/etag.d.ts +3 -1
  27. package/cjs/{etag.js → vendors/etag.js} +1 -1
  28. package/cjs/vendors/fresh.d.ts +23 -0
  29. package/cjs/vendors/fresh.js +102 -0
  30. package/cjs/vendors/mime.d.ts +1 -0
  31. package/cjs/vendors/mime.js +42 -0
  32. package/cjs/vendors/mimeDb.d.ts +9413 -0
  33. package/cjs/vendors/mimeDb.js +9417 -0
  34. package/cjs/vendors/mimeScore.d.ts +5 -0
  35. package/cjs/vendors/mimeScore.js +50 -0
  36. package/cjs/vendors/onFinished.d.ts +14 -0
  37. package/cjs/vendors/onFinished.js +245 -0
  38. package/cjs/vendors/rangeParser.d.ts +10 -0
  39. package/cjs/vendors/rangeParser.js +125 -0
  40. package/esm/body.d.ts +7 -7
  41. package/esm/body.js +2 -2
  42. package/esm/bodyHelpers.js +3 -3
  43. package/esm/connect.d.ts +2 -2
  44. package/esm/connect.js +3 -3
  45. package/esm/dx.d.ts +1 -1
  46. package/esm/dx.js +23 -14
  47. package/esm/dxHelpers.d.ts +2 -2
  48. package/esm/dxHelpers.js +10 -5
  49. package/esm/express.d.ts +1 -1
  50. package/esm/express.js +16 -14
  51. package/esm/helpers.d.ts +2 -2
  52. package/esm/helpers.js +2 -2
  53. package/esm/index.d.ts +6 -6
  54. package/esm/index.js +6 -6
  55. package/esm/polyfillWithResolvers.js +2 -3
  56. package/esm/router.js +6 -6
  57. package/esm/static.d.ts +3 -3
  58. package/esm/static.js +10 -10
  59. package/esm/staticHelpers.d.ts +16 -4
  60. package/esm/staticHelpers.js +191 -9
  61. package/esm/stream.d.ts +1 -1
  62. package/esm/stream.js +3 -3
  63. package/{cjs → esm/vendors}/contentType.js +3 -7
  64. package/{cjs → esm/vendors}/etag.d.ts +3 -1
  65. package/esm/vendors/etag.js +90 -0
  66. package/esm/vendors/fresh.d.ts +23 -0
  67. package/esm/vendors/fresh.js +96 -0
  68. package/esm/vendors/mime.d.ts +1 -0
  69. package/esm/vendors/mime.js +35 -0
  70. package/esm/vendors/mimeDb.d.ts +9413 -0
  71. package/esm/vendors/mimeDb.js +9415 -0
  72. package/esm/vendors/mimeScore.d.ts +5 -0
  73. package/esm/vendors/mimeScore.js +46 -0
  74. package/esm/vendors/onFinished.d.ts +14 -0
  75. package/esm/vendors/onFinished.js +241 -0
  76. package/esm/vendors/rangeParser.d.ts +10 -0
  77. package/esm/vendors/rangeParser.js +121 -0
  78. package/package.json +4 -6
  79. package/cjs/file.d.ts +0 -3
  80. package/cjs/file.js +0 -12
  81. package/cjs/router.d.ts +0 -43
  82. package/esm/etag.js +0 -90
  83. package/esm/file.d.ts +0 -3
  84. package/esm/file.js +0 -8
  85. package/esm/router.d.ts +0 -43
  86. /package/cjs/{contentType.d.ts → vendors/contentType.d.ts} +0 -0
  87. /package/esm/{contentType.d.ts → vendors/contentType.d.ts} +0 -0
package/README.md CHANGED
@@ -1,61 +1,142 @@
1
- # dx-server - modern, unopinionated, and satisfactory server
1
+ # dx-server
2
+
3
+ A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant request/response handling without prop drilling.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dx-server.svg)](https://www.npmjs.com/package/dx-server)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🚀 **Context-based architecture** - Access request/response from anywhere using AsyncLocalStorage
11
+ - 🔗 **Chainable middleware** - Elegant middleware composition with [jchain](https://www.npmjs.com/package/jchain)
12
+ - 🎯 **Type-safe** - Written in TypeScript with comprehensive type definitions
13
+ - 🔄 **Express compatible** - Use existing Express middleware and applications
14
+ - 📦 **Zero dependencies** - No runtime dependencies, all functionality built-in
15
+ - 🛡️ **Built-in body parsing** - JSON, text, URL-encoded, and raw body parsing with size limits
16
+ - 🗂️ **Static file serving** - Efficient static file handling with ETag support
17
+ - 🔀 **Modern routing** - URLPattern-based routing (not Express patterns)
18
+
19
+ ## Installation
2
20
 
3
- ## Install
4
21
  ```bash
22
+ # npm
23
+ npm install dx-server jchain
24
+
25
+ # yarn
5
26
  yarn add dx-server jchain
27
+
28
+ # pnpm
29
+ pnpm add dx-server jchain
30
+ ```
31
+
32
+ ### URLPattern Support
33
+
34
+ dx-server uses the [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for routing, which is natively supported in Node.js v23.8.0 and later.
35
+
36
+ **For Node.js < 23.8.0**, you need to install a polyfill:
37
+
38
+ ```bash
39
+ npm install urlpattern-polyfill
40
+ ```
41
+
42
+ Then import it before using dx-server:
43
+
44
+ ```javascript
45
+ // Add this at the top of your entry file
46
+ import 'urlpattern-polyfill'
47
+
48
+ // Then import dx-server
49
+ import dxServer from 'dx-server'
50
+ ```
51
+
52
+ To check if your runtime supports URLPattern natively:
53
+
54
+ ```javascript
55
+ if (typeof URLPattern === 'undefined') {
56
+ console.log('URLPattern not supported, polyfill required')
57
+ }
6
58
  ```
7
59
 
8
- ## Usage
9
60
 
10
- Check below sample with comment for more details.
61
+ ## Quick Start
11
62
 
12
- Simple server
63
+ ### Basic Server
13
64
 
14
65
  ```javascript
15
66
  import {Server} from 'node:http'
16
67
  import chain from 'jchain'
17
- import dxServer, {getReq, getRes, router, setHtml, setText,} from 'dx-server'
68
+ import dxServer, {getReq, getRes, router, setHtml, setText} from 'dx-server'
18
69
 
19
70
  new Server().on('request', (req, res) => chain(
20
- dxServer(req, res),
21
- async next => {
22
- try {
23
- getRes().setHeader('Cache-Control', 'no-cache')
24
- console.log(getReq().method, getReq().url)
25
- await next()
26
- } catch (e) {
27
- console.error(e)
28
- setHtml('internal server error', {status: 500})
29
- }
30
- },
31
- router.get({
32
- '/'() {setHtml('hello world')},
33
- '/health'() {setText('ok')}
34
- }),
35
- () => setHtml('not found', {status: 404}),
36
- )()
37
- ).listen(3000, () => console.log('server is listening at 3000'))
71
+ dxServer(req, res),
72
+ async next => {
73
+ try {
74
+ // Access req/res from anywhere - no prop drilling!
75
+ getRes().setHeader('Cache-Control', 'no-cache')
76
+ console.log(getReq().method, getReq().url)
77
+ await next()
78
+ } catch (e) {
79
+ console.error(e)
80
+ setHtml('internal server error', {status: 500})
81
+ }
82
+ },
83
+ router.get({
84
+ '/'() {setHtml('hello world')},
85
+ '/health'() {setText('ok')}
86
+ }),
87
+ () => setHtml('not found', {status: 404}),
88
+ )()).listen(3000, () => console.log('server is listening at 3000'))
38
89
  ```
39
90
 
40
- File server:
91
+ ### TypeScript Example
92
+
93
+ ```typescript
94
+ import {Server} from 'node:http'
95
+ import chain from 'jchain'
96
+ import dxServer, {router, setJson, getJson} from 'dx-server'
97
+
98
+ interface User {
99
+ id: number
100
+ name: string
101
+ }
102
+
103
+ new Server().on('request', (req, res) => chain(
104
+ dxServer(req, res),
105
+ router.post({
106
+ async '/api/users'() {
107
+ const body = await getJson<{name: string}>()
108
+ if (!body?.name) {
109
+ setJson({error: 'Name required'}, {status: 400})
110
+ return
111
+ }
112
+ const user: User = {id: 1, name: body.name}
113
+ setJson(user, {status: 201})
114
+ }
115
+ }),
116
+ () => setJson({error: 'Not found'}, {status: 404})
117
+ )()).listen(3000)
118
+ ```
119
+
120
+ ### Static File Server
41
121
 
42
122
  ```javascript
43
123
  import {Server} from 'node:http'
44
124
  import chain from 'jchain'
45
125
  import dxServer, {chainStatic, setHtml} from 'dx-server'
46
- import {resolve, dirname} from 'node:path'
47
- import {fileURLToPath} from 'node:url'
126
+ import {resolve} from 'node:path'
48
127
 
49
128
  new Server().on('request', (req, res) => chain(
50
- dxServer(req, res),
51
- chainStatic('/*', {root: resolve(dirname(fileURLToPath(import.meta.url)), 'public')}),
52
- () => setHtml('not found', {status: 404}),
53
- )()
54
- ).listen(3000, () => console.log('server is listening at 3000'))
129
+ dxServer(req, res),
130
+ chainStatic('/*', {
131
+ root: resolve(import.meta.dirname, 'public'),
132
+ }),
133
+ () => setHtml('not found', {status: 404}),
134
+ )()).listen(3000)
55
135
  ```
56
136
 
57
- More complex server with express.
58
- This sample additionally requires: `yarn install express morgan`
137
+ ### Production-Ready Server with Express Integration
138
+
139
+ This example requires: `npm install express morgan helmet cors`
59
140
 
60
141
 
61
142
  ```javascript
@@ -63,10 +144,10 @@ import {Server} from 'node:http'
63
144
  import {promisify} from 'node:util'
64
145
  import chain from 'jchain'
65
146
  import dxServer, {
66
- getReq, getRes,
67
- getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
68
- setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
69
- router, connectMiddlewares, chainStatic, makeDxContext
147
+ getReq, getRes,
148
+ getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
149
+ setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
150
+ router, connectMiddlewares, chainStatic, makeDxContext
70
151
  } from 'dx-server'
71
152
  import {expressApp} from 'dx-server/express'
72
153
  import express from 'express'
@@ -74,291 +155,522 @@ import morgan from 'morgan'
74
155
 
75
156
  // it is best practice to create custom error class for non-system error
76
157
  class ServerError extends Error {
77
- name = 'ServerError'
158
+ name = 'ServerError'
78
159
 
79
- constructor(message, status = 400, code = 'unknown') {
80
- super(message)
81
- this.status = status
82
- this.code = code
83
- }
160
+ constructor(message, status = 400, code = 'unknown') {
161
+ super(message)
162
+ this.status = status
163
+ this.code = code
164
+ }
84
165
  }
85
166
 
86
167
  const authContext = makeDxContext(async () => {
87
- if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
168
+ if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
88
169
  })
89
170
 
90
171
  const requireAuth = () => {
91
- if (!authContext.value) throw new ServerError('unauthorized', 401, 'unauthorized')
172
+ if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
92
173
  }
93
174
 
94
175
  const serverChain = chain(
95
- next => {
96
- // this is the difference between express and dx-server
97
- // req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
98
- getRes().setHeader('Cache-Control', 'no-cache')
99
- return next() // must return or await
100
- },
101
- async next => {// global error catching for all following middlewares
102
- try {
103
- await next()
104
- } catch (e) {// only app error message should be shown to user
105
- if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
106
- else {// report system error
107
- console.error(e)
108
- setHtml('internal server error (code: internal)', {status: 500})
109
- }
110
- }
111
- },
112
- connectMiddlewares(
113
- morgan('common'),
114
- // cors(),
115
- ),
116
- await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
117
- app.set('trust proxy', true)
118
- if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
119
- app.use('/public', express.static('public'))
120
- }),
121
- authContext.chain(), // chain context will set the context value to authContext.value in every request
122
- router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
123
- try {
124
- await next()
125
- } catch (e) {
126
- if (e instanceof ServerError) setJson({// only app error message should be shown to user
127
- error: e.message,
128
- code: e.code,
129
- }, {status: e.status})
130
- else {// report system error
131
- console.error(e)
132
- setJson({
133
- message: 'internal server error',
134
- code: 'internal'
135
- }, {status: 500})
136
- }
137
- }
138
- }),
139
- router.post({
140
- '/api/sample-public-api'() { // sample POST router
141
- setJson({name: 'joe'})
142
- },
143
- '/api/me'() { // sample private router
144
- requireAuth()
145
- setJson({name: authContext.value.name})
146
- },
147
- }),
148
- router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
149
- router.get('/health', () => setText('ok')),
150
- () => { // not found router
151
- throw new ServerError('not found', 404, 'not_found')
152
- },
176
+ next => {
177
+ // this is the difference between express and dx-server
178
+ // req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
179
+ getRes().setHeader('Cache-Control', 'no-cache')
180
+ return next() // must return or await
181
+ },
182
+ async next => {// global error catching for all following middlewares
183
+ try {
184
+ await next()
185
+ } catch (e) {// only app error message should be shown to user
186
+ if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
187
+ else {// report system error
188
+ console.error(e)
189
+ setHtml('internal server error (code: internal)', {status: 500})
190
+ }
191
+ }
192
+ },
193
+ connectMiddlewares(
194
+ morgan('common'),
195
+ // cors(),
196
+ ),
197
+ await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
198
+ app.set('trust proxy', true)
199
+ if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
200
+ app.use('/public', express.static('public'))
201
+ }),
202
+ authContext.chain(), // chain context will set the context value to authContext.value in every request
203
+ router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
204
+ try {
205
+ await next()
206
+ } catch (e) {
207
+ if (e instanceof ServerError) setJson({// only app error message should be shown to user
208
+ error: e.message,
209
+ code: e.code,
210
+ }, {status: e.status})
211
+ else {// report system error
212
+ console.error(e)
213
+ setJson({
214
+ message: 'internal server error',
215
+ code: 'internal'
216
+ }, {status: 500})
217
+ }
218
+ }
219
+ }),
220
+ router.post({
221
+ '/api/sample-public-api'() { // sample POST router
222
+ setJson({name: 'joe'})
223
+ },
224
+ '/api/me'() { // sample private router
225
+ requireAuth()
226
+ setJson({name: authContext.value.name})
227
+ },
228
+ }),
229
+ router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
230
+ router.get('/health', () => setText('ok')),
231
+ () => { // not found router
232
+ throw new ServerError('Not found', 404, 'NOT_FOUND')
233
+ },
153
234
  )
154
235
 
155
236
  const tcpServer = new Server()
156
- .on('request', async (req, res) => {
157
- try {
158
- await chain(
159
- dxServer(req, res, {jsonBeautify: process.env.NODE_ENV !== 'production'}), // basic dx-server context
160
- serverChain,
161
- )()
162
- } catch (e) {
163
- console.error(e)
164
- res.end()
165
- }
166
- })
237
+ .on('request', async (req, res) => {
238
+ try {
239
+ await chain(
240
+ dxServer(req, res, {jsonBeautify: process.env.NODE_ENV !== 'production'}), // basic dx-server context
241
+ serverChain,
242
+ )()
243
+ } catch (e) {
244
+ console.error(e)
245
+ res.end()
246
+ }
247
+ })
167
248
 
168
249
  await promisify(tcpServer.listen.bind(tcpServer))(3000)
169
250
  console.log('server is listening at 3000')
170
251
  ```
171
252
 
172
- ## Note:
253
+ ## Core Concepts
254
+
255
+ ### Context-Based Architecture
256
+
257
+ dx-server uses Node.js AsyncLocalStorage to provide request/response context globally, eliminating prop drilling:
258
+
259
+ ```javascript
260
+ // Access request/response from anywhere
261
+ import {getReq, getRes} from 'dx-server'
262
+
263
+ function someDeepFunction() {
264
+ const req = getReq() // No need to pass req through multiple layers
265
+ const res = getRes()
266
+ res.setHeader('X-Custom', 'value')
267
+ }
268
+ ```
269
+
270
+ ### Lazy Body Parsing
173
271
 
174
- `getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery` are all asynchronous functions.
175
- The associated results are calculated in the first time they are called and cached for subsequent calls.
272
+ Body parsing functions are asynchronous and cached per request:
176
273
 
177
- If you want to get these values synchronously, chain it, like follows:
178
274
  ```javascript
179
- import {getJson} from 'dx-server'
275
+ import {getJson, getText, getBuffer, getUrlEncoded} from 'dx-server'
180
276
 
277
+ // Async usage (lazy-loaded and cached)
278
+ const json = await getJson()
279
+ const text = await getText()
280
+
281
+ // Sync usage (requires chaining)
181
282
  chain(
182
- getJson.chain(/*option*/), // json body is parsed and stored in context in every request
183
- next => {
184
- console.log(getJson.value) // json body can be accessed synchronously
185
- return next()
186
- }
283
+ getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
284
+ next => {
285
+ console.log(getJson.value) // Access synchronously
286
+ return next()
287
+ }
187
288
  )
188
289
  ```
189
290
 
190
- Context can be created using `makeDxContext` function:
291
+ ### Custom Contexts
292
+
293
+ Create reusable context objects with `makeDxContext`:
191
294
 
192
295
  ```javascript
193
- import {makeDxContext} from 'dx-server'
296
+ import {makeDxContext, getReq} from 'dx-server'
194
297
 
195
- const authContext = makeDxContext(() => {
196
- if (getReq().headers.authorization) return {id: 1, name: 'joe (authorized)'}
298
+ // Create auth context
299
+ const authContext = makeDxContext(async () => {
300
+ const token = getReq().headers.authorization
301
+ if (!token) return null
302
+ return await validateToken(token) // Your validation logic
197
303
  })
198
- const requireAuth = () => {
199
- if (!authContext.value) throw new Error('unauthorized')
200
- }
201
- chain(
202
- authContext.chain(),
203
- next => {
204
- requireAuth()
205
- return next()
206
- }
207
- )
208
- // or await authContext() to lazy load the context and don't require chaining authContext.chain()
304
+
305
+ // Use in middleware
209
306
  chain(
210
- async next => {
211
- console.log(await authContext())
212
- return next()
213
- }
307
+ authContext.chain(), // Initialize for all requests
308
+ next => {
309
+ if (!authContext.value) {
310
+ setJson({error: 'Unauthorized'}, {status: 401})
311
+ return
312
+ }
313
+ return next()
314
+ }
214
315
  )
316
+
215
317
  ```
216
318
 
217
- # API References
218
- All exported APIs:
319
+ ## API Reference
320
+
321
+ ### Main Exports
322
+
219
323
  ```javascript
220
324
  import dxServer, {
221
- getReq, getRes, getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
222
- setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
223
- router, connectMiddlewares, chainStatic, makeDxContext
325
+ // Request/Response access
326
+ getReq, getRes,
327
+
328
+ // Request body parsers
329
+ getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
330
+
331
+ // Response setters
332
+ setHtml, setJson, setText, setEmpty, setBuffer, setRedirect,
333
+ setNodeStream, setWebStream, setFile,
334
+
335
+ // Utilities
336
+ router, connectMiddlewares, chainStatic, makeDxContext
224
337
  } from 'dx-server'
225
- import {expressApp, expressRouter} from 'dx-server/express' // requires express installed
338
+
339
+ // Express integration (requires express installed)
340
+ import {expressApp, expressRouter} from 'dx-server/express'
341
+
342
+ // Low-level helpers
226
343
  import {
227
- setBufferBodyDefaultOptions,
228
- bufferFromReq, jsonFromReq, rawFromReq, textFromReq, urlEncodedFromReq, queryFromReq,
344
+ setBufferBodyDefaultOptions,
345
+ bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
346
+ urlEncodedFromReq, queryFromReq,
229
347
  } from 'dx-server/helpers'
230
348
  ```
231
349
 
232
- ## Basic
233
- ```javascript
234
- import dxServer, {
235
- getReq, getRes, getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
236
- setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
237
- makeDxContext
238
- } from 'dx-server'
350
+ ### Core Functions
351
+
352
+ #### Request/Response Access
353
+ - **`getReq()`** - Get the current request object
354
+ - **`getRes()`** - Get the current response object
355
+
356
+ #### Body Parsers
357
+ All body parsers are async, lazy-loaded, and cached per request:
358
+
359
+ - **`getJson(options?)`** - Parse JSON body (requires `Content-Type: application/json`)
360
+ - **`getText(options?)`** - Parse text body (requires `Content-Type: text/plain`)
361
+ - **`getBuffer(options?)`** - Get raw buffer
362
+ - **`getRaw(options?)`** - Get raw body (requires `Content-Type: application/octet-stream`)
363
+ - **`getUrlEncoded(options?)`** - Parse URL-encoded form (requires `Content-Type: application/x-www-form-urlencoded`)
364
+ - **`getQuery(options?)`** - Parse query string parameters
365
+
366
+ Options:
367
+ ```typescript
368
+ {
369
+ bodyLimit?: number // Max body size in bytes (default: 100KB)
370
+ urlEncodedParser?: (search: string) => any
371
+ queryParser?: (search: string) => any
372
+ }
239
373
  ```
240
374
 
241
- - `getReq()`, `getRes()`: get request and response objects from anywhere.
375
+ #### Response Setters
376
+ - **`setJson(data, {status?, headers?})`** - Send JSON response
377
+ - **`setHtml(html, {status?, headers?})`** - Send HTML response
378
+ - **`setText(text, {status?, headers?})`** - Send plain text
379
+ - **`setBuffer(buffer, {status?, headers?})`** - Send buffer
380
+ - **`setFile(path, options?)`** - Send file
381
+ - **`setNodeStream(stream, {status?, headers?})`** - Send Node.js stream
382
+ - **`setWebStream(stream, {status?, headers?})`** - Send Web stream
383
+ - **`setRedirect(url, {status?, headers?})`** - Redirect response
384
+ - **`setEmpty({status?, headers?})`** - Send empty response
385
+
386
+ #### Context Management
387
+ - **`makeDxContext(fn)`** - Create a custom context object
388
+ ```javascript
389
+ const ctx = makeDxContext(() => computeValue())
390
+
391
+ // Access value
392
+ await ctx() // Lazy load
393
+ ctx.value // Sync access (after loading)
394
+ ctx.get(req) // Get for specific request
395
+
396
+ // Set value
397
+ ctx.value = newValue
398
+ ctx.set(req, newValue)
399
+ ```
400
+
401
+ #### Middleware Utilities
402
+ - **`connectMiddlewares(...middlewares)`** - Use Connect/Express middleware
403
+ - **`chainStatic(pattern, options)`** - Serve static files
404
+ ```javascript
405
+ chainStatic('/public/*', {
406
+ root: '/path/to/files',
407
+ getPathname(matched){return matched.pathname}, // take URLPattern matched object, epects to return the file path
408
+ // the returned file path must be run through decodeURIComponent before returning
409
+ dotfiles: 'deny',
410
+ disableEtag: false,
411
+ lastModified: true
412
+ })
413
+ ```
414
+
415
+ ### Routing
416
+
417
+ dx-server uses [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for routing, which differs from Express patterns:
418
+
419
+ ```javascript
420
+ import {router} from 'dx-server'
421
+
422
+ // Single route
423
+ router.get('/users/:id', ({matched}) => {
424
+ const {id} = matched.pathname.groups
425
+ setJson({userId: id})
426
+ })
427
+
428
+ // Multiple routes
429
+ router.post({
430
+ '/api/users': () => { /* create user */ },
431
+ '/api/users/:id': ({matched}) => { /* update user */ },
432
+ '/api/users/:id/posts': ({matched}) => { /* get user posts */ }
433
+ })
242
434
 
243
- - `getBuffer()`, `getJson()`, `getRaw()`, `getText()`, `getUrlEncoded()`, `getQuery()`: get parsed request body, raw body, text body, url encoded body, query string from anywhere.
244
- These are DX context object, can be used as follows:
245
- - `const json = await getJson()`: lazily load the context, once loaded, it is cached for subsequent calls.
246
- No chaining is required.
247
- - Chain it to get the value synchronously: `chain(getJson.chain(), next => console.log(getJson.value))`. Note that the value is calculated in every request.
248
- - `ctx = makeDxContext(fn)`: create a DX context object.
249
- `fn` is called once per request to calculate the value.
250
- The value is cached for subsequent calls.
435
+ // All HTTP methods supported
436
+ router.get(pattern, handler)
437
+ router.post(pattern, handler)
438
+ router.put(pattern, handler)
439
+ router.delete(pattern, handler)
440
+ router.patch(pattern, handler)
441
+ router.head(pattern, handler)
442
+ router.options(pattern, handler)
443
+ router.all(pattern, handler) // Any method
444
+
445
+ // Custom method
446
+ router.method('CUSTOM', pattern, handler)
447
+
448
+ // With prefix option
449
+ router.get({
450
+ '/users': listUsers,
451
+ '/users/:id': getUser
452
+ }, {prefix: '/api'}) // Routes become /api/users, /api/users/:id
453
+ ```
251
454
 
252
- - Context value can be accessed by:
455
+ #### URLPattern vs Express Patterns
253
456
 
254
- - `const value = await ctx()`: lazily load the context, once loaded, it is cached for subsequent calls.
255
- - `const value = ctx.value`: get the value synchronously. Note that the value must be fetched at least once before via `await ctx()` or by chaining `chain(ctx.chain())`.
256
- - `const value = ctx.get(req)`: similar condition as `ctx.value`.
457
+ | Pattern | URLPattern | Express |
458
+ |---------|------------|---------|
459
+ | Wildcard | `/api/*` | `/api/*` or `/api/(.*)` |
460
+ | Optional trailing slash | `{/}?` | `/path/?` |
461
+ | Named params | `/:id` | `/:id` |
462
+ | Optional params | `/:id?` | `/:id?` |
257
463
 
258
- - Context value can be set or overridden by:
464
+ **Important differences:**
465
+ - `'/foo'` matches `/foo` but NOT `/foo/`
466
+ - `'/foo/'` matches `/foo/` but NOT `/foo`
467
+ - Use `'/foo{/}?'` to match both
259
468
 
260
- - `ctx.value = value`.
261
- - `ctx.set(req, value)`.
469
+ ### Express Integration
262
470
 
263
- - `setHtml`, `setJson`, `setText`, `setEmpty`, `setBuffer`, `setRedirect`, `setNodeStream`, `setWebStream`, `setFile`: set response body.
471
+ dx-server seamlessly integrates with Express applications and middleware:
264
472
 
265
- - `connectMiddlewares(...middlewares)`: connect middlewares. For example:
266
473
  ```javascript
267
- import {connectMiddlewares} from 'dx-server'
268
- import morgan from 'morgan'
474
+ import {expressApp, expressRouter} from 'dx-server/express'
475
+ import express from 'express'
269
476
  import cors from 'cors'
477
+ import helmet from 'helmet'
270
478
 
271
- connectMiddlewares(
272
- morgan('common'),
273
- cors(),
479
+ chain(
480
+ // Use entire Express app
481
+ await expressApp(app => {
482
+ app.set('trust proxy', true)
483
+ app.set('json spaces', 2)
484
+ app.use(helmet())
485
+ app.use('/static', express.static('public'))
486
+ }),
487
+
488
+ // Or use Express router
489
+ expressRouter(router => {
490
+ router.use(cors())
491
+ router.get('/legacy', (req, res) => {
492
+ res.json({message: 'Express route'})
493
+ })
494
+ })
274
495
  )
275
496
  ```
276
497
 
277
- - `chainStatic(path, options)`: serve static files. For example:
498
+ ### Low-Level Helpers
499
+
500
+ Pure functions for custom implementations:
501
+
278
502
  ```javascript
279
- import {chainStatic} from 'dx-server'
280
- import {resolve, dirname} from 'node:path'
281
- import {fileURLToPath} from 'node:url'
503
+ import {
504
+ setBufferBodyDefaultOptions,
505
+ bufferFromReq, jsonFromReq, rawFromReq,
506
+ textFromReq, urlEncodedFromReq, queryFromReq
507
+ } from 'dx-server/helpers'
282
508
 
509
+ // Set global defaults
510
+ setBufferBodyDefaultOptions({
511
+ bodyLimit: 10 * 1024 * 1024, // 10MB
512
+ queryParser: (search) => myCustomParser(search)
513
+ })
514
+
515
+ // Use directly with req/res (no context required)
516
+ const json = await jsonFromReq(req, {bodyLimit: 1024})
517
+ const query = queryFromReq(req)
518
+ ```
519
+
520
+ ## Security Considerations
521
+
522
+ ### Body Size Limits
523
+ Always set appropriate body size limits to prevent DoS attacks:
524
+
525
+ ```javascript
283
526
  chain(
284
- chainStatic('/assets/*', {
285
- root: resolve(dirname(fileURLToPath(import.meta.url)), 'public'),
286
- getPathname(matched) {
287
- return new URL(getReq().url, 'http://localhost').pathname.slice('/assets'.length)
288
- },
289
- }),
527
+ getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
528
+ // or globally:
529
+ dxServer(req, res, {bodyLimit: 5 * 1024 * 1024}) // 5MB
290
530
  )
291
531
  ```
292
532
 
293
- ## Routing
533
+ ### Error Handling
534
+ Never expose internal errors to clients:
535
+
294
536
  ```javascript
295
- import {router} from 'dx-server'
296
- ```
537
+ class AppError extends Error {
538
+ constructor(message, status = 400, code = 'ERROR') {
539
+ super(message)
540
+ this.status = status
541
+ this.code = code
542
+ }
543
+ }
297
544
 
298
- - `router.get`, `router.post`, `router.put`, `router.delete`, `router.patch`, `router.head`, `router.options`, `router.connect`, `router.trace`: create router.
299
- These functions accept 2 formats:
300
- - `router.get(routes: {[pattern: string]: Route}, options: RouterOptions)`: create multiple routes.
301
- - `router.get(pattern: string, handler: Route, options: RouterOptions)`: create route for GET method.
545
+ chain(
546
+ async next => {
547
+ try {
548
+ await next()
549
+ } catch (error) {
550
+ if (error instanceof AppError) {
551
+ setJson({error: error.message, code: error.code}, {status: error.status})
552
+ } else {
553
+ console.error(error) // Log for debugging
554
+ setJson({error: 'Internal server error'}, {status: 500})
555
+ }
556
+ }
557
+ }
558
+ )
559
+ ```
302
560
 
303
- - `router.all(...)`: same as `router.get()` but for any method.
304
- - `router.method()`: create router with custom method. Similar to `router.get()`, this function accepts 2 formats.
305
- - `router.method(method: string, routes: {[pattern: string]: Route}, options: RouterOptions)`: create multiple routes.
306
- - `router.method(method: string, pattern: string, handler: Route, options: RouterOptions)`: create route for `method` method.
561
+ ### Input Validation
562
+ Always validate input data:
307
563
 
308
- `RouterOptions` is defined as follows:
309
- ```typescript
310
- interface RouterOptions {
311
- prefix?: string
312
- }
564
+ ```javascript
565
+ router.post('/api/users', async () => {
566
+ const data = await getJson()
567
+
568
+ // Validate
569
+ if (!data?.email || !isValidEmail(data.email)) {
570
+ throw new AppError('Invalid email', 400, 'INVALID_EMAIL')
571
+ }
572
+
573
+ // Process...
574
+ })
313
575
  ```
314
576
 
315
- Patterns are matched using [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern).
316
- This does not always match the same as ExpressJS.
317
- For example, to match any path prefixed with `/api/`, use `/api/*`.
318
- Note the following:
319
- - `''` matches nothing.
320
- - `'/'` matches both https://example.com and https://example.com/.
321
- - `'/foo'` matches https://example.com/foo but not https://example.com/foo/.
322
- - `'/foo/'` matches https://example.com/foo/ but not https://example.com/foo.
577
+ ### Security Headers
578
+ Use security middleware:
323
579
 
324
- `Route` is defined as follows:
325
- ```typescript
326
- interface RouteContext {
327
- matched: URLPatternResult // result returned from URLPattern.exec(). To get params: matched.pathname.groups
328
- next(): any
329
- }
330
- type Route = (context: RouteContext) => any
580
+ ```javascript
581
+ import helmet from 'helmet'
582
+ import cors from 'cors'
583
+
584
+ chain(
585
+ connectMiddlewares(
586
+ helmet(),
587
+ cors({
588
+ origin: process.env.ALLOWED_ORIGINS?.split(','),
589
+ credentials: true
590
+ })
591
+ )
592
+ )
331
593
  ```
332
594
 
333
- ## Helpers
595
+ ## Advanced Examples
596
+
597
+ ### File Upload with Busboy
334
598
  ```javascript
335
- import {
336
- setBufferBodyDefaultOptions,
337
- bufferFromReq, jsonFromReq, rawFromReq, textFromReq, urlEncodedFromReq, queryFromReq,
338
- } from 'dx-server/helpers'
599
+ import busboy from 'busboy'
600
+
601
+ router.post('/upload', () => {
602
+ const req = getReq()
603
+ const bb = busboy({headers: req.headers, limits: {fileSize: 10 * 1024 * 1024}})
604
+
605
+ bb.on('file', (name, file, info) => {
606
+ // Handle file stream
607
+ })
608
+
609
+ req.pipe(bb)
610
+ })
339
611
  ```
340
612
 
341
- Helpers are all pure functions, and do not rely on any context.
342
- These functions are independent of the context and can be used anywhere, even outside of this package.
343
- They require request and response objects to be passed.
613
+ ### WebSocket Upgrade
614
+ ```javascript
615
+ import {WebSocketServer} from 'ws'
616
+
617
+ const wss = new WebSocketServer({noServer: true})
344
618
 
345
- ## ExpressJS
619
+ server.on('upgrade', (request, socket, head) => {
620
+ if (request.url === '/ws') {
621
+ wss.handleUpgrade(request, socket, head, (ws) => {
622
+ wss.emit('connection', ws, request)
623
+ })
624
+ }
625
+ })
626
+ ```
627
+
628
+ ### Rate Limiting
346
629
  ```javascript
347
- import {expressApp, expressRouter} from 'dx-server/express' // requires express installed
630
+ import rateLimit from 'express-rate-limit'
348
631
 
349
632
  chain(
350
- await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
351
- app.set('trust proxy', true)
352
- if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
353
- app.use('/public', express.static('public'))
354
- }),
355
- expressRouter(router => {
356
- router.use(cors())
357
- }),
633
+ connectMiddlewares(
634
+ rateLimit({
635
+ windowMs: 15 * 60 * 1000, // 15 minutes
636
+ max: 100 // limit each IP to 100 requests per windowMs
637
+ })
638
+ )
358
639
  )
359
640
  ```
360
641
 
361
- ## Other functionalities
362
- - Download file: set the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header.
363
- - Upload file: recommend [busboy](https://www.npmjs.com/package/busboy) package.
364
- - Cookie: recommend [cookie](https://www.npmjs.com/package/cookie) package.
642
+ ## Performance Tips
643
+
644
+ 1. **Use lazy body parsing** - Only parse bodies when needed
645
+ 2. **Enable compression** at reverse proxy level (nginx, CDN)
646
+ 3. **Use streaming** for large responses:
647
+ ```javascript
648
+ import {createReadStream} from 'fs'
649
+ setNodeStream(createReadStream('large-file.pdf'))
650
+ ```
651
+ 4. **Cache contexts** that are expensive to compute
652
+ 5. **Use `chainStatic` with proper cache headers** for static assets
653
+
654
+ ## Migration from Express
655
+
656
+ ```javascript
657
+ // Express
658
+ app.get('/users/:id', (req, res) => {
659
+ const {id} = req.params
660
+ res.json({userId: id})
661
+ })
662
+
663
+ // dx-server
664
+ router.get('/users/:id', ({matched}) => {
665
+ const {id} = matched.pathname.groups
666
+ setJson({userId: id})
667
+ })
668
+ ```
669
+
670
+ ## Contributing
671
+
672
+ Contributions are welcome! Please feel free to submit a Pull Request.
673
+
674
+ ## License
675
+
676
+ MIT © [Sang Tran](https://github.com/tranvansang)