serverless-compose
v2.4.0
Published
A lightweight framework for AWS Lambda middleware
Downloads
5,505
Readme
serverless-compose
A lightweight functional middleware framework for AWS lambda that stays out of your way and lets you build kick-ass, composable middleware for your lambdas.
Zero dependencies. Functional interface. Reusable code.
Table of Contents
- Installation
- Project Purpose
- Basic Usage
- Customizing Existing Middleware
- Creating Partial Middleware
- Building New Middleware
Installation
To use serverless-compose
in your nodejs project,
yarn add serverless-compose
or
npm i serverless-compose
serverless-compose
has no dependencies itself (peer or otherwise), so that's it!
Project Purpose
The goal of this project is to provide a very thin middleware framework for AWS lambda.
Without a well-established middleware pattern, too many bad habits can fossilize from one-off functions into bad middleware.
This library provides a compose
function for wrapping middleware around a handler without having deeply nested code.
This function wrapping pattern allows explicitly definied, functional and onion-style (a well-established style) middleware.
serverless-compose
also builds on the basic compose function, offering a few patterns that the author(s) have seen in the wild for rapid customization.
Too many middlework frameworks enforce bad designs and opinions onto their users.
serverless-compose
doesn't impose much at all and it stays out of your way once you define your stack.
Basic Usage
The most basic use of serverless-compose
is to add timing and error middleware to your handlers.
recoveryMiddleware
adds code for rejected promises (or thrown async
functions) in your handlers.
The following example wraps a rejection with an actual response so AWS API Gateway can handle and pass to clients, rather than sending its own 503 error with no information.
import {
compose,
recoveryMiddleware,
timingLogMiddleware,
} from 'serverless-compose'
// Suppose this is a client that fetches the weather from some external API
import { WeatherClient } from 'made-up-weather-library'
// Your actual handler code
async function getWeather(request, context) {
// All this does is call this API, but suppose the API is bad and fails a lot,
// so this is likely to throw an error, rejecting the promise of this function
response = await WeatherClient.askBadAPIForWeather()
}
// Your own custom logging function
async function logDuration(duration) {
console.log(`It took: ${duration}ms to return the weather`)
}
// Your own custom error handler
async function sendError(error) {
return {
statusCode: 500,
body: JSON.stringify({
error: `${error}`,
message: 'The darn weather API failed me again!',
}),
}
}
const TimingMiddleware = timingLogMiddleware(logDuration)
const RecoveryMiddleware = recoveryMiddleware(sendError)
const MyMiddlewareStack = compose(
TimingMiddleware,
RecoveryMiddleware,
)
export const lambdaHandler = MyMiddlewareStack(getWeather)
Customizing Existing Middleware
serverless-compose
provides a number of middleware patterns for users to customize and create their own middleware.
These patterns aren't extensive, but do provide a large variety of options.
The current API contains the following middleware options:
timingLogMiddleware
import { timingLogMiddleware } from 'serverless-compose'
timinglogMiddlware
takes in a logging function of the signature (duration, {event, result}) => void
and calls this function after the next
handler has returned.
This middleware is useful for recording the time taken by a lambda in other means than the existing AWS lambda logging. The function passed in can log a formatted message to console or remotely as it chooses.
Creating Partial Middleware
One of the best things about compose
is that it itself returns a middleware, so you can create partial middleware chains and reuse code within a project by using compose
on those partial chains.
Suppose you have HTTP handlers and non HTTP handlers and the non HTTP handlers don't require authentication/authorization or validation but do require recovery, timing and input mapping.
Implementing multiple middleware stacks looks like:
// middleware.js
import { compose } from 'serverless-compose'
// ... OMITTED MIDDLEWARE IMPLEMENTATIONS ...
const OuterMiddleware = compose(
MyRecoveryMiddleware,
MyTimingMiddleware,
)
const ValidationMiddleware = compose(
MyAuthMiddleware,
MyEventValidationMiddleware,
)
export const HttpMiddleware = compose(
OuterMiddleware,
ValidationMiddleware,
MyEventMappingMiddleware,
)
export const RegularMiddleware = compose(
OuterMiddleware,
MyEventMappingMiddleware,
)
Now, in the AWS handlers, only the middleware that is needed can be used.
import { HttpMiddleware, RegularMiddleware } from './middleware'
function myHttpHandler(event, context) {
// DO SOMETHING...
}
function myRegularHandler(event, context) {
// DO SOMETHING ELSE...
}
export const handleHttp = HttpMiddleware(myHttpHandler)
export const handleCloudwatchEvents = RegularMiddleware(myRegularHandler)
Building New Middleware
While compose
is a strong function for assembling middleware, the value it provides is as a starting point as a framework for your own unique requirements.
The only requirement of a middleware is that it accept a Handler
as its only argument and return a Handler
, which means that as long as you follow this rule, you can easily create custom, composable middleware, or assemble middleware out of well known patterns provided by serverless-compose
via its convenience functions.
Below is an example for creating your own side effect middleware:
// middleware.js
import { mySideEffect } from './my-library'
// we use the convention next to indicate the next handler in the middleware chain
// since each middleware returns a handler, we pass that inner-defined handler into the outer middleware when we invoke it
export function MyMiddleware(next) {
return async (event, context) => {
// A lambda with Handler signature
await mySideEffect()
// After calling side effect, actually call the handler
return next(event, context)
}
}
And that's it.
Now when assembling your middleware stack, mySideEffect
will be called before the next middleware and before the handler.
Side effects or translations of the event or response can be called before or after the next handler.
Using the middleware looks like:
// handler.js
import { compose, recoveryMiddleware } from 'serverless-compose'
import { MyMiddleware } from './middleware'
import { OtherMadeupMiddleware, SupposeThisExistsMiddleware } from 'shared-lib'
async function handler(event, context) {
// DO SOMETHING ...
}
export lambdaHandler = compose(
recoveryMiddleware(async (error) => {console.log(error)})
SupposeThisExistsMiddleware,
MyMiddleware,
OtherMadeupMiddleware,
)(handler)
Now an error logging recovery middleware will happen outside (before your middleware and after all other middleware have returned).
Then SupposeThisExistsMiddleware
will do whatever it does and call your MyMiddleware
handler before your MyMiddleware
handler calls OtherMadeupMiddleware
.
OtherMadeupMiddleware
, most likely, will call handler
.
It's wired to do that, but depending on what it does, it may not.
As a middleware author, typically, you should call next
but you don't have to.
An example of when you might not call next
would be an authorization middleware or request validation middleware -- they're there to prevent the handler from being called if it doesn't meet certain criteria.
Check src/middleware.ts
for some examples of translating events and responses or optionally not calling next based on input.
Note that middleware.ts
contains middleware generators or functions that produce Middleware
.
As an author, you can decide to generate it based on user-defined parameters or hard-code a middleware.
Suppose you want to convert a ping/pong handler into a pong only handler. You could implement a pattern similar to responseMiddleware
from middleware.ts
but hardcode the mapping function:
export function PongOnlyMiddleware(next) {
return async (event, context) => {
const response = await next(event, context)
// Intercept the response if it's "ping" and send "pong" instead
if response === 'ping' {
return 'pong'
}
// Do whatever otherwise
return response
}
}
It's pretty simple to get started making your own middleware. If you're not sure, open an issue in github and ask!
Happy Coding!