AWS Lambda: Local development with Express
2019
AWS Lambda / Node / Express
I often use AWS Lambda functions when I just need some basic back-end functionality to support a front-end application.
I almost always trigger my Lambda functions with AWS API Gateway so that I have a url to post requests to.
Developing this can be difficult because I don’t want to keep pushing new code up to AWS every time I make a change. I want to rapidly develop locally before pushing the code to AWS.
For small projects, I like to run a simple Express server so that I can trigger my Lambda function via a HTTP endpoint on localhost, in place of API Gateway. I can then hook it up to my front-end applications during development.
Table of contents
Creating our Lambda function
First let’s start a new project and add an index.js
file. This will contain our Lambda function.
Our Lambda function for this article will simply convert a given word to uppercase, just so we can easily see that it’s working when we test it out:
exports.handler = async function(event) {
let statusCode
let body
try {
// "body" will come from API Gateway as plain text
const { word } = JSON.parse(event.body)
statusCode = 200
body = {
upperCaseWord: word.toUpperCase()
}
}
catch (err) {
statusCode = 400
}
// Return object required by API Gateway
return {
statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
},
body: JSON.stringify(body)
}
}
We are returning an object in the form required by API Gateway, including CORS headers and a stringified JSON body.
Note, CORS headers need to be set in the Lambda function response in order for a client-side application to use the function via AWS API Gateway.
Adding our development server
Now we are going to set up our Express server to make our function available at some endpoint.
Fisrt, let’s install Express as a dependency along with Nodemon to automatically reload our server when we make any changes to our files:
npm install --save-dev express nodemon
So, now we can create dev-server.js
with the following content:
const express = require('express')
const app = express()
// Process body as plain text as this is
// how it would come from API Gateway
app.use(express.text())
app.listen(3000, () => console.log('listening on port: 3000'))
Let’s now also add a script to our package.json
so that we can start our development server with Nodemon:
...
"scripts": {
"start": "nodemon dev-server.js"
},
...
Serving our Lambda function
We are now going to make use of a package called local-lambda which is a tool for running AWS Lambda functions locally. Let’s install it:
npm install --save-dev lambda-local
We can give local-lambda the path and the name of our Lambda function and it will excute it and return the result.
The local-lambda package performs some nice logging, including catching and reporting any errors from our Lambda function, which can be very useful during development.
It also includes other useful features such as supplying environment variables and being able to control the timeout. You can find out more about the options available at: https://www.npmjs.com/package/lambda-local.
So let’s execute our function on requests to the /lambda
path (this can be whatever you want) and await the result:
const path = require('path')
const express = require('express')
const lambdaLocal = require('lambda-local')
const app = express()
app.use(express.text())
app.use('/lambda', async (req, res) => {
const result = await lambdaLocal.execute({
lambdaPath: path.join(__dirname, 'index'),
lambdaHandler: 'handler'
})
})
app.listen(3000, () => console.log('listening on port: 3000'))
If needed, we can supply environment variables to the Lambda function with a path to a .env
file:
// ...
app.use('/lambda', async (req, res) => {
const result = await lambdaLocal.execute({
lambdaPath: path.join(__dirname, 'index'),
lambdaHandler: 'handler',
envfile: path.join(__dirname, '.env')
})
})
We’re also going to pass along the request headers and body (as plain text) to our Lambda function via the event
object to simulate how we will receive data from API Gateway once deployed to AWS:
// ...
app.use('/lambda', async (req, res) => {
const result = await lambdaLocal
.execute({
lambdaPath: path.join(__dirname, 'index'),
lambdaHandler: 'handler',
envfile: path.join(__dirname, '.env'),
event: {
headers: req.headers, // Pass on request headers
body: req.body // Pass on request body
}
})
})
Finally, we need to respond to the HTTP request.
We can simply use the statusCode
, headers
and body
returned from our Lambda function to respond to the Express request:
// ...
app.use('/lambda', async (req, res) => {
const result = await lambdaLocal
.execute({
lambdaPath: path.join(__dirname, 'index'),
lambdaHandler: 'handler',
envfile: path.join(__dirname, '.env'),
event: {
headers: req.headers, // Pass on request headers
body: req.body // Pass on request body
}
})
// Respond to HTTP request
res
.status(result.statusCode)
.set(result.headers)
.end(result.body)
})
Note that setting the headers from the Lambda function will pass the CORS headers through, so we don’t need to handle this separately with Express.
Testing it out
We can now use a tool such as Postman to fire requests at localhost:3000/lambda
If you have a client application ready, then you can use this in place of your API Gateway url for development.
End
So, we have a simple development environment for a Lambda function that reloads when we make changes and can be easily extended to serve multiple Lambda functions from different endpoints if needed.