dx-server 0.9.0 → 0.10.1
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 +246 -252
- package/cjs/body.d.ts +1 -1
- package/cjs/bodyHelpers.js +2 -2
- package/cjs/connect.d.ts +1 -1
- package/cjs/connect.js +1 -1
- package/cjs/dx.d.ts +1 -1
- package/cjs/dx.js +20 -11
- package/cjs/dxHelpers.d.ts +1 -1
- package/cjs/dxHelpers.js +8 -3
- package/cjs/express.js +14 -12
- package/cjs/polyfillWithResolvers.js +2 -3
- package/cjs/router.js +4 -3
- package/cjs/static.d.ts +5 -0
- package/cjs/static.js +8 -8
- package/cjs/staticHelpers.d.ts +15 -3
- package/cjs/staticHelpers.js +190 -8
- package/cjs/stream.js +2 -2
- 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 +1 -1
- package/esm/bodyHelpers.js +2 -2
- package/esm/connect.d.ts +1 -1
- package/esm/connect.js +1 -1
- package/esm/dx.d.ts +1 -1
- package/esm/dx.js +20 -11
- package/esm/dxHelpers.d.ts +1 -1
- package/esm/dxHelpers.js +8 -3
- package/esm/express.js +14 -12
- package/esm/polyfillWithResolvers.js +2 -3
- package/esm/router.js +4 -3
- package/esm/static.d.ts +5 -0
- package/esm/static.js +8 -8
- package/esm/staticHelpers.d.ts +15 -3
- package/esm/staticHelpers.js +190 -8
- package/esm/stream.js +2 -2
- 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
|
|
|
@@ -144,16 +138,15 @@ new Server().on('request', (req, res) => chain(
|
|
|
144
138
|
|
|
145
139
|
This example requires: `npm install express morgan helmet cors`
|
|
146
140
|
|
|
147
|
-
|
|
148
141
|
```javascript
|
|
149
142
|
import {Server} from 'node:http'
|
|
150
143
|
import {promisify} from 'node:util'
|
|
151
144
|
import chain from 'jchain'
|
|
152
145
|
import dxServer, {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
getReq, getRes,
|
|
147
|
+
getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
|
|
148
|
+
setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
|
|
149
|
+
router, connectMiddlewares, chainStatic, makeDxContext
|
|
157
150
|
} from 'dx-server'
|
|
158
151
|
import {expressApp} from 'dx-server/express'
|
|
159
152
|
import express from 'express'
|
|
@@ -161,96 +154,96 @@ import morgan from 'morgan'
|
|
|
161
154
|
|
|
162
155
|
// it is best practice to create custom error class for non-system error
|
|
163
156
|
class ServerError extends Error {
|
|
164
|
-
|
|
157
|
+
name = 'ServerError'
|
|
165
158
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
159
|
+
constructor(message, status = 400, code = 'unknown') {
|
|
160
|
+
super(message)
|
|
161
|
+
this.status = status
|
|
162
|
+
this.code = code
|
|
163
|
+
}
|
|
171
164
|
}
|
|
172
165
|
|
|
173
166
|
const authContext = makeDxContext(async () => {
|
|
174
|
-
|
|
167
|
+
if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
|
|
175
168
|
})
|
|
176
169
|
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
function requireAuth() {
|
|
171
|
+
if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
|
|
179
172
|
}
|
|
180
173
|
|
|
181
174
|
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
|
-
|
|
175
|
+
next => {
|
|
176
|
+
// this is the difference between express and dx-server
|
|
177
|
+
// req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
|
|
178
|
+
getRes().setHeader('Cache-Control', 'no-cache')
|
|
179
|
+
return next() // must return or await
|
|
180
|
+
},
|
|
181
|
+
async next => {// global error catching for all following middlewares
|
|
182
|
+
try {
|
|
183
|
+
await next()
|
|
184
|
+
} catch (e) {// only app error message should be shown to user
|
|
185
|
+
if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
|
|
186
|
+
else {// report system error
|
|
187
|
+
console.error(e)
|
|
188
|
+
setHtml('internal server error (code: internal)', {status: 500})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
connectMiddlewares(
|
|
193
|
+
morgan('common'),
|
|
194
|
+
// cors(),
|
|
195
|
+
),
|
|
196
|
+
await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
|
|
197
|
+
app.set('trust proxy', true)
|
|
198
|
+
if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
|
|
199
|
+
app.use('/public', express.static('public'))
|
|
200
|
+
}),
|
|
201
|
+
authContext.chain(), // chain context will set the context value to authContext.value in every request
|
|
202
|
+
router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
|
|
203
|
+
try {
|
|
204
|
+
await next()
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (e instanceof ServerError) setJson({// only app error message should be shown to user
|
|
207
|
+
error: e.message,
|
|
208
|
+
code: e.code,
|
|
209
|
+
}, {status: e.status})
|
|
210
|
+
else {// report system error
|
|
211
|
+
console.error(e)
|
|
212
|
+
setJson({
|
|
213
|
+
message: 'internal server error',
|
|
214
|
+
code: 'internal'
|
|
215
|
+
}, {status: 500})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
router.post({
|
|
220
|
+
'/api/sample-public-api'() { // sample POST router
|
|
221
|
+
setJson({name: 'joe'})
|
|
222
|
+
},
|
|
223
|
+
'/api/me'() { // sample private router
|
|
224
|
+
requireAuth()
|
|
225
|
+
setJson({name: authContext.value.name})
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
|
|
229
|
+
router.get('/health', () => setText('ok')),
|
|
230
|
+
() => { // not found router
|
|
231
|
+
throw new ServerError('Not found', 404, 'NOT_FOUND')
|
|
232
|
+
},
|
|
240
233
|
)
|
|
241
234
|
|
|
242
235
|
const tcpServer = new Server()
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
236
|
+
.on('request', async (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
await chain(
|
|
239
|
+
dxServer(req, res, {jsonBeautify: process.env.NODE_ENV !== 'production'}), // basic dx-server context
|
|
240
|
+
serverChain,
|
|
241
|
+
)()
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error(e)
|
|
244
|
+
res.end()
|
|
245
|
+
}
|
|
246
|
+
})
|
|
254
247
|
|
|
255
248
|
await promisify(tcpServer.listen.bind(tcpServer))(3000)
|
|
256
249
|
console.log('server is listening at 3000')
|
|
@@ -267,9 +260,9 @@ dx-server uses Node.js AsyncLocalStorage to provide request/response context glo
|
|
|
267
260
|
import {getReq, getRes} from 'dx-server'
|
|
268
261
|
|
|
269
262
|
function someDeepFunction() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
const req = getReq() // No need to pass req through multiple layers
|
|
264
|
+
const res = getRes()
|
|
265
|
+
res.setHeader('X-Custom', 'value')
|
|
273
266
|
}
|
|
274
267
|
```
|
|
275
268
|
|
|
@@ -286,11 +279,11 @@ const text = await getText()
|
|
|
286
279
|
|
|
287
280
|
// Sync usage (requires chaining)
|
|
288
281
|
chain(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
|
|
283
|
+
next => {
|
|
284
|
+
console.log(getJson.value) // Access synchronously
|
|
285
|
+
return next()
|
|
286
|
+
}
|
|
294
287
|
)
|
|
295
288
|
```
|
|
296
289
|
|
|
@@ -303,21 +296,21 @@ import {makeDxContext, getReq} from 'dx-server'
|
|
|
303
296
|
|
|
304
297
|
// Create auth context
|
|
305
298
|
const authContext = makeDxContext(async () => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
299
|
+
const token = getReq().headers.authorization
|
|
300
|
+
if (!token) return null
|
|
301
|
+
return await validateToken(token) // Your validation logic
|
|
309
302
|
})
|
|
310
303
|
|
|
311
304
|
// Use in middleware
|
|
312
305
|
chain(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
return next()
|
|
306
|
+
authContext.chain(), // Initialize for all requests
|
|
307
|
+
next => {
|
|
308
|
+
if (!authContext.value) {
|
|
309
|
+
setJson({error: 'Unauthorized'}, {status: 401})
|
|
310
|
+
return
|
|
320
311
|
}
|
|
312
|
+
return next()
|
|
313
|
+
}
|
|
321
314
|
)
|
|
322
315
|
|
|
323
316
|
```
|
|
@@ -328,18 +321,18 @@ chain(
|
|
|
328
321
|
|
|
329
322
|
```javascript
|
|
330
323
|
import dxServer, {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
324
|
+
// Request/Response access
|
|
325
|
+
getReq, getRes,
|
|
326
|
+
|
|
327
|
+
// Request body parsers
|
|
328
|
+
getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
|
|
329
|
+
|
|
330
|
+
// Response setters
|
|
331
|
+
setHtml, setJson, setText, setEmpty, setBuffer, setRedirect,
|
|
332
|
+
setNodeStream, setWebStream, setFile,
|
|
333
|
+
|
|
334
|
+
// Utilities
|
|
335
|
+
router, connectMiddlewares, chainStatic, makeDxContext
|
|
343
336
|
} from 'dx-server'
|
|
344
337
|
|
|
345
338
|
// Express integration (requires express installed)
|
|
@@ -347,9 +340,9 @@ import {expressApp, expressRouter} from 'dx-server/express'
|
|
|
347
340
|
|
|
348
341
|
// Low-level helpers
|
|
349
342
|
import {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
343
|
+
setBufferBodyDefaultOptions,
|
|
344
|
+
bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
|
|
345
|
+
urlEncodedFromReq, queryFromReq,
|
|
353
346
|
} from 'dx-server/helpers'
|
|
354
347
|
```
|
|
355
348
|
|
|
@@ -372,9 +365,9 @@ All body parsers are async, lazy-loaded, and cached per request:
|
|
|
372
365
|
Options:
|
|
373
366
|
```typescript
|
|
374
367
|
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
368
|
+
bodyLimit?: number // Max body size in bytes (default: 100KB)
|
|
369
|
+
urlEncodedParser?: (search: string) => any
|
|
370
|
+
queryParser?: (search: string) => any
|
|
378
371
|
}
|
|
379
372
|
```
|
|
380
373
|
|
|
@@ -409,11 +402,12 @@ Options:
|
|
|
409
402
|
- **`chainStatic(pattern, options)`** - Serve static files
|
|
410
403
|
```javascript
|
|
411
404
|
chainStatic('/public/*', {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
405
|
+
root: '/path/to/files',
|
|
406
|
+
getPathname(matched){return matched.pathname}, // take URLPattern matched object, epects to return the file path
|
|
407
|
+
// the returned file path must be run through decodeURIComponent before returning
|
|
408
|
+
dotfiles: 'deny',
|
|
409
|
+
disableEtag: false,
|
|
410
|
+
lastModified: true
|
|
417
411
|
})
|
|
418
412
|
```
|
|
419
413
|
|
|
@@ -426,15 +420,15 @@ import {router} from 'dx-server'
|
|
|
426
420
|
|
|
427
421
|
// Single route
|
|
428
422
|
router.get('/users/:id', ({matched}) => {
|
|
429
|
-
|
|
430
|
-
|
|
423
|
+
const {id} = matched.pathname.groups
|
|
424
|
+
setJson({userId: id})
|
|
431
425
|
})
|
|
432
426
|
|
|
433
427
|
// Multiple routes
|
|
434
428
|
router.post({
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
429
|
+
'/api/users'() { /* create user */ },
|
|
430
|
+
'/api/users/:id'({matched}) { /* update user */ },
|
|
431
|
+
'/api/users/:id/posts'({matched}) { /* get user posts */ }
|
|
438
432
|
})
|
|
439
433
|
|
|
440
434
|
// All HTTP methods supported
|
|
@@ -452,8 +446,8 @@ router.method('CUSTOM', pattern, handler)
|
|
|
452
446
|
|
|
453
447
|
// With prefix option
|
|
454
448
|
router.get({
|
|
455
|
-
|
|
456
|
-
|
|
449
|
+
'/users': listUsers,
|
|
450
|
+
'/users/:id': getUser
|
|
457
451
|
}, {prefix: '/api'}) // Routes become /api/users, /api/users/:id
|
|
458
452
|
```
|
|
459
453
|
|
|
@@ -482,21 +476,21 @@ import cors from 'cors'
|
|
|
482
476
|
import helmet from 'helmet'
|
|
483
477
|
|
|
484
478
|
chain(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
})
|
|
479
|
+
// Use entire Express app
|
|
480
|
+
await expressApp(app => {
|
|
481
|
+
app.set('trust proxy', true)
|
|
482
|
+
app.set('json spaces', 2)
|
|
483
|
+
app.use(helmet())
|
|
484
|
+
app.use('/static', express.static('public'))
|
|
485
|
+
}),
|
|
486
|
+
|
|
487
|
+
// Or use Express router
|
|
488
|
+
expressRouter(router => {
|
|
489
|
+
router.use(cors())
|
|
490
|
+
router.get('/legacy', (req, res) => {
|
|
491
|
+
res.json({message: 'Express route'})
|
|
499
492
|
})
|
|
493
|
+
})
|
|
500
494
|
)
|
|
501
495
|
```
|
|
502
496
|
|
|
@@ -506,15 +500,15 @@ Pure functions for custom implementations:
|
|
|
506
500
|
|
|
507
501
|
```javascript
|
|
508
502
|
import {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
503
|
+
setBufferBodyDefaultOptions,
|
|
504
|
+
bufferFromReq, jsonFromReq, rawFromReq,
|
|
505
|
+
textFromReq, urlEncodedFromReq, queryFromReq
|
|
512
506
|
} from 'dx-server/helpers'
|
|
513
507
|
|
|
514
508
|
// Set global defaults
|
|
515
509
|
setBufferBodyDefaultOptions({
|
|
516
|
-
|
|
517
|
-
|
|
510
|
+
bodyLimit: 10 * 1024 * 1024, // 10MB
|
|
511
|
+
queryParser(search){return myCustomParser(search)}
|
|
518
512
|
})
|
|
519
513
|
|
|
520
514
|
// Use directly with req/res (no context required)
|
|
@@ -529,9 +523,9 @@ Always set appropriate body size limits to prevent DoS attacks:
|
|
|
529
523
|
|
|
530
524
|
```javascript
|
|
531
525
|
chain(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
526
|
+
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
|
|
527
|
+
// or globally:
|
|
528
|
+
dxServer(req, res, {bodyLimit: 5 * 1024 * 1024}) // 5MB
|
|
535
529
|
)
|
|
536
530
|
```
|
|
537
531
|
|
|
@@ -540,26 +534,26 @@ Never expose internal errors to clients:
|
|
|
540
534
|
|
|
541
535
|
```javascript
|
|
542
536
|
class AppError extends Error {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
537
|
+
constructor(message, status = 400, code = 'ERROR') {
|
|
538
|
+
super(message)
|
|
539
|
+
this.status = status
|
|
540
|
+
this.code = code
|
|
541
|
+
}
|
|
548
542
|
}
|
|
549
543
|
|
|
550
544
|
chain(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
545
|
+
async next => {
|
|
546
|
+
try {
|
|
547
|
+
await next()
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error instanceof AppError) {
|
|
550
|
+
setJson({error: error.message, code: error.code}, {status: error.status})
|
|
551
|
+
} else {
|
|
552
|
+
console.error(error) // Log for debugging
|
|
553
|
+
setJson({error: 'Internal server error'}, {status: 500})
|
|
554
|
+
}
|
|
562
555
|
}
|
|
556
|
+
}
|
|
563
557
|
)
|
|
564
558
|
```
|
|
565
559
|
|
|
@@ -568,14 +562,14 @@ Always validate input data:
|
|
|
568
562
|
|
|
569
563
|
```javascript
|
|
570
564
|
router.post('/api/users', async () => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
565
|
+
const data = await getJson()
|
|
566
|
+
|
|
567
|
+
// Validate
|
|
568
|
+
if (!data?.email || !isValidEmail(data.email)) {
|
|
569
|
+
throw new AppError('Invalid email', 400, 'INVALID_EMAIL')
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Process...
|
|
579
573
|
})
|
|
580
574
|
```
|
|
581
575
|
|
|
@@ -587,13 +581,13 @@ import helmet from 'helmet'
|
|
|
587
581
|
import cors from 'cors'
|
|
588
582
|
|
|
589
583
|
chain(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
584
|
+
connectMiddlewares(
|
|
585
|
+
helmet(),
|
|
586
|
+
cors({
|
|
587
|
+
origin: process.env.ALLOWED_ORIGINS?.split(','),
|
|
588
|
+
credentials: true
|
|
589
|
+
})
|
|
590
|
+
)
|
|
597
591
|
)
|
|
598
592
|
```
|
|
599
593
|
|
|
@@ -604,14 +598,14 @@ chain(
|
|
|
604
598
|
import busboy from 'busboy'
|
|
605
599
|
|
|
606
600
|
router.post('/upload', () => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
601
|
+
const req = getReq()
|
|
602
|
+
const bb = busboy({headers: req.headers, limits: {fileSize: 10 * 1024 * 1024}})
|
|
603
|
+
|
|
604
|
+
bb.on('file', (name, file, info) => {
|
|
605
|
+
// Handle file stream
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
req.pipe(bb)
|
|
615
609
|
})
|
|
616
610
|
```
|
|
617
611
|
|
|
@@ -622,11 +616,11 @@ import {WebSocketServer} from 'ws'
|
|
|
622
616
|
const wss = new WebSocketServer({noServer: true})
|
|
623
617
|
|
|
624
618
|
server.on('upgrade', (request, socket, head) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
619
|
+
if (request.url === '/ws') {
|
|
620
|
+
wss.handleUpgrade(request, socket, head, ws => {
|
|
621
|
+
wss.emit('connection', ws, request)
|
|
622
|
+
})
|
|
623
|
+
}
|
|
630
624
|
})
|
|
631
625
|
```
|
|
632
626
|
|
|
@@ -635,12 +629,12 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
635
629
|
import rateLimit from 'express-rate-limit'
|
|
636
630
|
|
|
637
631
|
chain(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
632
|
+
connectMiddlewares(
|
|
633
|
+
rateLimit({
|
|
634
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
635
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
|
636
|
+
})
|
|
637
|
+
)
|
|
644
638
|
)
|
|
645
639
|
```
|
|
646
640
|
|
|
@@ -661,14 +655,14 @@ chain(
|
|
|
661
655
|
```javascript
|
|
662
656
|
// Express
|
|
663
657
|
app.get('/users/:id', (req, res) => {
|
|
664
|
-
|
|
665
|
-
|
|
658
|
+
const {id} = req.params
|
|
659
|
+
res.json({userId: id})
|
|
666
660
|
})
|
|
667
661
|
|
|
668
662
|
// dx-server
|
|
669
663
|
router.get('/users/:id', ({matched}) => {
|
|
670
|
-
|
|
671
|
-
|
|
664
|
+
const {id} = matched.pathname.groups
|
|
665
|
+
setJson({userId: id})
|
|
672
666
|
})
|
|
673
667
|
```
|
|
674
668
|
|