DEV Community

Cover image for ⏱ 10 Minute Tutorial: Creating a Serverless Express Web Server & API
Nader Dabit for AWS

Posted on • Updated on

⏱ 10 Minute Tutorial: Creating a Serverless Express Web Server & API

One of the most popular use cases of serverless functions is deploying and running a web server complete with routing. In this tutorial, I'll show you how to get this up and running in only a few minutes using AWS Lambda, Amazon API Gateway, and AWS Amplify.

The library I'll be using is the Serverless Express project that's made specifically for this use case.

Using this library, you can easily proxy the event and context into the express server, and from there you'll have access to the different routes and HTTP methods like get, post, put, and delete.

app.get('/', (req, res) => {
  res.json(req.apiGateway.event)
})
Enter fullscreen mode Exit fullscreen mode

Getting started

There are many ways to deploy a Lambda function, you can go directly into the AWS console, use the serverless framework, or a multitude of other tools that utilize infrastructure as code under the hood.

I will be using a CLI based approach with the Amplify Framework.

To get started, first install and configure the Amplify CLI.

To see a video walkthrough of how to configure the CLI, click here.

$ npm install -g @aws-amplify/cli

$ amplify configure
Enter fullscreen mode Exit fullscreen mode

Now, create a project using your JavaScript framework of choice (React, Angular, Vue etc..).

$ npx create-react-app myapp

$ cd myapp

$ npm install aws-amplify
Enter fullscreen mode Exit fullscreen mode

Next, initialize a new Amplify project in the root of your JS project:

$ amplify init

# Answer the questions prompted by the CLI
Enter fullscreen mode Exit fullscreen mode

Now, we can create the API and web server. To do so, we can use the Amplify add command:

$ amplify add api

? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this category in the project: myapi
? Provide a path (e.g., /items): /items (or whatever path you would like)
? Choose a Lambda source: Create a new Lambda function
? Provide a friendly name for your resource to be used as a label for this category in the project: mylambda
? Provide the AWS Lambda function name: mylambda
? Choose the function template that you want to use: Serverless express function
? Do you want to access other resources created in this project from your Lambda function? N
? Do you want to edit the local lambda function now? N
? Restrict API access: N
? Do you want to add another path? N
Enter fullscreen mode Exit fullscreen mode

The CLI has created a few things for us:

  • API endpoint
  • Lambda function
  • Web server using Serverless Express in the function
  • Some boilerplate code for different methods on the /items route

Let's open the code for the server.

Open amplify/backend/function/mylambda/src/index.js. Here, you will see the main function handler with the event and context being proxied to an express server located at ./app.js:

const awsServerlessExpress = require('aws-serverless-express');
const app = require('./app');

const server = awsServerlessExpress.createServer(app);

exports.handler = (event, context) => {
  console.log(`EVENT: ${JSON.stringify(event)}`);
  awsServerlessExpress.proxy(server, event, context);
};

Enter fullscreen mode Exit fullscreen mode

Next, open amplify/backend/function/mylambda/src/app.js.

Here, you will see the code for the express server and some boilerplate for the different HTTP methods for the route we declared. Find the route for app.get('/items') and update it to the following:

// amplify/backend/function/mylambda/src/app.js
app.get('/items', function(req, res) {
  const items = ['hello', 'world']
  res.json({ success: 'get call succeed!', items });
});
Enter fullscreen mode Exit fullscreen mode

We can test it locally before deploying it, but we first need to install the dependencies for the Lambda:

$ cd amplify/backend/function/mylambda/src && npm install && cd ../../../../../
Enter fullscreen mode Exit fullscreen mode

To invoke the function and start the server, run the following command:

$ amplify function invoke mylambda
Enter fullscreen mode Exit fullscreen mode

Now, the server is running on port 3000 and we can make requests against it. To do this from the command line, we can run this curl command:

$ curl http://localhost:3000/items

# {"success":"get call succeed!","items":["hello","world"]}%
Enter fullscreen mode Exit fullscreen mode

