sleepy-serv 0.3.0 → 0.4.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 CHANGED
@@ -73,19 +73,23 @@ The route's logic is implemented in the route definition files. These must `expo
73
73
  Here's an example of a simple handler function:
74
74
 
75
75
  ```js
76
- export default function (req, res) {
76
+ export default function (req, res, next) {
77
77
  console.log('res:', res)
78
+ console.log('next:', next) // null for route handlers
78
79
 
79
80
  return new Response('Hello world')
80
81
  }
81
82
  ```
82
83
 
83
- These functions take two parameters:
84
+ These functions take three parameters:
84
85
  - `req`: a `BunRequest` object
85
86
  - `res`: a results object
87
+ - `next`: a function to call the next middleware in the chain (or `null` for the final handler)
86
88
 
87
89
  The `res` parameter might seem familiar if coming from ExpressJS, but this parameter has a different purpose. The `res` parameter is meant to act as a dedicated, persistent object that middleware can write their results to. This way, the `req` object stays as a lean `BunRequest`.
88
90
 
91
+ The `next` parameter is a function that allows middleware to pass control to the next function in the middleware chain. For route handlers (the final function in the chain), `next` will be `null`.
92
+
89
93
  These handlers can also be `async` functions and `sleepy-serv` will wait for them to finish before moving on.
90
94
 
91
95
  ### Middleware
@@ -94,10 +98,11 @@ As mentioned earlier, method definition files can also export an array of functi
94
98
 
95
99
  ```js
96
100
  export default [
97
- async (req, res) => {
101
+ async (req, res, next) => {
98
102
  res.body = await req.json()
103
+ return next() /* call the next middleware */
99
104
  },
100
- (req, res) => {
105
+ (req, res) => { /* notice that the last function doesn't take `next` */
101
106
  console.log('JSON body:', res.body)
102
107
 
103
108
  return new Response('Hello world')
@@ -105,9 +110,58 @@ export default [
105
110
  ]
106
111
  ```
107
112
 
108
- This is useful if you want to break common logic up into reusable functions. The functions in the array are called in-order, and are provided the same `req` object. If you'd like to cache results between middleware functions, you can simply attach them to the `req` object.
113
+ This is useful if you want to break common logic up into reusable functions. The functions in the array are called in-order, and each receives three parameters: `req`, `res`, and `next`.
114
+
115
+ #### Middleware Chain Behavior
116
+
117
+ Each middleware function must do one of the following:
118
+ - **Call `next()`**: Continue to the next middleware in the chain by returning `next()`
119
+ - **Return a Response**: End the chain (early) by returning a `BunResponse` object
120
+ - **Throw an error**: Stop the chain and trigger error handling
121
+
122
+ The `next` parameter will be `null` for the final function in the chain (the route handler), indicating there are no more middleware functions to call.
123
+
124
+ #### Example with Explicit `next()` Calls
125
+
126
+ ```js
127
+ export default [
128
+ (req, res, next) => {
129
+ console.log('1st middleware')
130
+
131
+ if (req.headers.get('authorization')) {
132
+ return next() // continue to next middleware
133
+ } else {
134
+ return new Response('Unauthorized', { status: 401 }) // end chain early
135
+ }
136
+ },
137
+ (req, res, next) => {
138
+ console.log('2nd middleware')
139
+
140
+ res.user = {
141
+ id: 123,
142
+ name: 'John',
143
+ }
144
+
145
+ return next() // continue to route handler
146
+ },
147
+ (req, res) => {
148
+ console.log('Final, "route" handler')
149
+
150
+ return new Response(`Hello ${res.user.name}`)
151
+ },
152
+ ]
153
+ ```
154
+
155
+ #### Middleware Execution Order
156
+
157
+ Middleware functions are executed in the following order:
158
+ 1. **App-level middleware** (defined in `createApp()` options)
159
+ 2. **Directory-level middleware** (from `meta.js` files, from root to leaf)
160
+ 3. **Route-level middleware** (defined in the route file array)
109
161
 
