dx-server 0.9.0 → 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 +245 -250
- 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 +22 -13
- 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 -6
- package/cjs/static.d.ts +5 -0
- 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 +21 -12
- 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 -5
- package/esm/static.d.ts +5 -0
- 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 +1 -5
- package/cjs/file.d.ts +0 -3
- package/cjs/file.js +0 -12
- package/esm/etag.js +0 -90
- package/esm/file.d.ts +0 -3
- package/esm/file.js +0 -8
- /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
|
@@ -11,7 +11,7 @@ A modern, unopinionated, and performant Node.js server framework built on AsyncL
|
|
|
11
11
|
- 🔗 **Chainable middleware** - Elegant middleware composition with [jchain](https://www.npmjs.com/package/jchain)
|
|
12
12
|
- 🎯 **Type-safe** - Written in TypeScript with comprehensive type definitions
|
|
13
13
|
- 🔄 **Express compatible** - Use existing Express middleware and applications
|
|
14
|
-
- 📦 **
|
|
14
|
+
- 📦 **Zero dependencies** - No runtime dependencies, all functionality built-in
|
|
15
15
|
- 🛡️ **Built-in body parsing** - JSON, text, URL-encoded, and raw body parsing with size limits
|
|
16
16
|
- 🗂️ **Static file serving** - Efficient static file handling with ETag support
|
|
17
17
|
- 🔀 **Modern routing** - URLPattern-based routing (not Express patterns)
|
|
@@ -31,9 +31,9 @@ pnpm add dx-server jchain
|
|
|
31
31
|
|
|
32
32
|
### URLPattern Support
|
|
33
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
|
|
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
35
|
|
|
36
|
-
**For Node.js <
|
|
36
|
+
**For Node.js < 23.8.0**, you need to install a polyfill:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npm install urlpattern-polyfill
|
|
@@ -57,9 +57,6 @@ if (typeof URLPattern === 'undefined') {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
### Future Roadmap
|
|
61
|
-
|
|
62
|
-
**Zero Dependencies**: The `send` package (currently used for static file serving) is planned for removal in a future version. This will make dx-server a true zero-dependency framework. Until then, if you don't need static file serving, the `send` dependency won't be loaded or affect your application.
|
|
63
60
|
|
|
64
61
|
## Quick Start
|
|
65
62
|
|
|
@@ -71,23 +68,23 @@ import chain from 'jchain'
|
|
|
71
68
|
import dxServer, {getReq, getRes, router, setHtml, setText} from 'dx-server'
|
|
72
69
|
|
|
73
70
|
new Server().on('request', (req, res) => chain(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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}),
|
|
91
88
|
)()).listen(3000, () => console.log('server is listening at 3000'))
|
|
92
89
|
```
|
|
93
90
|
|
|
@@ -104,19 +101,19 @@ interface User {
|
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
new Server().on('request', (req, res) => chain(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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})
|
|
120
117
|
)()).listen(3000)
|
|
121
118
|
```
|
|
122
119
|
|
|
@@ -126,17 +123,14 @@ new Server().on('request', (req, res) => chain(
|
|
|
126
123
|
import {Server} from 'node:http'
|
|
127
124
|
import chain from 'jchain'
|
|
128
125
|
import dxServer, {chainStatic, setHtml} from 'dx-server'
|
|
129
|
-
import {resolve
|
|
130
|
-
import {fileURLToPath} from 'node:url'
|
|
126
|
+
import {resolve} from 'node:path'
|
|
131
127
|
|
|
132
128
|
new Server().on('request', (req, res) => chain(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}),
|
|
139
|
-
() => setHtml('not found', {status: 404}),
|
|
129
|
+
dxServer(req, res),
|
|
130
|
+
chainStatic('/*', {
|
|
131
|
+
root: resolve(import.meta.dirname, 'public'),
|
|
132
|
+
}),
|
|
133
|
+
() => setHtml('not found', {status: 404}),
|
|
140
134
|
)()).listen(3000)
|
|
141
135
|
```
|
|
142
136
|
|
|
@@ -150,10 +144,10 @@ import {Server} from 'node:http'
|
|
|
150
144
|
import {promisify} from 'node:util'
|
|
151
145
|
import chain from 'jchain'
|
|
152
146
|
import dxServer, {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
157
151
|
} from 'dx-server'
|
|
158
152
|
import {expressApp} from 'dx-server/express'
|
|
159
153
|
import express from 'express'
|
|
@@ -161,96 +155,96 @@ import morgan from 'morgan'
|
|
|
161
155
|
|
|
162
156
|
// it is best practice to create custom error class for non-system error
|
|
163
157
|
class ServerError extends Error {
|
|
164
|
-
|
|
158
|
+
name = 'ServerError'
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
160
|
+
constructor(message, status = 400, code = 'unknown') {
|
|
161
|
+
super(message)
|
|
162
|
+
this.status = status
|
|
163
|
+
this.code = code
|
|
164
|
+
}
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
const authContext = makeDxContext(async () => {
|
|
174
|
-
|
|
168
|
+
if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
|
|
175
169
|
})
|
|
176
170
|
|
|
177
171
|
const requireAuth = () => {
|
|
178
|
-
|
|
172
|
+
if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
|
|
179
173
|
}
|
|
180
174
|
|
|
181
175
|
const serverChain = chain(
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
},
|
|
240
234
|
)
|
|
241
235
|
|
|
242
236
|
const tcpServer = new Server()
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
})
|
|
254
248
|
|
|
255
249
|
await promisify(tcpServer.listen.bind(tcpServer))(3000)
|
|
256
250
|
console.log('server is listening at 3000')
|
|
@@ -267,9 +261,9 @@ dx-server uses Node.js AsyncLocalStorage to provide request/response context glo
|
|
|
267
261
|
import {getReq, getRes} from 'dx-server'
|
|
268
262
|
|
|
269
263
|
function someDeepFunction() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
264
|
+
const req = getReq() // No need to pass req through multiple layers
|
|
265
|
+
const res = getRes()
|
|
266
|
+
res.setHeader('X-Custom', 'value')
|
|
273
267
|
}
|
|
274
268
|
```
|
|
275
269
|
|
|
@@ -286,11 +280,11 @@ const text = await getText()
|
|
|
286
280
|
|
|
287
281
|
// Sync usage (requires chaining)
|
|
288
282
|
chain(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
283
|
+
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
|
|
284
|
+
next => {
|
|
285
|
+
console.log(getJson.value) // Access synchronously
|
|
286
|
+
return next()
|
|
287
|
+
}
|
|
294
288
|
)
|
|
295
289
|
```
|
|
296
290
|
|
|
@@ -303,21 +297,21 @@ import {makeDxContext, getReq} from 'dx-server'
|
|
|
303
297
|
|
|
304
298
|
// Create auth context
|
|
305
299
|
const authContext = makeDxContext(async () => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
const token = getReq().headers.authorization
|
|
301
|
+
if (!token) return null
|
|
302
|
+
return await validateToken(token) // Your validation logic
|
|
309
303
|
})
|
|
310
304
|
|
|
311
305
|
// Use in middleware
|
|
312
306
|
chain(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
return next()
|
|
307
|
+
authContext.chain(), // Initialize for all requests
|
|
308
|
+
next => {
|
|
309
|
+
if (!authContext.value) {
|
|
310
|
+
setJson({error: 'Unauthorized'}, {status: 401})
|
|
311
|
+
return
|
|
320
312
|
}
|
|
313
|
+
return next()
|
|
314
|
+
}
|
|
321
315
|
)
|
|
322
316
|
|
|
323
317
|
```
|
|
@@ -328,18 +322,18 @@ chain(
|
|
|
328
322
|
|
|
329
323
|
```javascript
|
|
330
324
|
import dxServer, {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
343
337
|
} from 'dx-server'
|
|
344
338
|
|
|
345
339
|
// Express integration (requires express installed)
|
|
@@ -347,9 +341,9 @@ import {expressApp, expressRouter} from 'dx-server/express'
|
|
|
347
341
|
|
|
348
342
|
// Low-level helpers
|
|
349
343
|
import {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
setBufferBodyDefaultOptions,
|
|
345
|
+
bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
|
|
346
|
+
urlEncodedFromReq, queryFromReq,
|
|
353
347
|
} from 'dx-server/helpers'
|
|
354
348
|
```
|
|
355
349
|
|
|
@@ -372,9 +366,9 @@ All body parsers are async, lazy-loaded, and cached per request:
|
|
|
372
366
|
Options:
|
|
373
367
|
```typescript
|
|
374
368
|
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
369
|
+
bodyLimit?: number // Max body size in bytes (default: 100KB)
|
|
370
|
+
urlEncodedParser?: (search: string) => any
|
|
371
|
+
queryParser?: (search: string) => any
|
|
378
372
|
}
|
|
379
373
|
```
|
|
380
374
|
|
|
@@ -409,11 +403,12 @@ Options:
|
|
|
409
403
|
- **`chainStatic(pattern, options)`** - Serve static files
|
|
410
404
|
```javascript
|
|
411
405
|
chainStatic('/public/*', {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
417
412
|
})
|
|
418
413
|
```
|
|
419
414
|
|
|
@@ -426,15 +421,15 @@ import {router} from 'dx-server'
|
|
|
426
421
|
|
|
427
422
|
// Single route
|
|
428
423
|
router.get('/users/:id', ({matched}) => {
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
const {id} = matched.pathname.groups
|
|
425
|
+
setJson({userId: id})
|
|
431
426
|
})
|
|
432
427
|
|
|
433
428
|
// Multiple routes
|
|
434
429
|
router.post({
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
430
|
+
'/api/users': () => { /* create user */ },
|
|
431
|
+
'/api/users/:id': ({matched}) => { /* update user */ },
|
|
432
|
+
'/api/users/:id/posts': ({matched}) => { /* get user posts */ }
|
|
438
433
|
})
|
|
439
434
|
|
|
440
435
|
// All HTTP methods supported
|
|
@@ -452,8 +447,8 @@ router.method('CUSTOM', pattern, handler)
|
|
|
452
447
|
|
|
453
448
|
// With prefix option
|
|
454
449
|
router.get({
|
|
455
|
-
|
|
456
|
-
|
|
450
|
+
'/users': listUsers,
|
|
451
|
+
'/users/:id': getUser
|
|
457
452
|
}, {prefix: '/api'}) // Routes become /api/users, /api/users/:id
|
|
458
453
|
```
|
|
459
454
|
|
|
@@ -482,21 +477,21 @@ import cors from 'cors'
|
|
|
482
477
|
import helmet from 'helmet'
|
|
483
478
|
|
|
484
479
|
chain(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
})
|
|
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'})
|
|
499
493
|
})
|
|
494
|
+
})
|
|
500
495
|
)
|
|
501
496
|
```
|
|
502
497
|
|
|
@@ -506,15 +501,15 @@ Pure functions for custom implementations:
|
|
|
506
501
|
|
|
507
502
|
```javascript
|
|
508
503
|
import {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
504
|
+
setBufferBodyDefaultOptions,
|
|
505
|
+
bufferFromReq, jsonFromReq, rawFromReq,
|
|
506
|
+
textFromReq, urlEncodedFromReq, queryFromReq
|
|
512
507
|
} from 'dx-server/helpers'
|
|
513
508
|
|
|
514
509
|
// Set global defaults
|
|
515
510
|
setBufferBodyDefaultOptions({
|
|
516
|
-
|
|
517
|
-
|
|
511
|
+
bodyLimit: 10 * 1024 * 1024, // 10MB
|
|
512
|
+
queryParser: (search) => myCustomParser(search)
|
|
518
513
|
})
|
|
519
514
|
|
|
520
515
|
// Use directly with req/res (no context required)
|
|
@@ -529,9 +524,9 @@ Always set appropriate body size limits to prevent DoS attacks:
|
|
|
529
524
|
|
|
530
525
|
```javascript
|
|
531
526
|
chain(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
527
|
+
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
|
|
528
|
+
// or globally:
|
|
529
|
+
dxServer(req, res, {bodyLimit: 5 * 1024 * 1024}) // 5MB
|
|
535
530
|
)
|
|
536
531
|
```
|
|
537
532
|
|
|
@@ -540,26 +535,26 @@ Never expose internal errors to clients:
|
|
|
540
535
|
|
|
541
536
|
```javascript
|
|
542
537
|
class AppError extends Error {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
538
|
+
constructor(message, status = 400, code = 'ERROR') {
|
|
539
|
+
super(message)
|
|
540
|
+
this.status = status
|
|
541
|
+
this.code = code
|
|
542
|
+
}
|
|
548
543
|
}
|
|
549
544
|
|
|
550
545
|
chain(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
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
|
+
}
|
|
562
556
|
}
|
|
557
|
+
}
|
|
563
558
|
)
|
|
564
559
|
```
|
|
565
560
|
|
|
@@ -568,14 +563,14 @@ Always validate input data:
|
|
|
568
563
|
|
|
569
564
|
```javascript
|
|
570
565
|
router.post('/api/users', async () => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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...
|
|
579
574
|
})
|
|
580
575
|
```
|
|
581
576
|
|
|
@@ -587,13 +582,13 @@ import helmet from 'helmet'
|
|
|
587
582
|
import cors from 'cors'
|
|
588
583
|
|
|
589
584
|
chain(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
585
|
+
connectMiddlewares(
|
|
586
|
+
helmet(),
|
|
587
|
+
cors({
|
|
588
|
+
origin: process.env.ALLOWED_ORIGINS?.split(','),
|
|
589
|
+
credentials: true
|
|
590
|
+
})
|
|
591
|
+
)
|
|
597
592
|
)
|
|
598
593
|
```
|
|
599
594
|
|
|
@@ -604,14 +599,14 @@ chain(
|
|
|
604
599
|
import busboy from 'busboy'
|
|
605
600
|
|
|
606
601
|
router.post('/upload', () => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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)
|
|
615
610
|
})
|
|
616
611
|
```
|
|
617
612
|
|
|
@@ -622,11 +617,11 @@ import {WebSocketServer} from 'ws'
|
|
|
622
617
|
const wss = new WebSocketServer({noServer: true})
|
|
623
618
|
|
|
624
619
|
server.on('upgrade', (request, socket, head) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
620
|
+
if (request.url === '/ws') {
|
|
621
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
622
|
+
wss.emit('connection', ws, request)
|
|
623
|
+
})
|
|
624
|
+
}
|
|
630
625
|
})
|
|
631
626
|
```
|
|
632
627
|
|
|
@@ -635,12 +630,12 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
635
630
|
import rateLimit from 'express-rate-limit'
|
|
636
631
|
|
|
637
632
|
chain(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
633
|
+
connectMiddlewares(
|
|
634
|
+
rateLimit({
|
|
635
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
636
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
|
637
|
+
})
|
|
638
|
+
)
|
|
644
639
|
)
|
|
645
640
|
```
|
|
646
641
|
|
|
@@ -661,14 +656,14 @@ chain(
|
|
|
661
656
|
```javascript
|
|
662
657
|
// Express
|
|
663
658
|
app.get('/users/:id', (req, res) => {
|
|
664
|
-
|
|
665
|
-
|
|
659
|
+
const {id} = req.params
|
|
660
|
+
res.json({userId: id})
|
|
666
661
|
})
|
|
667
662
|
|
|
668
663
|
// dx-server
|
|
669
664
|
router.get('/users/:id', ({matched}) => {
|
|
670
|
-
|
|
671
|
-
|
|
665
|
+
const {id} = matched.pathname.groups
|
|
666
|
+
setJson({userId: id})
|
|
672
667
|
})
|
|
673
668
|
```
|
|
674
669
|
|