A middleware and lifecycle framework for building service functions in AWS lambda functions.
This project was inspired by middy js, a stylish library with some excellent tooling for building service functions
. The project has taken a few of those ideas and attempts to apply a more functional programming style to their implementation. The project was originally created and started at aws-middleware-js
Install using yarn:
yarn add mambda
Install using npm:
npm install mambda
mambda fits seemlessly into the programming model for writing AWS lambdas
const lambda = require('mambda')
function myHandler(event, context, callback) {
//... function code
callback(null, "some success message");
// or
// callback("some error type");
}
exports.handler = lambda(myHandler)
You can also use promises directly
const lambda = require('mambda')
function myAsyncHandler(event, context, callback) {
//... function code
const promise = foo(); // Some asynchronously executed code
return promise
.then(resultOfFoo => process(resultOfFoo))
.catch(err => {
console.log('Naughty error', err)
return { statusCode: 500, body: 'An error occurred during execution' };
})
}
exports.handler = lambda(myAsyncHandler)
and use async/await syntax
const lambda = require('mambda')
async function myAsyncHandler(event, context, callback) {
//... function code
const foo = await bar(); // Some asynchronously executed code
return someOperation(foo);
}
exports.handler = lambda(myAsyncHandler)
The value of using the middlewares is that they encapsulate a composable, configurable API for isolating and your wrapping business logic in common boiler plate that would otherwise be cluttering your code base.
const lambda = require('mambda');
const jsonBodyParser = require('mambda/middlewares/json-body-parser');
const httpErrorHandler = require('mambda/middlewares/http-error-handler');
async function myAsyncHandler(event, context, callback) {
// All of this commented boiler-plate is made unnecessary simply by adding the the jsonBodyParser middleware and error handling middleware
// try {
// const body = headers['Content-Type'] === 'application/json')
// ? JSON.parse(event.body)
// : {}
// } catch (error) {
// other error handling...
// return { statusCode: 422, body: 'Unprocessable entity, invalid json in request body' }
// }
const body = event.body; // A javascript object from the deserialized json in the original event
const foo = await bar();
//... function code
return someOperation(foo);
}
exports.handler = lambda(myAsyncHandler)
.use(jsonBodyParser())
.use(httpErrorHandler())
Mambda adds an initialisation step to the lambda function, mambda lazily evaluates the resource and caches the result for reuse of the execution environment.
The lazy evaluation makes mocking/stubbing shared resources much easier for unit testing with frameworks/libraries such as jest, sinon, simple-mock, etc...
Caching allows users to make full use and reuse of the lambda execution context
const lambda = require('mambda');
const AWS = require('aws-sdk')
const myHandler = s3 => (event, context, callback) => {
var params = { Bucket: process.env.BUCKET_NAME, Key: process.env.BUCKET_KEY, Body: process.env.BODY };
// function code...
s3.putObject(params, function(err, data) {
if (err) {
callback(err)
} else {
callback(null, 'Put the body into the bucket! YAY!')
}
});
}
exports.handler = lambda({ init: () => new AWS.S3(), handler: myHandler });
The LambdaFunc configuration object also accepts a logger parameter that will log progress and other helpful debug information. The logger need only expose info, debug, warn, error and trace functions of a similar signature to the built in console logger making it compatible with other loggers such as pinojs, winstonjs or signale
Using console logging
const lambda = require('mambda');
exports.handler = lambda({
logger: console,
handler: (event, context, callback) => {
// function code...
}
});
But the console could be replaced with... pinojs (Or any other logger with a similar API)
const lambda = require('mambda');
const pino = require('pino')();
exports.handler = lambda({
logger: pino,
handler: (event, context, callback) => {
// function code...
}
});
The logger must be set to be used.
Can't find quite what you're looking for? Why not consider contributing..., raising a feature request or upvoting an existing one it would be much appreciated and is really helpful in prioritisation, but if you're in a hurry here's how to create custom middleware.
The middlewares are all simple javascript objects with atleast 1 of the following 3 functions:
/**
* @param {AWSLambdaEvent} event aws event triggering the lambda
* @param {AWSLambdaContext} context aws runtime/execution context
* @returns {(Array|AWSLambdaEvent|undefined)}
* - An array with 2 elements [newEvent {AWSLambdaEvent}, newContext {AWSLambdaContext}]
* - A new AWSLambdaEvent
* - {undefined} if the middleware had no changes to make to the event/context
*/
function before(event, context) { // ...function code
/**
* @param {AWSLambdaResponse} result the result of having called the lambda function with the given event and context
* @param {AWSLambdaEvent} event aws event that triggered the lambda
* @param {AWSLambdaContext} context aws runtime/execution context
* @returns {(Array|AWSLambdaResponse|undefined)}
* - An array with 2 elements [newEvent {AWSLambdaResponse}, newContext {AWSLambdaContext}]
* - A new AWSLambdaResponse
* - {undefined} if the middleware had no changes to make to the event/context
*/
function after(result, event, context) { // ...function code
/**
* @param {AWSLambdaResponse} result the accumulated response of other error handling middlewares
* @param {Error} error the error that was thrown either by a preceeding middleware or the lambda function
* @param {AWSLambdaEvent} event aws event that triggered the lambda prior to the exception being thrown
* @param {AWSLambdaContext} context aws runtime/execution context
* @returns {(Array|AWSLambdaResponse|undefined)}
* - An array with 2 elements [new result {AWSLambdaResponse}, new Error {Error}]
* - A new result {AWSLambdaResponse}
* - {undefined} if the middleware had no changes to make to the event/context
*/
function onError(result, error, event, context) { // ...function code
Documentation detailing the contents of:
const myCustomMiddleware = (middlewareConfig) => ({
before: (event, context) => { /* function code... */ },
after: (result, event, context) => { /* function code... */ },
onError: (error, event, context) => { /* function code... */ }
});
Once created there is no special transformation or class, just... use it as you would any other middleware
export.handler = lambdaFunc(handler).use(myCustomMiddleware(myMiddlewareConfig))
All of the middlewares function can be either synchronous or asynchronous, it's up to you and your use case.
This is pretty simple to do and Amazon provide a number of examples of code that does exactly that where the coding samples will initialise global/static variables outside of the scope of handler function so that if the execution context is reused they are already initialised and the recycled environment is more performant than when the lambda function is executed from a cold start.
The problem with these examples is that the shared resources are often initialised as a side effect of simply loading the javascript file into memory (fairly poor practice when writing modular code), it makes the service functions more difficult to test because it is difficult to mock or provide stub dependencies. This middleware library provides a lifecycle which supports enables mocking dependencies and encourages developers to write pure javascript files free from side effects.
When writing lambdas there can be a fair bit of boiler plate code wrapping up the business logic and cluttering your code base, AWS middleware abstracts this out into common reusable middlewares that can be configured and shared across all your lambdas
A key factor here is that the lambdaFunc will respect how you want to write AWS lambdas:
Always while wrapping up your code in the middlewares you've chosen with no extra work.
This library is designed and written to be as small and lightweight as possible (a nano library if you will). Therefore I won't include any dependencies in here specific to just a single middleware however if it's useful I will try to create and maintain seperate intergrations with other libraries.
See our contributing doc, be sure to checkout the code of conduct
This project used conventional commits to manage versions and releases of the library therefore when making a commit please use yarn commit <COMMIT_PARAMETERS>
and this will guide you through writing a conventional commit message which can be understood work with the ci pipeline