110
- ### Breaking the Middleware Chain - Responses
162
+ Each middleware function receives the same `req` and `res` objects, allowing data to be passed between middleware functions through the `res` object.
163
+
164
+ #### Breaking the Middleware Chain - Responses
111
165
 
112
166
  You might want to break the middleware chain with a response early.
113
167
 
@@ -115,27 +169,29 @@ Here's an example:
115
169
 
116
170
  ```js
117
171
  export default [
118
- req => {
172
+ (req, res, next) => {
119
173
  if (req.params.userId === '123') {
120
174
  return new Response('Returned early')
175
+ } else {
176
+ return next() // continue to next middleware
121
177
  }
122
178
  },
123
- req => {
179
+ (req, res) => {
124
180
  return new Response('End of chain')
125
181
  },
126
182
  ]
127
183
  ```
128
184
 
129
- The the case above, the 1st middleware function will return a `Response` object if the route param's `userId` is equal to a specific value. In those cases, the last function in the middleware chain will not execute for that request.
185
+ In the case above, the 1st middleware function will return a `Response` object if the route param's `userId` is equal to a specific value. In those cases, the last function in the middleware chain will not execute for that request. If the condition is not met, `next()` is called to continue to the next middleware.
130
186
 
131
- ### Breaking the Middleware Chain - Errors
187
+ #### Breaking the Middleware Chain - Errors
132
188
 
133
189
  It's also common to throw errors for things like request validation or when a desired resource is not found. `sleepy-serv` has defined custom `Error` types for every type of 4xx and 5xx error:
134
190
 
135
191
  ```js
136
192
  import { NotFoundError } from 'sleepy-serv'
137
193
 
138
- export default async function (req) {
194
+ export default async function (req, res) {
139
195
  const users = await sql`
140
196
  SELECT * FROM Users
141
197
  WHERE userId=${req.params.userId}
@@ -156,7 +212,7 @@ If the user is found in the database, then the request will return with a succes
156
212
  Throwing generic errors also works too:
157
213
 
158
214
  ```js
159
- export default async function (req) {
215
+ export default async function (req, res) {
160
216
  throw new Error('A problem occurred')
161
217
  }
162
218
  ```
