Deploy an ExpressJS application using Serverless, Express and API Gateway
I was looking for a simple way to create and deploy an ExpressJS application without managing the maintenance (or cost) of an EC2 instance and found a great solution using Lambda. The below tutorial will show how to create a simple app with a single endpoint that connects to a Lambda function.
Overview
This simple tutorial will create an ExpressJS application using Cloudformation, Lambda and API Gateway. Here is the workflow:
- Cloudformation will create a new stack]
- S3 Deployment Bucket
- IAM Lambda Role Assignment
- Lambda Function
- API Gateway Rest
Step 0 - Getting Started
Install the Serverless framework for NodeJS.
npm install -g serverless
Step 1 - Create a new Node application
Create a new express directory.
mkdir ~/Desktop/my-express-app && cd ~/Desktop/my-express-app
Create a new package.json
npm init -f
Step 2 - Install Libraries
Install ExpressJS
npm install express --save
Install the Serverless HTTP application
This package is the glue between ExpressJS and Lambda.
npm install serverless-http --save
Install Serverless Deployment Bucket
Serverless Deployment Bucket enables you to create a custom S3 bucket name for your upcoming Cloudformation workflow. What's nice about this package is that if a bucket does not exist in S3, it auto-magically creates one for you.
npm install serverless-deployment-bucket --save
Step 3 - Create an index.js
file
Our next step is to create a file and paste this ExpressJS related code. The code below accepts any GET
request and returns a JSON object. This setup is ideal for creating a simple backend API service.
Regarding each GET response (res.json()
), I'm borrowing the Stripe convention of responding to each request with a payload that includes status
, data
and message
. I find that client-side developers appreciate consistency in their API responses.
'use strict';
const serverless = require('serverless-http');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.locals.pretty = true;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.set('json spaces', 2); // Format JSON Response
app.get('/:hash?', (req, res) => {
let path = req.path;
let { params, query, body } = req;
res.status(200).send({
status: res.statusCode,
data: null,
message: `${req.method} :hash - ${path}`,
params: params,
query: query
})
})
/* ** ** ** ** ** ** **
* Error handling
* ** ** ** ** ** ** **/
app.use((err, req, res, next) => {
console.error(err);
res.status(404).json({
status: res.statusCode,
data: err.message,
message: 'Not Found'
});
});
// Development error handler will print stacktrace
if (app.get('env') === 'development') {
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
stage: "development",
status: res.status,
data: null,
message: err.message
});
});
}
// Production error handler no stacktraces leaked to user
// You must provide four arguments to identify it as an error-handling middleware function.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
status: res.statusCode,
data: err.message,
message: 'Internal Error'
});
});
module.exports.handler = serverless(app);
Step 4 - Create a serverless.yaml
configuration file
This ExpressJS application will have one function (express
) which points to the handler
function within ./index.js
.
The app
method currently accepts all HTTP verbs (GET
, POST
, PUT
, DELETE
) which you can later lockdown.
# serverless.yml
service: my-service
custom:
appName: my-express-api
prefix: app-${self:custom.appName}-
logLevel: ${self:custom.prefix}${self:provider.stage}.LogLevel
stagePrefix:
dev: ${self:provider.stage}
staging: ${self:provider.stage}
prod: ''
service: # These are the names of the services
cloudFormation: ${self:custom.prefix}${self:custom.stagePrefix.${self:provider.stage}, ''}
apiGateway: ${self:custom.prefix}${self:custom.stagePrefix.${self:provider.stage}, ''}
cfBucketName: cf-stack-${self:custom.appName}-${self:provider.region}-${self:custom.stagePrefix.${self:provider.stage}, ''}
provider:
name: aws # (required) Serverless provider.
runtime: nodejs12.x # (required) Node version.
memorySize: 128 # (optional) Lambda memory. Default is 1024
timeout: 10 # (optional) Lambda timeout. Default is 30
stage: ${opt:stage, 'dev'} # (optional) Either take --stage param from the command line, or use default.
region: ${opt:region, 'us-east-1'} # (optional) Either take --region param from the command line, or use default.
stackName: ${self:custom.service.cloudFormation} # (optional) Name Cloudformation stack
apiName: ${self:custom.service.apiGateway} # (optional) Name API Gateway
env: # (optional) env vars
DEBUG: 'express:*' # (optional) Express specific env var will print express debug logs
logLevel: ${self:custom.logLevel} # (optional) Custom name for Cloudwatch logs
logRetentionInDays: 1 # (optional) Keep your Cloudwatch Logs for x days
deploymentBucket: # (optional) Custom S3 bucketname to store
name: ${self:custom.cfBucketName} # (optional) Custom bucket name created by Cloudformation
serverSideEncryption: AES256 # (optional) Encrypt the data
profile: ${opt:profile, "default"} # (optional) Add an AWS profile through --profile option
plugins:
- serverless-deployment-bucket # https://www.npmjs.com/package/serverless-deployment-bucket
functions:
express: # Name of function
handler: index.handler # Path to function
events:
- http: ANY /
- http: 'ANY {proxy+}' # All requests on this domain are routed to this function
Step 5 - Deploy App
Deploying your app is pretty straightforward. If you do not have an --aws-profile
created yet, read this tutorial
serverless deploy --aws-profile default