Steve Holgado

AWS Lambda: Local development with Express

2019AWS 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.


Hi, I'm a Senior Front-End Developer based in London. Feel free to contact me with questions, opportunities, help with open source projects, or anything else :)

You can find me on GitHub, Stack Overflow or email me directly.