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.
- package/README.md +557 -245
- package/cjs/body.d.ts +7 -7
- package/cjs/body.js +8 -8
- package/cjs/bodyHelpers.js +6 -6
- package/cjs/connect.d.ts +2 -2
- package/cjs/connect.js +5 -5
- package/cjs/dx.d.ts +1 -1
- package/cjs/dx.js +24 -15
- package/cjs/dxHelpers.d.ts +2 -2
- package/cjs/dxHelpers.js +12 -7
- package/cjs/express.d.ts +1 -1
- package/cjs/express.js +16 -14
- package/cjs/helpers.d.ts +2 -2
- package/cjs/helpers.js +10 -10
- package/cjs/index.d.ts +6 -6
- package/cjs/index.js +28 -28
- package/cjs/polyfillWithResolvers.js +2 -3
- package/cjs/router.js +7 -7
- package/cjs/static.d.ts +3 -3
- package/cjs/static.js +12 -12
- package/cjs/staticHelpers.d.ts +16 -4
- package/cjs/staticHelpers.js +191 -9
- package/cjs/stream.d.ts +1 -1
- package/cjs/stream.js +3 -3
- package/{esm → cjs/vendors}/contentType.js +7 -3
- package/{esm → cjs/vendors}/etag.d.ts +3 -1
- package/cjs/{etag.js → vendors/etag.js} +1 -1
- package/cjs/vendors/fresh.d.ts +23 -0
- package/cjs/vendors/fresh.js +102 -0
- package/cjs/vendors/mime.d.ts +1 -0
- package/cjs/vendors/mime.js +42 -0
- package/cjs/vendors/mimeDb.d.ts +9413 -0
- package/cjs/vendors/mimeDb.js +9417 -0
- package/cjs/vendors/mimeScore.d.ts +5 -0
- package/cjs/vendors/mimeScore.js +50 -0
- package/cjs/vendors/onFinished.d.ts +14 -0
- package/cjs/vendors/onFinished.js +245 -0
- package/cjs/vendors/rangeParser.d.ts +10 -0
- package/cjs/vendors/rangeParser.js +125 -0
- package/esm/body.d.ts +7 -7
- package/esm/body.js +2 -2
- package/esm/bodyHelpers.js +3 -3
- package/esm/connect.d.ts +2 -2
- package/esm/connect.js +3 -3
- package/esm/dx.d.ts +1 -1
- package/esm/dx.js +23 -14
- package/esm/dxHelpers.d.ts +2 -2
- package/esm/dxHelpers.js +10 -5
- package/esm/express.d.ts +1 -1
- package/esm/express.js +16 -14
- package/esm/helpers.d.ts +2 -2
- package/esm/helpers.js +2 -2
- package/esm/index.d.ts +6 -6
- package/esm/index.js +6 -6
- package/esm/polyfillWithResolvers.js +2 -3
- package/esm/router.js +6 -6
- package/esm/static.d.ts +3 -3
- package/esm/static.js +10 -10
- package/esm/staticHelpers.d.ts +16 -4
- package/esm/staticHelpers.js +191 -9
- package/esm/stream.d.ts +1 -1
- package/esm/stream.js +3 -3
- package/{cjs → esm/vendors}/contentType.js +3 -7
- package/{cjs → esm/vendors}/etag.d.ts +3 -1
- package/esm/vendors/etag.js +90 -0
- package/esm/vendors/fresh.d.ts +23 -0
- package/esm/vendors/fresh.js +96 -0
- package/esm/vendors/mime.d.ts +1 -0
- package/esm/vendors/mime.js +35 -0
- package/esm/vendors/mimeDb.d.ts +9413 -0
- package/esm/vendors/mimeDb.js +9415 -0
- package/esm/vendors/mimeScore.d.ts +5 -0
- package/esm/vendors/mimeScore.js +46 -0
- package/esm/vendors/onFinished.d.ts +14 -0
- package/esm/vendors/onFinished.js +241 -0
- package/esm/vendors/rangeParser.d.ts +10 -0
- package/esm/vendors/rangeParser.js +121 -0
- package/package.json +4 -6
- package/cjs/file.d.ts +0 -3
- package/cjs/file.js +0 -12
- package/cjs/router.d.ts +0 -43
- package/esm/etag.js +0 -90
- package/esm/file.d.ts +0 -3
- package/esm/file.js +0 -8
- package/esm/router.d.ts +0 -43
- /package/cjs/{contentType.d.ts → vendors/contentType.d.ts} +0 -0
- /package/esm/{contentType.d.ts → vendors/contentType.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,61 +1,142 @@
|
|
|
1
|
-
# dx-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
|
+
[](https://www.npmjs.com/package/dx-server)
|
|
6
|
+
[](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
|
-
|
|
61
|
+
## Quick Start
|
|
11
62
|
|
|
12
|
-
|
|
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
|
|
68
|
+
import dxServer, {getReq, getRes, router, setHtml, setText} from 'dx-server'
|
|
18
69
|
|
|
19
70
|
new Server().on('request', (req, res) => chain(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
import {fileURLToPath} from 'node:url'
|
|
126
|
+
import {resolve} from 'node:path'
|
|
48
127
|
|
|
49
128
|
new Server().on('request', (req, res) => chain(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
158
|
+
name = 'ServerError'
|
|
78
159
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
168
|
+
if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
|
|
88
169
|
})
|
|
89
170
|
|
|
90
171
|
const requireAuth = () => {
|
|
91
|
-
|
|
172
|
+
if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
|
|
92
173
|
}
|
|
93
174
|
|
|
94
175
|
const serverChain = chain(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
|
|
319
|
+
## API Reference
|
|
320
|
+
|
|
321
|
+
### Main Exports
|
|
322
|
+
|
|
219
323
|
```javascript
|
|
220
324
|
import dxServer, {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
338
|
+
|
|
339
|
+
// Express integration (requires express installed)
|
|
340
|
+
import {expressApp, expressRouter} from 'dx-server/express'
|
|
341
|
+
|
|
342
|
+
// Low-level helpers
|
|
226
343
|
import {
|
|
227
|
-
|
|
228
|
-
|
|
344
|
+
setBufferBodyDefaultOptions,
|
|
345
|
+
bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
|
|
346
|
+
urlEncodedFromReq, queryFromReq,
|
|
229
347
|
} from 'dx-server/helpers'
|
|
230
348
|
```
|
|
231
349
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
455
|
+
#### URLPattern vs Express Patterns
|
|
253
456
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
- `ctx.set(req, value)`.
|
|
469
|
+
### Express Integration
|
|
262
470
|
|
|
263
|
-
-
|
|
471
|
+
dx-server seamlessly integrates with Express applications and middleware:
|
|
264
472
|
|
|
265
|
-
- `connectMiddlewares(...middlewares)`: connect middlewares. For example:
|
|
266
473
|
```javascript
|
|
267
|
-
import {
|
|
268
|
-
import
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
-
|
|
498
|
+
### Low-Level Helpers
|
|
499
|
+
|
|
500
|
+
Pure functions for custom implementations:
|
|
501
|
+
|
|
278
502
|
```javascript
|
|
279
|
-
import {
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
533
|
+
### Error Handling
|
|
534
|
+
Never expose internal errors to clients:
|
|
535
|
+
|
|
294
536
|
```javascript
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
##
|
|
595
|
+
## Advanced Examples
|
|
596
|
+
|
|
597
|
+
### File Upload with Busboy
|
|
334
598
|
```javascript
|
|
335
|
-
import
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
613
|
+
### WebSocket Upgrade
|
|
614
|
+
```javascript
|
|
615
|
+
import {WebSocketServer} from 'ws'
|
|
616
|
+
|
|
617
|
+
const wss = new WebSocketServer({noServer: true})
|
|
344
618
|
|
|
345
|
-
|
|
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
|
|
630
|
+
import rateLimit from 'express-rate-limit'
|
|
348
631
|
|
|
349
632
|
chain(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
##
|
|
362
|
-
|
|
363
|
-
-
|
|
364
|
-
|
|
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)
|