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.
- package/README.md +417 -293
- package/{cjs → lib}/body.d.ts +2 -3
- package/lib/body.js +10 -0
- package/lib/body.js.map +1 -0
- package/{esm → lib}/bodyHelpers.d.ts +2 -4
- package/lib/bodyHelpers.js +102 -0
- package/lib/bodyHelpers.js.map +1 -0
- package/{esm → lib}/dx.d.ts +6 -9
- package/lib/dx.js +133 -0
- package/lib/dx.js.map +1 -0
- package/{cjs → lib}/dxHelpers.d.ts +2 -5
- package/lib/dxHelpers.js +135 -0
- package/lib/dxHelpers.js.map +1 -0
- package/lib/helpers.js.map +1 -0
- package/{cjs → lib}/index.d.ts +1 -2
- package/{esm/index.d.ts → lib/index.js} +1 -2
- package/lib/index.js.map +1 -0
- package/lib/logger.d.ts +4 -0
- package/lib/logger.js +64 -0
- package/lib/logger.js.map +1 -0
- package/lib/router.d.ts +42 -0
- package/lib/router.js +43 -0
- package/lib/router.js.map +1 -0
- package/lib/static.js +23 -0
- package/lib/static.js.map +1 -0
- package/{cjs → lib}/staticHelpers.d.ts +5 -3
- package/lib/staticHelpers.js +202 -0
- package/lib/staticHelpers.js.map +1 -0
- package/{cjs → lib}/stream.d.ts +3 -8
- package/lib/stream.js +96 -0
- package/lib/stream.js.map +1 -0
- package/lib/vendors/contentType.js +64 -0
- package/lib/vendors/contentType.js.map +1 -0
- package/{cjs → lib}/vendors/etag.d.ts +2 -5
- package/lib/vendors/etag.js +94 -0
- package/lib/vendors/etag.js.map +1 -0
- package/{cjs → lib}/vendors/fresh.d.ts +2 -2
- package/lib/vendors/fresh.js +88 -0
- package/lib/vendors/fresh.js.map +1 -0
- package/lib/vendors/mime.d.ts +1 -0
- package/lib/vendors/mime.js +35 -0
- package/lib/vendors/mime.js.map +1 -0
- package/{cjs → lib}/vendors/mimeDb.d.ts +2544 -2544
- package/lib/vendors/mimeDb.js +9435 -0
- package/lib/vendors/mimeDb.js.map +1 -0
- package/{cjs → lib}/vendors/mimeScore.d.ts +1 -1
- package/lib/vendors/mimeScore.js +44 -0
- package/lib/vendors/mimeScore.js.map +1 -0
- package/{cjs → lib}/vendors/onFinished.d.ts +1 -1
- package/lib/vendors/onFinished.js +231 -0
- package/lib/vendors/rangeParser.d.ts +12 -0
- package/lib/vendors/rangeParser.js +108 -0
- package/lib/vendors/rangeParser.js.map +1 -0
- package/package.json +32 -36
- package/cjs/body.js +0 -14
- package/cjs/bodyHelpers.d.ts +0 -16
- package/cjs/bodyHelpers.js +0 -101
- package/cjs/connect.d.ts +0 -5
- package/cjs/connect.js +0 -44
- package/cjs/dx.d.ts +0 -46
- package/cjs/dx.js +0 -144
- package/cjs/dxHelpers.js +0 -123
- package/cjs/express.d.ts +0 -4
- package/cjs/express.js +0 -43
- package/cjs/helpers.js +0 -14
- package/cjs/index.js +0 -38
- package/cjs/logger.d.ts +0 -3
- package/cjs/logger.js +0 -61
- package/cjs/package.json +0 -3
- package/cjs/polyfillWithResolvers.d.ts +0 -1
- package/cjs/polyfillWithResolvers.js +0 -17
- package/cjs/router.js +0 -47
- package/cjs/static.js +0 -27
- package/cjs/staticHelpers.js +0 -195
- package/cjs/stream.js +0 -97
- package/cjs/vendors/contentType.js +0 -92
- package/cjs/vendors/etag.js +0 -136
- package/cjs/vendors/fresh.js +0 -102
- package/cjs/vendors/mime.d.ts +0 -1
- package/cjs/vendors/mime.js +0 -42
- package/cjs/vendors/mimeDb.js +0 -9417
- package/cjs/vendors/mimeScore.js +0 -50
- package/cjs/vendors/onFinished.js +0 -245
- package/cjs/vendors/rangeParser.d.ts +0 -10
- package/cjs/vendors/rangeParser.js +0 -126
- package/esm/body.d.ts +0 -8
- package/esm/body.js +0 -11
- package/esm/bodyHelpers.js +0 -90
- package/esm/connect.d.ts +0 -5
- package/esm/connect.js +0 -40
- package/esm/dx.js +0 -128
- package/esm/dxHelpers.d.ts +0 -49
- package/esm/dxHelpers.js +0 -119
- package/esm/express.d.ts +0 -4
- package/esm/express.js +0 -35
- package/esm/helpers.js +0 -3
- package/esm/index.js +0 -9
- package/esm/logger.d.ts +0 -3
- package/esm/logger.js +0 -57
- package/esm/polyfillWithResolvers.d.ts +0 -1
- package/esm/polyfillWithResolvers.js +0 -16
- package/esm/router.js +0 -44
- package/esm/static.d.ts +0 -5
- package/esm/static.js +0 -23
- package/esm/staticHelpers.d.ts +0 -18
- package/esm/staticHelpers.js +0 -188
- package/esm/stream.d.ts +0 -12
- package/esm/stream.js +0 -92
- package/esm/vendors/contentType.d.ts +0 -4
- package/esm/vendors/contentType.js +0 -88
- package/esm/vendors/etag.d.ts +0 -10
- package/esm/vendors/etag.js +0 -105
- package/esm/vendors/fresh.d.ts +0 -23
- package/esm/vendors/fresh.js +0 -96
- package/esm/vendors/mime.d.ts +0 -1
- package/esm/vendors/mime.js +0 -35
- package/esm/vendors/mimeDb.d.ts +0 -9413
- package/esm/vendors/mimeDb.js +0 -9415
- package/esm/vendors/mimeScore.d.ts +0 -5
- package/esm/vendors/mimeScore.js +0 -46
- package/esm/vendors/onFinished.d.ts +0 -14
- package/esm/vendors/onFinished.js +0 -241
- package/esm/vendors/rangeParser.d.ts +0 -10
- package/esm/vendors/rangeParser.js +0 -122
- /package/{cjs → lib}/helpers.d.ts +0 -0
- /package/{esm/helpers.d.ts → lib/helpers.js} +0 -0
- /package/{cjs → lib}/static.d.ts +0 -0
- /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
|
[](https://www.npmjs.com/package/dx-server)
|
|
6
6
|
[](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** -
|
|
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
|
-
|
|
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()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
###
|
|
98
|
+
### Custom Contexts
|
|
85
99
|
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
name: string
|
|
94
|
-
}
|
|
102
|
+
```javascript
|
|
103
|
+
import {makeDxContext, getReq} from 'dx-server'
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
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
|
-
|
|
178
|
+
name = 'ServerError'
|
|
151
179
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
188
|
+
if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
|
|
161
189
|
})
|
|
162
190
|
|
|
163
191
|
function requireAuth() {
|
|
164
|
-
|
|
192
|
+
if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
const serverChain = chain(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
- **`
|
|
366
|
-
- **`
|
|
367
|
-
- **`
|
|
368
|
-
- **`
|
|
369
|
-
- **`
|
|
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()
|
|
378
|
-
ctx.value
|
|
379
|
-
ctx.get(req)
|
|
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
|
-
|
|
453
|
+
|
|
388
454
|
- **`chainStatic(pattern, options)`** - Serve static files
|
|
455
|
+
|
|
389
456
|
```javascript
|
|
390
457
|
chainStatic('/public/*', {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
410
|
-
|
|
494
|
+
const {id} = matched.pathname.groups
|
|
495
|
+
setJson({userId: id})
|
|
411
496
|
})
|
|
412
497
|
|
|
413
498
|
// Multiple routes
|
|
414
499
|
router.post({
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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)
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
| Wildcard
|
|
445
|
-
| Optional trailing slash | `{/}?`
|
|
446
|
-
| Named params
|
|
447
|
-
| Optional params
|
|
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
|
|
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
|
|
554
|
+
import chain from 'jchain'
|
|
555
|
+
import {getReq, getRes} from 'dx-server'
|
|
460
556
|
import express from 'express'
|
|
461
|
-
import
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
651
|
-
|
|
774
|
+
const {id} = matched.pathname.groups
|
|
775
|
+
setJson({userId: id})
|
|
652
776
|
})
|
|
653
777
|
```
|
|
654
778
|
|