@@ -167,7 +223,7 @@ export default async function (req) {
167
223
 
168
224
  It's also possible for resource directories to contain a `meta.js` file. These files can export various things that have some sort semantic relationship to the part of the route that they're defined in.
169
225
 
170
- ### Directory-Level Middleware
226
+ #### Directory-Level Middleware
171
227
 
172
228
  `meta.js` files can also export an array of middleware functions:
173
229
 
@@ -175,7 +231,10 @@ It's also possible for resource directories to contain a `meta.js` file. These f
175
231
  // meta.js
176
232
 
177
233
  export const middleware = [
178
- req => { /* do middleware things */ },
234
+ (req, res, next) => {
235
+ /* do middleware things */
236
+ return next() // Continue to next middleware
237
+ },
179
238
  ]
180
239
  ```
181
240
 
@@ -223,25 +282,69 @@ The middleware defined in `/api/users/:userId/meta.js` will be applied the follo
223
282
  - `DELETE /users/:userId`
224
283
  - `PUT /users/:userId`
225
284
 
226
- ### Future Use-Cases
285
+ ### Meta Files
227
286
 
228
- At the time of this writing, `meta.js` only exports middleware functions.
287
+ At the time of this writing, `meta.js` only exports middleware functions, but it will eventually be able to provide additional configuration for the endpoint definitions within its scope.
229
288
 
230
- ## Build-In Middleware
289
+ ## Built-In Middleware
231
290
 
232
291
  `sleepy-serv` also comes built-in with a few useful middleware functions that are commonly used.
233
292
 
234
- ### parseJson(req, _res)
293
+ ### parseJson(req, res, next)
294
+
295
+ This middleware parses the request's `body` property as a JSON string, and then stores the results in `res.body`. It calls `next()` if parsing succeeds, and throws a `BadRequestError` if parsing fails. Additional body parsers can be written in the future to accommodate other body encoding schemes (such as XML or protobuf).
235
296
 
236
- This middleware parses the request's `body` property as a JSON string, and then stores the results in `res.body`. Additional body parsers can be written in the future a accommodate other body encoding schemes.
297
+ Example usage:
298
+
299
+ ```js
300
+ import { middleware } from 'sleepy-serv'
301
+
302
+ export default [
303
+ middleware.parseJson,
304
+ (req, res) => {
305
+ console.log('Parsed JSON:', res.body)
306
+
307
+ return new Response('JSON received')
308
+ },
309
+ ]
310
+ ```
237
311
 
238
312
  ### validateSchema(schemas)
239
313
 
240
- This function takes a `schemas` object. Each property is optional:
241
- - `headers`: takes a string formatter schema to evaulate `req.headers`
242
- - `params`: takes a string formatter schema to evaulate `req.params`
243
- - `query`: takes a string foramtter schema to evaulate `req.query`
244
- - `body`: takes a JSON validation schema to evaulate `res.body`
314
+ This function returns a middleware function that validates request data against provided schemas. The returned middleware function has the signature `(req, res, next)` and automatically calls `next()` after successful validation.
315
+
316
+ The `schemas` object can contain these optional properties:
317
+ - `headers`: takes a string formatter schema to evaluate `req.headers`
318
+ - `params`: takes a string formatter schema to evaluate `req.params`
319
+ - `query`: takes a string formatter schema to evaluate `req.query`
320
+ - `body`: takes a JSON validation schema to evaluate `res.body`
321
+
322
+ Example usage:
323
+
324
+ ```js
325
+ import { middleware } from 'sleepy-serv'
326
+
327
+ export default [
328
+ middleware.parseJson,
329
+ middleware.validateSchema({
330
+ params: {
331
+ userId: { type: 'format', value: 'uuid' }
332
+ },
333
+ body: {
334
+ type: 'object',
335
+ properties: {
336
+ name: { type: 'string' },
337
+ email: { type: 'string', format: 'email' }
338
+ },
339
+ required: ['name', 'email']
340
+ }
341
+ }),
342
+ (req, res, next) => {
343
+ // Validation passed, process the request
344
+ return new Response('Data is valid')
345
+ },
346
+ ]
347
+ ```
245
348
 
246
349
  Here's an example of the schema in action:
247
350
 
@@ -295,6 +398,26 @@ Calling `setValidationFormats()` extends the possible values that can be passed
295
398
 
296
399
  ## `createApp()` Options
297
400
 
401
+ ### `middleware`
402
+
403
+ You can define app-level middleware that will be applied to all routes:
404
+
405
+ ```js
406
+ import { middleware } from 'sleepy-serv'
407
+
408
+ const app = await createApp(PORT, import.meta.dirname, {
409
+ middleware: [
410
+ middleware.parseJson,
411
+ (req, res, next) => {
412
+ console.log(`${req.method} ${req.url}`)
413
+ return next()
414
+ },
415
+ ],
416
+ })
417
+ ```
418
+
419
+ App-level middleware is executed before any directory-level or route-level middleware.
420
+
298
421
  ### `hostname`
299
422
 
300
423
  The hostname can be customized like so:
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "sleepy-serv",
3
3
  "description": "A more opinionated web server for building REST-ful applications",
4
4
  "author": "Travis J True",
5
- "version": "0.3.0",
5
+ "version": "0.4.0",
6
6
  "exports": "./src/index.js",
7
7
  "type": "module",
8
8
  "keywords": [
package/src/index.js CHANGED
@@ -45,17 +45,18 @@ function methodNotAllowedHandler (_req) {
45
45
  throw new MethodNotAllowedError()
46
46
  }
47
47
 
48
+ /* TODO: add whitelist support */
48
49
  function validateDirectoryIllegalFiles (targetPath, filenames) {
49
- const hasInvalidFiles = filenames.some(filename =>
50
- !ALLOWED_FILES_ALL.includes(filename)
51
- )
52
-
53
- if (hasInvalidFiles) {
54
- throw new TypeError(`
55
- Directory contains illegal files:
56
- ${targetPath}
57
- `.trim())
58
- }
50
+ // const hasInvalidFiles = filenames.some(filename =>
51
+ // !ALLOWED_FILES_ALL.includes(filename)
52
+ // )
53
+
54
+ // if (hasInvalidFiles) {
55
+ // throw new TypeError(`
56
+ // Directory contains illegal files:
57
+ // ${targetPath}
58
+ // `.trim())
59
+ // }
59
60
  }
