diff --git a/package-lock.json b/package-lock.json
index 0e2ae13ab..10a804dc6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2488,6 +2488,10 @@
"resolved": "packages/appconfig",
"link": true
},
+ "node_modules/@middy/cloudformation-response": {
+ "resolved": "packages/cloudformation-response",
+ "link": true
+ },
"node_modules/@middy/cloudformation-router": {
"resolved": "packages/cloudformation-router",
"link": true
@@ -11319,6 +11323,22 @@
"url": "https://github.com/sponsors/willfarrell"
}
},
+ "packages/cloudformation-response": {
+ "name": "@middy/cloudformation-response",
+ "version": "6.0.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@middy/core": "6.0.0",
+ "@types/aws-lambda": "^8.10.100"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/willfarrell"
+ }
+ },
"packages/cloudformation-router": {
"name": "@middy/cloudformation-router",
"version": "6.0.0",
diff --git a/packages/cloudformation-response/README.md b/packages/cloudformation-response/README.md
new file mode 100644
index 000000000..beb6620f2
--- /dev/null
+++ b/packages/cloudformation-response/README.md
@@ -0,0 +1,46 @@
+
+
+## License
+
+Licensed under [MIT License](LICENSE). Copyright (c) 2017-2024 [Luciano Mammino](https://github.com/lmammino), [will Farrell](https://github.com/willfarrell), and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).
+
+
+
+
diff --git a/packages/cloudformation-response/__benchmarks__/index.js b/packages/cloudformation-response/__benchmarks__/index.js
new file mode 100644
index 000000000..42c8ee848
--- /dev/null
+++ b/packages/cloudformation-response/__benchmarks__/index.js
@@ -0,0 +1,27 @@
+import { Bench } from 'tinybench'
+import middy from '../../core/index.js'
+import middleware from '../index.js'
+
+const bench = new Bench({ time: 1_000 })
+
+const context = {
+ getRemainingTimeInMillis: () => 30000
+}
+const setupHandler = () => {
+ const baseHandler = () => {}
+ return middy(baseHandler).use(middleware())
+}
+
+const coldHandler = setupHandler()
+
+const event = {}
+await bench
+ .add('without cache', async () => {
+ try {
+ await coldHandler(event, context)
+ } catch (e) {}
+ })
+
+ .run()
+
+console.table(bench.table())
diff --git a/packages/cloudformation-response/__tests__/fuzz.js b/packages/cloudformation-response/__tests__/fuzz.js
new file mode 100644
index 000000000..b39db8e3c
--- /dev/null
+++ b/packages/cloudformation-response/__tests__/fuzz.js
@@ -0,0 +1,23 @@
+import { test } from 'node:test'
+import fc from 'fast-check'
+import middy from '../../core/index.js'
+import middleware from '../index.js'
+
+const handler = middy((event) => event).use(middleware())
+const context = {
+ getRemainingTimeInMillis: () => 1000
+}
+
+test('fuzz `event` w/ `object`', async () => {
+ fc.assert(
+ fc.asyncProperty(fc.object(), async (event) => {
+ await handler(event, context)
+ }),
+ {
+ numRuns: 100_000,
+ verbose: 2,
+
+ examples: []
+ }
+ )
+})
diff --git a/packages/cloudformation-response/__tests__/index.js b/packages/cloudformation-response/__tests__/index.js
new file mode 100644
index 000000000..d91de9526
--- /dev/null
+++ b/packages/cloudformation-response/__tests__/index.js
@@ -0,0 +1,88 @@
+import { test } from 'node:test'
+import { deepEqual } from 'node:assert/strict'
+
+import middy from '../../core/index.js'
+
+import cloudformationResponse from '../index.js'
+
+const defaultEvent = {
+ RequestType: 'Create',
+ RequestId: 'RequestId',
+ LogicalResourceId: 'LogicalResourceId',
+ StackId: 'StackId'
+}
+const context = {
+ getRemainingTimeInMillis: () => 1000
+}
+
+test('It should return SUCCESS when empty response', async (t) => {
+ const handler = middy((event, context) => {})
+
+ handler.use(cloudformationResponse())
+
+ const event = defaultEvent
+ const response = await handler(event, context)
+ deepEqual(response, {
+ Status: 'SUCCESS',
+ RequestId: 'RequestId',
+ LogicalResourceId: 'LogicalResourceId',
+ StackId: 'StackId'
+ })
+})
+
+test('It should return SUCCESS when empty object', async (t) => {
+ const handler = middy((event, context) => {
+ return {}
+ })
+
+ handler.use(cloudformationResponse())
+
+ const event = defaultEvent
+ const response = await handler(event, context)
+ deepEqual(response, {
+ Status: 'SUCCESS',
+ RequestId: 'RequestId',
+ LogicalResourceId: 'LogicalResourceId',
+ StackId: 'StackId'
+ })
+})
+
+test('It should return FAILURE when error thrown', async (t) => {
+ const handler = middy((event, context) => {
+ throw new Error('Internal Error')
+ })
+
+ handler.use(cloudformationResponse())
+
+ const event = defaultEvent
+ const response = await handler(event, context)
+ deepEqual(response, {
+ Status: 'FAILED',
+ Reason: 'Internal Error',
+ RequestId: 'RequestId',
+ LogicalResourceId: 'LogicalResourceId',
+ StackId: 'StackId'
+ })
+})
+
+test('It should not override response values', async (t) => {
+ const handler = middy((event, context) => {
+ return {
+ Status: 'FAILED',
+ RequestId: 'RequestId*',
+ LogicalResourceId: 'LogicalResourceId*',
+ StackId: 'StackId*'
+ }
+ })
+
+ handler.use(cloudformationResponse())
+
+ const event = defaultEvent
+ const response = await handler(event, context)
+ deepEqual(response, {
+ Status: 'FAILED',
+ RequestId: 'RequestId*',
+ LogicalResourceId: 'LogicalResourceId*',
+ StackId: 'StackId*'
+ })
+})
diff --git a/packages/cloudformation-response/index.d.ts b/packages/cloudformation-response/index.d.ts
new file mode 100644
index 000000000..95d6904ee
--- /dev/null
+++ b/packages/cloudformation-response/index.d.ts
@@ -0,0 +1,5 @@
+import middy from '@middy/core'
+
+declare function cloudformationResponse (): middy.MiddlewareObj
+
+export default cloudformationResponse
diff --git a/packages/cloudformation-response/index.js b/packages/cloudformation-response/index.js
new file mode 100644
index 000000000..7f6f5ca55
--- /dev/null
+++ b/packages/cloudformation-response/index.js
@@ -0,0 +1,25 @@
+const cloudformationCustomResourceMiddleware = () => {
+ const cloudformationCustomResourceMiddlewareAfter = (request) => {
+ let { response } = request
+ response ??= {}
+ response.Status ??= 'SUCCESS'
+ response.RequestId ??= request.event.RequestId
+ response.LogicalResourceId ??= request.event.LogicalResourceId
+ response.StackId ??= request.event.StackId
+ request.response = response
+ }
+ const cloudformationCustomResourceMiddlewareOnError = (request) => {
+ const response = {
+ Status: 'FAILED',
+ Reason: request.error.message
+ }
+ request.response = response
+ cloudformationCustomResourceMiddlewareAfter(request)
+ }
+ return {
+ after: cloudformationCustomResourceMiddlewareAfter,
+ onError: cloudformationCustomResourceMiddlewareOnError
+ }
+}
+
+export default cloudformationCustomResourceMiddleware
diff --git a/packages/cloudformation-response/index.test-d.ts b/packages/cloudformation-response/index.test-d.ts
new file mode 100644
index 000000000..b9d16f8af
--- /dev/null
+++ b/packages/cloudformation-response/index.test-d.ts
@@ -0,0 +1,8 @@
+import { expectType } from 'tsd'
+import middy from '@middy/core'
+
+import cloudformationResponse from '.'
+
+// use with default options
+const middleware = cloudformationResponse()
+expectType(middleware)
diff --git a/packages/cloudformation-response/package.json b/packages/cloudformation-response/package.json
new file mode 100644
index 000000000..6d4d8a913
--- /dev/null
+++ b/packages/cloudformation-response/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@middy/cloudformation-response",
+ "version": "6.0.0",
+ "description": "CloudFormation Custom Response event response handling for the middy framework",
+ "type": "module",
+ "engines": {
+ "node": ">=20"
+ },
+ "engineStrict": true,
+ "publishConfig": {
+ "access": "public"
+ },
+ "module": "./index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./index.d.ts",
+ "default": "./index.js"
+ },
+ "require": {
+ "default": "./index.js"
+ }
+ }
+ },
+ "types": "index.d.ts",
+ "files": [
+ "index.js",
+ "index.d.ts"
+ ],
+ "scripts": {
+ "test": "npm run test:unit",
+ "test:unit": "node --test __tests__/index.js",
+ "test:benchmark": "node __benchmarks__/index.js"
+ },
+ "license": "MIT",
+ "keywords": [
+ "Lambda",
+ "Middleware",
+ "Serverless",
+ "Framework",
+ "AWS",
+ "AWS Lambda",
+ "Middy",
+ "CloudFormation",
+ "Custom Response"
+ ],
+ "author": {
+ "name": "Middy contributors",
+ "url": "https://github.com/middyjs/middy/graphs/contributors"
+ },
+ "repository": {
+ "type": "git",
+ "url": "github:middyjs/middy",
+ "directory": "packages/cloudformation-response"
+ },
+ "bugs": {
+ "url": "https://github.com/middyjs/middy/issues"
+ },
+ "homepage": "https://middy.js.org",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/willfarrell"
+ },
+ "devDependencies": {
+ "@middy/core": "6.0.0",
+ "@types/aws-lambda": "^8.10.100"
+ },
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
+}
diff --git a/packages/cloudformation-router/README.md b/packages/cloudformation-router/README.md
new file mode 100644
index 000000000..2a10c20e9
--- /dev/null
+++ b/packages/cloudformation-router/README.md
@@ -0,0 +1,46 @@
+
+
+## License
+
+Licensed under [MIT License](LICENSE). Copyright (c) 2017-2024 [Luciano Mammino](https://github.com/lmammino), [will Farrell](https://github.com/willfarrell), and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).
+
+
+
+
diff --git a/packages/cloudformation-router/__benchmarks__/index.js b/packages/cloudformation-router/__benchmarks__/index.js
new file mode 100644
index 000000000..42c8ee848
--- /dev/null
+++ b/packages/cloudformation-router/__benchmarks__/index.js
@@ -0,0 +1,27 @@
+import { Bench } from 'tinybench'
+import middy from '../../core/index.js'
+import middleware from '../index.js'
+
+const bench = new Bench({ time: 1_000 })
+
+const context = {
+ getRemainingTimeInMillis: () => 30000
+}
+const setupHandler = () => {
+ const baseHandler = () => {}
+ return middy(baseHandler).use(middleware())
+}
+
+const coldHandler = setupHandler()
+
+const event = {}
+await bench
+ .add('without cache', async () => {
+ try {
+ await coldHandler(event, context)
+ } catch (e) {}
+ })
+
+ .run()
+
+console.table(bench.table())
diff --git a/packages/cloudformation-router/__tests__/fuzz.js b/packages/cloudformation-router/__tests__/fuzz.js
new file mode 100644
index 000000000..fc1f4b07e
--- /dev/null
+++ b/packages/cloudformation-router/__tests__/fuzz.js
@@ -0,0 +1,56 @@
+import { test } from 'node:test'
+import fc from 'fast-check'
+import middy from '../../core/index.js'
+import router from '../index.js'
+
+const handler = middy(router())
+const context = {
+ getRemainingTimeInMillis: () => 1000
+}
+
+test('fuzz `event` w/ `object`', async () => {
+ fc.assert(
+ fc.asyncProperty(fc.object(), async (event) => {
+ try {
+ await handler(event, context)
+ } catch (e) {
+ if (e.cause?.package !== '@middy/cloudformation-router') {
+ throw e
+ }
+ }
+ }),
+ {
+ numRuns: 100_000,
+ verbose: 2,
+
+ examples: []
+ }
+ )
+})
+
+test('fuzz `event` w/ `record`', async () => {
+ fc.assert(
+ fc.asyncProperty(
+ fc.record({
+ RequestType: fc.string(),
+ RequestId: fc.string(),
+ LogicalResourceId: fc.string(),
+ StackId: fc.string()
+ }),
+ async (event) => {
+ try {
+ await handler(event, context)
+ } catch (e) {
+ if (e.cause?.package !== '@middy/cloudformation-router') {
+ throw e
+ }
+ }
+ }
+ ),
+ {
+ numRuns: 100_000,
+ verbose: 2,
+ examples: [[{ requestContext: { routeKey: 'valueOf' } }]]
+ }
+ )
+})
diff --git a/packages/cloudformation-router/__tests__/index.js b/packages/cloudformation-router/__tests__/index.js
new file mode 100644
index 000000000..4af8adfd2
--- /dev/null
+++ b/packages/cloudformation-router/__tests__/index.js
@@ -0,0 +1,117 @@
+import { test } from 'node:test'
+import { ok, equal } from 'node:assert/strict'
+import middy from '../../core/index.js'
+import cloudformationRouter from '../index.js'
+
+// const event = {}
+const context = {
+ getRemainingTimeInMillis: () => 1000
+}
+
+// Types of routes
+test('It should route to a static route', async (t) => {
+ const event = {
+ RequestType: 'Create'
+ }
+ const handler = cloudformationRouter([
+ {
+ requestType: 'Create',
+ handler: () => true
+ }
+ ])
+ const response = await handler(event, context)
+ ok(response)
+})
+
+test('It should thrown FAILURE when route not found', async (t) => {
+ const event = {
+ RequestType: 'Update'
+ }
+ const handler = cloudformationRouter([
+ {
+ requestType: 'Create',
+ handler: () => true
+ }
+ ])
+ try {
+ await handler(event, context)
+ } catch (e) {
+ equal(e.message, 'Route does not exist')
+ }
+})
+
+test('It should thrown FAILURE when route not found, using notFoundResponse', async (t) => {
+ const event = {
+ RequestType: 'Update'
+ }
+ const handler = cloudformationRouter({
+ routes: [
+ {
+ requestType: 'Create',
+ handler: () => true
+ }
+ ],
+ notFoundResponse: (args) => {
+ return {
+ Status: 'SUCCESS'
+ }
+ }
+ })
+ const res = await handler(event, context)
+
+ equal(res.Status, 'SUCCESS')
+})
+
+// with middleware
+test('It should run middleware that are part of route handler', async (t) => {
+ const event = {
+ RequestType: 'Create'
+ }
+ const handler = cloudformationRouter([
+ {
+ requestType: 'Create',
+ handler: middy(() => false).after((request) => {
+ request.response = true
+ })
+ }
+ ])
+ const response = await handler(event, context)
+ ok(response)
+})
+
+test('It should middleware part of router', async (t) => {
+ const event = {
+ RequestType: 'Create'
+ }
+ const handler = middy(
+ cloudformationRouter([
+ {
+ requestType: 'Create',
+ handler: () => false
+ }
+ ])
+ ).after((request) => {
+ request.response = true
+ })
+ const response = await handler(event, context)
+ ok(response)
+})
+
+// Errors
+
+test('It should throw when not a cloudformation event', async (t) => {
+ const event = {
+ path: '/'
+ }
+ const handler = cloudformationRouter([
+ {
+ requestType: 'Create',
+ handler: () => true
+ }
+ ])
+ try {
+ await handler(event, context)
+ } catch (e) {
+ equal(e.message, 'Unknown CloudFormation Custom Response event format')
+ }
+})
diff --git a/packages/cloudformation-router/index.d.ts b/packages/cloudformation-router/index.d.ts
new file mode 100644
index 000000000..84bf5af25
--- /dev/null
+++ b/packages/cloudformation-router/index.d.ts
@@ -0,0 +1,13 @@
+import middy from '@middy/core'
+import { CloudFormationCustomResourceHandler } from 'aws-lambda'
+
+interface Route {
+ requestType: string
+ handler: CloudFormationCustomResourceHandler
+}
+
+declare function cloudformationRouterHandler (
+ routes: Route[]
+): middy.MiddyfiedHandler
+
+export default cloudformationRouterHandler
diff --git a/packages/cloudformation-router/index.js b/packages/cloudformation-router/index.js
new file mode 100644
index 000000000..aba4c2220
--- /dev/null
+++ b/packages/cloudformation-router/index.js
@@ -0,0 +1,57 @@
+const defaults = {
+ routes: [],
+ notFoundResponse: ({ requestType }) => {
+ const err = new Error('Route does not exist', {
+ casue: {
+ package: '@middy/cloudformation-router',
+ data: { requestType }
+ }
+ })
+ throw err
+ }
+}
+const cloudformationCustomResourceRouteHandler = (opts = {}) => {
+ if (Array.isArray(opts)) {
+ opts = { routes: opts }
+ }
+ const { routes, notFoundResponse } = { ...defaults, ...opts }
+
+ const routesStatic = {}
+ for (const route of routes) {
+ const { requestType, handler } = route
+
+ // Static
+ routesStatic[requestType] = handler
+ }
+
+ const requestTypes = {
+ Create: true,
+ Update: true,
+ Delete: true
+ }
+ return (event, context, abort) => {
+ const { RequestType: requestType } = event
+ if (
+ !requestType ||
+ !Object.hasOwnProperty.call(requestTypes, requestType)
+ ) {
+ throw new Error('Unknown CloudFormation Custom Response event format', {
+ cause: {
+ package: '@middy/cloudformation-router',
+ data: { requestType }
+ }
+ })
+ }
+
+ // Static
+ if (Object.hasOwnProperty.call(routesStatic, requestType)) {
+ const handler = routesStatic[requestType]
+ return handler(event, context, abort)
+ }
+
+ // Not Found
+ return notFoundResponse({ requestType })
+ }
+}
+
+export default cloudformationCustomResourceRouteHandler
diff --git a/packages/cloudformation-router/index.test-d.ts b/packages/cloudformation-router/index.test-d.ts
new file mode 100644
index 000000000..4fd634e3b
--- /dev/null
+++ b/packages/cloudformation-router/index.test-d.ts
@@ -0,0 +1,28 @@
+import middy from '@middy/core'
+// import { CloudFormationCustomResourceHandler } from 'aws-lambda'
+import { expectType } from 'tsd'
+import cloudformationRouterHandler from '.'
+
+const createLambdaHandler: any = async () => {
+ return {
+ Status: 'SUCCESS'
+ }
+}
+
+const deleteLambdaHandler: any = async () => {
+ return {
+ Status: 'SUCCESS'
+ }
+}
+
+const middleware = cloudformationRouterHandler([
+ {
+ requestType: 'Create',
+ handler: createLambdaHandler
+ },
+ {
+ requestType: 'Delete',
+ handler: deleteLambdaHandler
+ }
+])
+expectType(middleware)
diff --git a/packages/cloudformation-router/package.json b/packages/cloudformation-router/package.json
new file mode 100644
index 000000000..095be145f
--- /dev/null
+++ b/packages/cloudformation-router/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "@middy/cloudformation-router",
+ "version": "6.0.0",
+ "description": "CloudFormation Custom Response event router for the middy framework",
+ "type": "module",
+ "engines": {
+ "node": ">=20"
+ },
+ "engineStrict": true,
+ "publishConfig": {
+ "access": "public"
+ },
+ "module": "./index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./index.d.ts",
+ "default": "./index.js"
+ },
+ "require": {
+ "default": "./index.js"
+ }
+ }
+ },
+ "types": "index.d.ts",
+ "files": [
+ "index.js",
+ "index.d.ts"
+ ],
+ "scripts": {
+ "test": "npm run test:unit",
+ "test:unit": "node --test __tests__/index.js",
+ "test:benchmark": "node __benchmarks__/index.js"
+ },
+ "license": "MIT",
+ "keywords": [
+ "Lambda",
+ "Middleware",
+ "Serverless",
+ "Framework",
+ "AWS",
+ "AWS Lambda",
+ "Middy",
+ "CloudFormation",
+ "Custom Response",
+ "router"
+ ],
+ "author": {
+ "name": "Middy contributors",
+ "url": "https://github.com/middyjs/middy/graphs/contributors"
+ },
+ "repository": {
+ "type": "git",
+ "url": "github:middyjs/middy",
+ "directory": "packages/cloudformation-router"
+ },
+ "bugs": {
+ "url": "https://github.com/middyjs/middy/issues"
+ },
+ "homepage": "https://middy.js.org",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/willfarrell"
+ },
+ "devDependencies": {
+ "@middy/core": "6.0.0",
+ "@types/aws-lambda": "^8.10.100"
+ },
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
+}
diff --git a/packages/http-router/index.js b/packages/http-router/index.js
index aa9dd8928..3dee4af4f 100644
--- a/packages/http-router/index.js
+++ b/packages/http-router/index.js
@@ -24,7 +24,7 @@ const httpRouteHandler = (opts = {}) => {
// Prevents `routesType[method][path] = handler` from flagging: This assignment may alter Object.prototype if a malicious '__proto__' string is injected from library input.
if (!enumMethods.includes(method)) {
throw new Error('Method not allowed', {
- cause: { package: '@middy/http-router', data: method }
+ cause: { package: '@middy/http-router', data: { method } }
})
}
@@ -48,12 +48,12 @@ const httpRouteHandler = (opts = {}) => {
if (!method) {
throw new Error('Unknown http event format', {
- cause: { package: '@middy/http-router', data: method }
+ cause: { package: '@middy/http-router', data: { method } }
})
}
if (!path) {
throw new Error('Unknown http event format', {
- cause: { package: '@middy/http-router', data: path }
+ cause: { package: '@middy/http-router', data: { path } }
})
}
diff --git a/website/docs/events/cloud-formation.md b/website/docs/events/cloud-formation.md
index c8b081bd0..2b3744e80 100644
--- a/website/docs/events/cloud-formation.md
+++ b/website/docs/events/cloud-formation.md
@@ -14,9 +14,52 @@ This page is a work in progress. If you want to help us to make this page better
## Example
```javascript
import middy from '@middy/core'
+import cloudformationRouterHandler from '@middy/cloudformation-router'
+import cloudformationResponseMiddleware from '@middy/cloudformation-response'
+import validatorMiddleware from '@middy/validator'
-export const handler = middy()
- .handler((event, context, {signal}) => {
- // ...
+const createHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...',
+ Data:{}
+ }
+ })
+
+const updateHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...',
+ Data: {}
+ }
+ })
+
+const deleteHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...'
+ }
})
+
+const routes = [
+ {
+ requesType: 'Create',
+ handler: createHandler
+ },
+ {
+ requesType: 'Update',
+ handler: updateHandler
+ },
+ {
+ routeKey: 'Delete',
+ handler: deleteHandler
+ }
+]
+
+export const handler = middy()
+ .use(cloudformationResponseMiddleware())
+ .handler(cloudformationRouterHandler(routes))
```
diff --git a/website/docs/middlewares/cloudformation-response.md b/website/docs/middlewares/cloudformation-response.md
new file mode 100644
index 000000000..8ad7aa045
--- /dev/null
+++ b/website/docs/middlewares/cloudformation-response.md
@@ -0,0 +1,34 @@
+---
+title: cloudformation-response
+---
+
+Manage CloudFormation Custom Resource responses.
+
+## Install
+
+To install this middleware you can use NPM:
+
+```bash npm2yarn
+npm install --save @middy/cloudformation-response
+```
+
+## Options
+
+None
+
+## Sample usage
+
+### General
+
+```javascript
+import middy from '@middy/core'
+import cloudformationResponse from '@middy/cloudformation-response'
+
+export const handler = middy((event, context) => {
+ return {
+ PhysicalResourceId:'...'
+ }
+})
+
+handler.use(cloudformationResponse())
+```
diff --git a/website/docs/routers/cloudformation-router.md b/website/docs/routers/cloudformation-router.md
new file mode 100644
index 000000000..fc5e5732c
--- /dev/null
+++ b/website/docs/routers/cloudformation-router.md
@@ -0,0 +1,80 @@
+---
+title: cloudformation-router
+---
+
+This handler can route to requests to one of a nested handler based on `requestType` of a CloudFormation Custom Response event.
+
+## Install
+
+To install this middleware you can use NPM:
+
+```bash
+npm install --save @middy/cloudformation-router
+```
+
+## Options
+
+- `routes` (array[\{routeKey, handler\}]) (required): Array of route objects.
+ - `routeKey` (string) (required): AWS formatted request type. ie `Create`, `Update`, `Delete`
+ - `handler` (function) (required): Any `handler(event, context, {signal})` function
+- `notFoundHandler` (function): Override default FAILED response with your own custom response. Passes in `{requestType}`
+
+NOTES:
+
+- Reponse parameters are automatically applied for `Status`, `RequestId`, `LogicalResourceId`, and/or `StackId` when not present.
+- Errors should be handled as part of the router middleware stack **or** the lambdaHandler middleware stack. Handled errors in the later will trigger the `after` middleware stack of the former.
+- Shared middlewares, connected to the router middleware stack, can only be run before the lambdaHandler middleware stack.
+
+## Sample usage
+
+```javascript
+import middy from '@middy/core'
+import cloudformationRouterHandler from '@middy/cloudformation-router'
+import cloudformationResponseMiddleware from '@middy/cloudformation-response'
+import validatorMiddleware from '@middy/validator'
+
+const createHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...',
+ Data:{}
+ }
+ })
+
+const updateHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...',
+ Data: {}
+ }
+ })
+
+const deleteHandler = middy()
+ .use(validatorMiddleware({eventSchema: {...} }))
+ .handler((event, context) => {
+ return {
+ PhysicalResourceId: '...'
+ }
+ })
+
+const routes = [
+ {
+ requesType: 'Create',
+ handler: createHandler
+ },
+ {
+ requesType: 'Update',
+ handler: updateHandler
+ },
+ {
+ routeKey: 'Delete',
+ handler: deleteHandler
+ }
+]
+
+export const handler = middy()
+ .use(cloudformationResponseMiddleware())
+ .handler(cloudformationRouterHandler(routes))
+```