forked from middyjs/middy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
README.md.hb
542 lines (391 loc) · 18.1 KB
/
README.md.hb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
<div align="center">
<img alt="Middy logo" src="https://raw.githubusercontent.com/middyjs/middy/master/img/middy-logo.png"/>
</div>
<div align="center">
<p><strong>The stylish Node.js middleware engine for AWS Lambda</strong></p>
</div>
<div align="center">
<p>
<a href="http://badge.fury.io/js/middy">
<img src="https://badge.fury.io/js/middy.svg" alt="npm version" style="max-width:100%;">
</a>
<a href="https://circleci.com/gh/middyjs/middy">
<img src="https://circleci.com/gh/middyjs/middy.svg?style=shield" alt="CircleCI" style="max-width:100%;">
</a>
<a href="https://codecov.io/gh/middyjs/middy">
<img src="https://codecov.io/gh/middyjs/middy/branch/master/graph/badge.svg" alt="codecov" style="max-width:100%;">
</a>
<a href="https://snyk.io/test/github/middyjs/middy">
<img src="https://snyk.io/test/github/middyjs/middy/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/middyjs/middy" style="max-width:100%;">
</a>
<a href="https://standardjs.com/">
<img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard Code Style" style="max-width:100%;">
</a>
<a href="https://greenkeeper.io/">
<img src="https://badges.greenkeeper.io/middyjs/middy.svg" alt="Greenkeeper badge" style="max-width:100%;">
</a>
<a href="https://gitter.im/middyjs/Lobby">
<img src="https://badges.gitter.im/gitterHQ/gitter.svg" alt="Chat on Gitter" style="max-width:100%;">
</a>
</p>
</div>
## TOC
- [A little appetizer](#a-little-appetizer)
- [Install](#install)
- [Requirements](#requirements)
- [Why?](#why)
- [Usage](#usage)
- [How it works](#how-it-works)
- [Writing a middleware](#writing-a-middleware)
- [Available middlewares](#available-middlewares)
- [API](#api)
- [Contributing](#contributing)
- [License](#license)
## A little appetizer
Middy is a very simple middleware engine. If you are used to web frameworks like
express, than you will be familiar with the concepts adopted in Middy and you will
be able to get started very quickly.
But code is better than 10,000 words, so let's jump into an example.
Let's assume you are building a JSON API to process a payment:
```javascript
# handler.js
const middy = require('middy')
const { urlEncodeBodyParser, validator, httpErrorHandler } = require('middy/middlewares')
// This is your common handler, in no way different than what you are used to doing every day
// in AWS Lambda
const processPayment = (event, context, callback) => {
// we don't need to deserialize the body ourself as a middleware will be used to do that
const { creditCardNumber, expiryMonth, expiryYear, cvc, nameOnCard, amount } = event.body
// do stuff with this data
// ...
return callback(null, { result: 'success', message: 'payment processed correctly'})
}
// Notice that in the handler you only added base business logic (no deserilization,
// validation or error handler), we will add the rest with middlewares
const inputSchema = {
type: 'object',
properties: {
body: {
type: 'object',
properties: {
creditCardNumber: { type: 'string', minLength: 12, maxLength: 19, pattern: '\d+' },
expiryMonth: { type: 'integer', minimum: 1, maximum: 12 },
expiryYear: { type: 'integer', minimum: 2017, maximum: 2027 },
cvc: { type: 'string', minLength: 3, maxLength: 4, pattern: '\d+' },
nameOnCard: { type: 'string' },
amount: { type: 'number' }
}
}
}
}
// Let's "middyfy" our handler, then we will be able to attach middlewares to it
const handler = middy(processPayment)
.use(urlEncodeBodyParser()) // parses the request body when it's a JSON and converts it to an object
.use(validator({inputSchema})) // validates the input
.use(httpErrorHandler()) // handles common http errors and returns proper responses
module.exports = { handler }
```
## Install
As simple as:
```bash
npm install middy
```
or
```bash
yarn add middy
```
## Requirements
Middy has been built to work by default from **Node >= 6.10**.
If you need to run it in earlier versions of Node (eg. 4.3) then you will have to
*transpile* middy's code yourself using [babel](https://babeljs.io/) or a similar tool.
## Why?
One of the main strengths of serverless and AWS Lambda is that, from a developer
perspective, your focus is mostly shifted toward implementing business logic.
Anyway, when you are writing a handler, you still have to deal with some common technical concerns
outside business logic, like input parsing and validation, output serialization,
error handling, etc.
Very often, all this necessary code ends up polluting the pure business logic code in
your handlers, making the code harder to read and to maintain.
In other contexts, like generic web frameworks ([express](http://expressjs.com/),
[fastify](http://fastify.io), [hapi](https://hapijs.com/), etc.), this
problem has been solved using the [middleware pattern](https://www.packtpub.com/mapt/book/web_development/9781783287314/4/ch04lvl1sec33/middleware).
This pattern allows developers to isolate these common technical concerns into
*"steps"* that *decorate* the main business logic code.
Middleware functions are generally written as independent modules and then plugged in into
the application in a configuration step, thus not polluting the main business logic
code that remains clean, readable and easy to maintain.
Since we couldn't find a similar approach for AWS Lambda handlers, we decided
to create middy, our own middleware framework for serverless in AWS land.
## Usage
As you might have already got from our first example here, using middy is very
simple and requires just few steps:
1. Write your Lambda handlers as usual, focusing mostly on implementing the bare
business logic for them.
2. Import `middy` and all the middlewares you want to use
3. Wrap your handler in the `middy()` factory function. This will return a new
enhanced instance of your original handler, to which you will be able to attach
the middlewares you need.
4. Attach all the middlewares you need using the function `.use(somemiddleware())`
Example:
```javascript
const middy = require('middy')
const { middleware1, middleware2, middleware3 } = require('middy/middlewares')
const originalHandler = (event, context, callback) => { /* your business logic */ }
const handler = middy(originalHandler)
handler
.use(middleware1())
.use(middleware2())
.use(middleware3())
module.exports = { handler }
```
You can also attach [inline middlewares](#inline-middlewares) by using the functions `.before`, `.after` and
`.onError`.
For a more detailed use case and examples check the [Writing a middleware section](#writing-a-middleware) and
the [API section](#api).
## How it works
Middy implements the classic *onion-like* middleware pattern, with some peculiar details.
![Middy middleware engine diagram](/img/middy-middleware-engine.png)
When you attach a new middleware this will wrap the business logic contained in the handler
in two separate steps.
When another middleware is attached this will wrap the handler again and it will be wrapped by
all the previously added middlewares in order, creating multiple layers for interacting with
the *request* (event) and the *response*.
This way the *request-response cycle* flows through all the middlewares, the
handler and all the middlewares again, giving the opportunity within every step to
modify or enrich the current request, context or the response.
### Execution order
Middlewares have two phases: `before` and `after`.
The `before` phase, happens *before* the handler is executed. In this code the
response is not created yet, so you will have access only to the request.
The `after` phase, happens *after* the handler is executed. In this code you will
have access to both the request and the response.
If you have three middlewares attached as in the image above this is the expected
order of execution:
- `middleware1` (before)
- `middleware2` (before)
- `middleware3` (before)
- `handler`
- `middleware3` (after)
- `middleware2` (after)
- `middleware1` (after)
Notice that in the `after` phase, middlewares are executed in inverted order,
this way the first handler attached is the one with the highest priority as it will
be the first able to change the request and last able to modify the response before
it gets sent to the user.
### Interrupt middleware execution early
Some middlewares might need to stop the whole execution flow and return a response immediately.
If you want to do this you can invoke `handler.callback` in your middleware and return early without invoking `next`.
**Note**: this will totally stop the execution of successive middlewares in any phase (`before` and `after`) and returns
an early response (or an error) directly at the Lambda level. If your middlewares do a specific task on every request
like output serialization or error handling, these won't be invoked in this case.
In this example we can use this capability for building a sample caching middleware:
```javascript
// some function that calculates the cache id based on the current event
const calculateCacheId = (event) => { /* ... */ }
const storage = {}
// middleware
const cacheMiddleware = (options) => {
let cacheKey
return ({
before: (handler, next) => {
cacheKey = options.calculateCacheId(handler.event)
if (options.storage.hasOwnProperty(cacheKey)) {
// exits early and returns the value from the cache if it's already there
return handler.callback(null, options.storage[cacheKey])
}
return next()
},
after: (handler, next) => {
// stores the calculated response in the cache
options.storage[cacheKey] = handler.response
next()
}
})
}
// sample usage
const handler = middy((event, context, callback) => { /* ... */ })
.use(cacheMiddleware({
calculateCacheId, storage
}))
```
### Handling errors
But what happens when there is an error?
When there is an error, the regular control flow is stopped and the execution is
moved back to all the middlewares that implements a special phase called `onError`, following
the order they have been attached.
Every `onError` middleware can decide to handle the error and create a proper response or
to delegate the error to the next middleware.
When a middleware handles the error and creates a response, the execution is still propagated to all the other
error middlewares and they have a chance to update or replace the response as
needed. At the end of the error middlewares sequence, the response is returned
to the user.
If no middleware manages the error, the Lambda execution fails reporting the unmanaged error.
### Promise support
Middy allows you to return promises (or throw errors) from your handlers (instead of calling `callback()`) and middlewares
(instead of calling `next()`).
Here is an example of a handler that returns a promise:
```javascript
middy((event, context, callback) => {
return someAsyncStuff()
.then(() => {
return someOtherAsyncStuff()
})
.then(() => {
return {foo: bar}
}
})
```
And here is an example of a middleware that returns a similar promise:
```javascript
const asyncValidator = () => {
before: (handler) => {
if (handler.event.body) {
return someAsyncStuff(handler.event.body)
.then(() => {
return {foo: bar}
})
}
return Promise.resolve()
}
}
handler.use(asyncValidator())
```
### Using async/await
Node.js 8.10 supports [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function),
allowing you to work with promises in a way that makes handling asynchronous logic easier to reason about and
asynchronous code easier to read.
You can still use async/await if you're running AWS Lambda on Node.js 6.10, but you will need to transpile your
`async/await` code (e.g. using [babel](https://babeljs.io/)).
Take the following code as an example of a handler written with async/await:
```javascript
middy(async (event, context) => {
await someAsyncStuff()
await someOtherAsyncStuff()
return ({foo: bar})
})
```
And here is an example of a middleware written with async/await:
```javascript
const asyncValidator = () => {
before: async (handler) => {
if (handler.event.body) {
await asyncValidate(handler.event.body)
return {foo: bar}
}
return
}
}
handler.use(asyncValidator())
```
## Writing a middleware
A middleware is an object that should contain at least 1 of 3 possible keys:
1. `before`: a function that is executed in the before phase
2. `after`: a function that is executed in the after phase
3. `onError`: a function that is executed in case of errors
`before`, `after` and `onError` functions need to have the following signature:
```javascript
function (handler, next) {
// ...
}
```
Where:
- `handler`: is a reference to the current context and it allows access to (and modification of)
the current `event` (request), the `response` (in the *after* phase) and `error`
(in case of an error).
- `next`: is a callback function that needs to be invoked when the middleware has finished
its job so that the next middleware can be invoked
### Configurable middlewares
In order to make middlewares configurable they are generally exported as a function that accepts
a configuration object. This function should then return the middleware object with `before`,
`after` and `onError` as keys.
E.g.
```javascript
# myMiddleware.js
const myMiddleware = (config) => {
// might set default options in config
return ({
before: (handler, next) => {
// might read options from `config`
},
after: (handler, next) => {
// might read options from `config`
},
onError: (handler, next) => {
// might read options from `config`
}
})
}
module.exports = myMiddleware
```
With this convention in mind, using a middleware will always look like the following example:
```javascript
const middy = require('middy')
const myMiddleware = require('myMiddleware')
const handler = middy((event, context, callback) => {
// do stuff
})
handler.use(myMiddleware({
option1: 'foo',
option2: 'bar'
}))
module.exports = { handler }
```
### Inline middlewares
Sometimes you want to create handlers that serve a very small need and that are not
necessarily re-usable. In such cases you probably will need to hook only into one of
the different phases (`before`, `after` or `onError`).
In these cases you can use **inline middlewares** which are shortcut functions to hook
logic into Middy's control flow.
Let's see how inline middlewares work with a simple example:
```javascript
const middy = require('middy')
const handler = middy((event, context, callback) => {
// do stuff
})
handler.before((handler, next) => {
// do something in the before phase
next()
})
handler.after((handler, next) => {
// do something in the after phase
next()
})
handler.onError((handler, next) => {
// do something in the on error phase
next()
})
module.exports = { handler }
```
As you can see above, a middy instance also exposes the `before`, `after` and `onError`
methods to allow you to quickly hook-in simple inline middlewares.
### More details on creating middlewares
Check the [code for existing middlewares](/src/middlewares) to see more examples
on how to write a middleware.
## Available middlewares
Currently available middlewares:
- [`cache`](/docs/middlewares.md#cache): A simple but flexible caching layer
- [`cors`](/docs/middlewares.md#cors): Sets CORS headers on response
- [`doNotWaitForEmptyEventLoop`](/docs/middlewares.md#donotwaitforemptyeventloop): Sets callbackWaitsForEmptyEventLoop property to false
- [`httpContentNegotiation`](/docs/middlewares.md#httpcontentnegotiation): Parses `Accept-*` headers and provides utilities for content negotiation (charset, encoding, language and media type) for HTTP requests
- [`httpErrorHandler`](/docs/middlewares.md#httperrorhandler): Creates a proper HTTP response for errors that are created with the [http-errors](https://www.npmjs.com/package/http-errors) module and represents proper HTTP errors.
- [`httpEventNormalizer`](/docs/middlewares.md#httpEventNormalizer): Normalizes HTTP events by adding an empty object for `queryStringParameters` and `pathParameters` if they are missing.
- [`httpHeaderNormalizer`](/docs/middlewares.md#httpheadernormalizer): Normalizes HTTP header names to their canonical format
- [`httpPartialResponse`](/docs/middlewares.md#httppartialresponse): Filter response objects attributes based on query string parameters.
- [`jsonBodyParser`](/docs/middlewares.md#jsonbodyparser): Automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of
`httpErrorHandler`.
- [`s3KeyNormalizer`](/docs/middlewares.md#s3keynormalizer): Normalizes key names in s3 events.
- [`ssm`](/docs/middlewares.md#ssm): Fetches parameters from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html).
- [`validator`](/docs/middlewares.md#validator): Automatically validates incoming events and outgoing responses against custom schemas
- [`urlEncodeBodyParser`](/docs/middlewares.md#urlencodebodyparser): Automatically parses HTTP requests with URL encoded body (typically the result of a form submit).
- [`warmup`](/docs/middlewares.md#warmup): Warmup middleware that helps to reduce the [cold-start issue](https://serverless.com/blog/keep-your-lambdas-warm/)
For dedicated documentation on available middlewares check out the [Middlewares
documentation](/docs/middlewares.md)
## Api
{{> main}}
## Contributing
Everyone is very welcome to contribute to this repository. Feel free to [raise issues](https://github.com/middyjs/middy/issues) or to [submit Pull Requests](https://github.com/middyjs/middy/pulls).
## License
Licensed under [MIT License](LICENSE). Copyright (c) 2017-2018 Luciano Mammino and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).
<a href="https://app.fossa.io/projects/git%2Bgithub.com%2Fmiddyjs%2Fmiddy?ref=badge_large">
<img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmiddyjs%2Fmiddy.svg?type=large" alt="FOSSA Status" style="max-width:100%;">
</a>