60
61
 
61
62
  function validateLeafDirectory (targetPath, filenames, entries) {
@@ -191,13 +192,24 @@ ${route.modulePath}
191
192
 
192
193
  req.query = query ? querystring.parse(query) : {}
193
194
 
194
- for (const fn of middlewareChain) {
195
- const response = await fn(req, res)
195
+ const executeMiddleware = async (index) => {
196
+ const currentMiddleware = middlewareChain[index]
197
+ const isLastMiddleware = index === middlewareChain.length - 1
198
+
199
+ const next = !isLastMiddleware ?
200
+ () => executeMiddleware(index + 1)
201
+ : null
196
202
 
197
- if (response) {
198
- return response
203
+ const result = await currentMiddleware(req, res, next)
204
+
205
+ if (result instanceof Response) {
206
+ return result
207
+ } else {
208
+ throw new TypeError('Handler does not return a Response object')
199
209
  }
200
210
  }
211
+
212
+ return executeMiddleware(0)
201
213
  }
202
214
 
203
215
  return {
package/src/middleware.js CHANGED
@@ -2,6 +2,7 @@ import Ajv from 'ajv'
2
2
  import addFormats from 'ajv-formats'
3
3
 
4
4
  import {
5
+ BadRequestError,
5
6
  UnsupportedMediaTypeError,
6
7
  UnprocessableContentError,
7
8
  } from './errors'
@@ -46,14 +47,26 @@ function buildFormatterSchema (schema) {
46
47
  }
47
48
  }
48
49
 
49
- export async function parseJson (req, res) {
50
+ export async function parseJson (req, res, next) {
50
51
  const contentType = req.headers.get('content-type')
51
52
 
52
- if (req.method !== 'GET' && contentType !== 'application/json') {
53
+ if (!contentType) {
54
+ return next()
55
+ }
56
+
57
+ if (contentType !== 'application/json') {
53
58
  throw new UnsupportedMediaTypeError('content-type')
54
59
  }
55
60
 
56
- res.body = await req.json()
61
+ try {
62
+ res.body = await req.json()
63
+ } catch (err) {
64
+ console.error(err)
65
+
66
+ throw new BadRequestError('Body is invalid JSON')
67
+ }
68
+
69
+ return next()
57
70
  }
58
71
 
59
72
  export function setValidationFormats (formats) {
@@ -61,7 +74,7 @@ export function setValidationFormats (formats) {
61
74
  }
62
75
 
63
76
  export function validateSchema (schemas) {
64
- return function (req, res) {
77
+ return function (req, res, next) {
65
78
  const formattedSchemas = {
66
79
  headers: buildFormatterSchema(schemas.headers),
67
80
  params: buildFormatterSchema(schemas.params),
@@ -97,5 +110,7 @@ export function validateSchema (schemas) {
97
110
  if (errors.length > 0) {
98
111
  throw new UnprocessableContentError(errors)
99
112
  }
113
+
114
+ return next()
100
115
  }
101
116
  }