Migrate your Node.js application to serverless using AWS Lambda
A step-by-step tutorial where we’ll learn how to convert your Node.js project to AWS Lambda and start saving money!
AWS Lambda is the main serverless AWS computing service. With AWS Lambda, you don’t have to create EC2 instances, you don’t have to provide any server, and you don’t care about scaling; you are just focused on your code.

One of the main reasons for using AWS Lambda is that you’ll only pay for what you use. If you are developing a proof of concept and don’t have many users, this is extremely useful! I used to pay $10 per month per proof of concept, and right now, it’s free for me! Also, the AWS Lambda free tier is amazing; it includes one million free requests and 400,000 GB-seconds of computing time per month! And if you go over it, you will only pay $0.20 per 1M requests (apart from other factors like the duration of the request).

The only limitation? AWS Lambda functions may run up to 15 minutes per execution, so you shouldn’t use this service if you have a really heavy endpoint. This shouldn’t be your case if you are developing a Node.js application like in this case. Let’s learn how to do it with a few commands!
- Install Serverless
- Refactor Express code
- Create Serverless.yml file
- Change behavior and models to allow concurrency
- Deploy the serverless application on AWS Lambda
- Change environment variables in the Frontend
1) Install Serverless
Before we start the process, we need to install three dependencies:
- “Serverless”, the Serverless framework.
- “Serverless-offline”, a plugin that will emulate AWS Lambda and API Gateway on your local machine. This will be really useful to test everything locally before deploying it.
- “Serverless-http”, a module that allows you to ‘wrap’ your API for serverless use.
You can install all of them by executing the following commands:
npm install -g serverless npm install serverless-offline --save-dev npm install serverless-http
Your package.json should look something like this (with all the other dependencies that your project need).
{
"dependencies": {
"aws-sdk": "^2.831.0",
"serverless-http": "^3.0.2",
"uuid-apikey": "^1.5.1",
"winston": "^3.3.3"
},
"devDependencies": {
"serverless-offline": "^8.1.0",
"nodemon": "^2.0.6"
}
}
2) Refactor Express Code
Right now, my application (the one we are trying to move to Lambda) is running on a server. As you can see, before starting this tutorial, it’s the typical code of an Express application:
require('dotenv').config();
const express = require('express');
require('./db');
const app = express();
require('./express')(app); //Load the middlewares that you need.
require('./routes')(app);
const PORT = process.env.PORT || 4100;
app.listen(PORT, () => {
return logger.info(`Server listening on port ${PORT}`);
});
Now we need to refactor this code to adapt it to serverless. Let’s see the steps that we’ve followed. I’m going to first show you the new code, and then I’ll explain the modifications.
require('dotenv').config();
const serverless = require('serverless-http');
const express = require('express');
require('./db');
const app = express();
require('./express')(app); //Load the middlewares that you need.
require('./routes')(app);
module.exports.handler = serverless(app);
- Remove the “app.listen()” function, which binds and listens to the specified host and port connections. As we will no longer use a server, we can omit them.
- We also added the “serverless-http” dependency, created the handler, and wrapped it with the serverless variable.
3) Create the Serverless.yml file
The serverless.yml file is one of the most important files when creating a serverless application. You can define your application's infrastructure, configuration, and deployment settings. Some parameters that you can configure:
- Resources that your application will use (AWS Lambda in this case)
- Events that will trigger your AWS Lambda functions (CRON expressions, HTTP requests, etc).
- The region where your application will be deployed
- … and more!
Before defining all the parameters to understand it, let’s talk a look at mine. Your serverless.yml file will be similar to this one, so let’s also try to understand it:
service: myproject-lambda
provider:
name: aws
runtime: nodejs14.x
lambdaHashingVersion: 20201221
stage: prod
region: eu-west-1
custom:
serverless-offline:
httpPort: 4100
functions:
app:
handler: src/index.handler
events:
- http:
path: /
method: ANY
cors: true
- http:
path: /{proxy+}
method: ANY
cors: true
plugins:
- serverless-offline
- Provider → It is used to specify the cloud provider you use for your serverless application and other configuration settings for this provider. In my case, I’ll deploy it to the“eu-west-1" AWS region. We can define other attributes, like the runtime, which is really important. If you are using Node.js 16, you’ll have to specify the correct version there.
- Custom → We can define custom settings for your serverless application. In this case, we’ll configure the “serverless-offline” plugin to run on port 4100 locally. We also need to enable it in the “plugins” section.
- Functions → This is probably the most important part of the file. We’ll define the Lambda functions and their configuration. In a serverless framework, you would normally define many of them, but in this case, it’s unnecessary. Why? All the routes are already defined in the index file of our project. Using the proxy+ property, we allow any call via the API gateway. You can read more about it at the following link. It’s also important to realize the ANY method for all of our express routes to work, so it will accept GET, POST, PUT, DELETE… requests.
After creating this file, we should be able to run our application! Let’s try locally by running the “serverless offline start” command. We should see something like this, listing all the endpoints we can access from our application. The two important ones are the ANY endpoints
ANY | http://localhost:4100/prod
POST | http://localhost:4100/2015-03-31/functions/app/invocations
ANY | http://localhost:4100/prod/{proxy*}
POST | http://localhost:4100/2015-03-31/functions/app/invocations
Just as an example, thanks to the proxy parameter that we defined, we can access the same routes that we previously had in our application:
GET http://localhost:4100/prod/api/apps
POST http://localhost:4100/prod/api/users
...and more!
4) Change behavior and MongoDB Models to allow concurrency
In this section, I will explain some of the errors I’ve found while moving the application to serverless. Most of them were regarding concurrency. Just as an example, when running the same function twice, the application gave me the following errors:
MongoDB Schema Errors: “Cannot overwrite model once compiled”.
Reading about this problem, I realized that the serverless function tried re-defining the MongoDB schema (after the first execution). To solve it, we just return the model if it already exists. Otherwise, we create it:
const mongoose = require("mongoose");
const { Schema } = mongoose;
const UserSchema = new Schema({
email: {
type: String,
unique: true,
},
});
module.exports = mongoose.models.User || mongoose.model("User", UserSchema);
Firebase Initializes App Twice.
Another specific error I found was regarding Firebase (the service I use to authenticate users). When running the lambda the second time, it tries to initialize the app twice. Again, a problem of AWS Lambda trying to create a resource multiple times, so we just need to control it.
if (!firebaseAdmin.apps.length) {
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URI,
});
}
Each application will face different problems, but most will be similar.
5) Deploy Serverless Application on AWS Lambda
To do that, you’ll need an AWS account. You can create one at the following link.
After deploying the project, this will create different resources on AWS:
- Several AWS Lambda functions.
- API Gateway → Will be used as our entry point. This will generate the URL we’ll have to call from the front end. We’ll see it in the next section.
6) Change Environment Variables in the Frontend
If you go to the Lambas Service on AWS and click on your function, you’ll see a screen like this one:

By clicking “API Gateway”, you’ll get the URL from your app. Substitute it in your frontend provider (Vercel, Netlify, etc), and it should work! Remember, change the {proxy} part for your desired endpoint.

Thanks for Reading!
If you like my work and want to support me:
- You can follow me on Medium here.
- Feel free to clap if this post is helpful for you! :)