To deploy the API and function, we can run the push command:

$ amplify push
Enter fullscreen mode Exit fullscreen mode

Now, from any JS client, you can start interacting with the API:

// get request
const items = await API.get('myapi', '/items')

// post with data
const data = { body: { items: ['some', 'new', 'items'] } }
await API.post('myapi', '/items', data)
Enter fullscreen mode Exit fullscreen mode

From here, you may want to add additional path. To do so, run the update command:

$ amplify update api
Enter fullscreen mode Exit fullscreen mode

From there, you can add, update, or remove paths.

For more info on the API category, click here.

Discussion (6)

Collapse
sorandom1611 profile image
Jason

Thank you for a very interesting article. I just have a quick question regarding this architecture.

There have been concerns surrounding using Lambda to serve as express servers, specifically surrounding the fact that Lambda has a cold start and therefore is unable to serve traffic on the same level as a regular docker container / EC2 instance.

As I am quite new to this architecture, would you mind providing some insight into this issue, as well as why one would look to migrate to this new design?

Collapse
matttyler profile image
Matt Tyler • Edited

The python and javascript runtimes provided by AWS for lambda are quite fast, and experience fairly short cold starts in comparison with larger VM based frameworks, such as Java and .NET. If you are using a smaller framework like express or flask, a typical cold start is usually under one second.

Two new features were announced recently to help performance issues. The first is provisioned concurrency, which allows you to set a number of lambda instances that are kept warm. This helps alleviated some of the issues with cold-start penalties and helps with long-tail latency. Secondly, HTTP API's were announced for API Gateway - this mode strips back some of the features of the existing API Gateway service and offers better performance for web frameworks like flask and express.

In most circumstances it's a cost optimization. Re-homing from EC2 or a container will likely result in less management and lower cost - though it does depend on the traffic to the application in question. For example, if you experience spiky traffic load, and/or demand that follows a certain pattern (e.g. significantly fewer users at certain periods), lambda's scale-to-zero model results in lower costs over other alternatives.

There are more advanced ways to architect an application use API Gateway, with things like VTL transformations/service integrations etc, and these offer better cost and performance, but asks for greater investment from a development team to learn and understand. You can often get a quicker return on your investment sticking with existing frameworks that your team is already familiar with.

As such, using light frameworks such as express and flask are viable ways to serve content from lambda and is a good way to start getting to grips with serverless without needing to throw out all your existing knowledge. It's a good example of AWS meeting people where they are.

Details:

aws.amazon.com/blogs/compute/annou...

aws.amazon.com/blogs/aws/new-provi...

Collapse
ankurloriya profile image
Ankur Loriya • Edited

amplify function invoke mylambda

{"statusCode":404,"body":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /</pre>\n</body>\n</html>\n","headers":{"x-powered-by":"Express","access-control-allow-origin":"*","access-control-allow-headers":"*","content-security-policy":"default-src 'none'","x-content-type-options":"nosniff","content-type":"text/html; charset=utf-8","content-length":"139","date":"Sat, 30 Jan 2021 07:17:46 GMT","connection":"close"},"isBase64Encoded":false}
Enter fullscreen mode Exit fullscreen mode
Collapse
victorioberra profile image
Victorio Berra

I have found this to be cleaner than distributing my endpoints through several unique functions, even using the Serverless Framework for NodeJS. There are some drawbacks, for example, with a dedicated function per endpoint I can scale and warm independently and the Serverless Framework gives me the ability to keep everything in one codebase, configuration as code and share code similar to this approach. More ramblings: Amplify looks like its starting to sprawl.

Collapse
pdamra profile image
Philip Damra

I know that "monolith Lambdas" are supposed to be an anti-pattern, but this seems like a great way to facilitate code reuse and help migrate existing codebases to serverless.

Collapse
georgewl profile image
George WL

It's weird that the npm module page for the official tooling has such poor documentation and you have to then find the documentation yourself.