How to deploy Lambda apps with the Serverless Framework
A step-by-step guide on how to use the Serverless Framework to deploy Lambda functions, including the set up and testing phases.
Dec 17, 2024 • 7 Minute Read
Serverless architectures have proven to be more than just a trend, and as more and more teams move towards leveraging functions and serverless services to build their architectures, you might be curious about how these architectures are deployed.
One such option, the Serverless Framework, lets users deploy functions and services not quite at the click of a button, but with a single command: serverless deploy. Cloud-agnostic, the Serverless Framework lets you work across clouds, and can be used to deploy some of the most popular function-as-a-service offerings, including AWS Lambda.
The Demo Scenario
To demonstrate deploying Lambda functions with the Serverless Framework, we'll need a demo scenario: And in this case, we'll be deploying a Node.js HTTP web API, although the steps we go through will remain the same regardless of language you decide to use in the future. The app will contain a serverless DynamoDB database, and two functions: One to add order information to the database, and one to retrieve any orders stored. Each component of the application will be deployed entirely with the Serverless Framework.
Set Up
To begin, we'll need to make sure the Serverless Framework is up and running. First, install it if you haven't already:
npm install -g serverless
We then need to configure our application. To do this, run the serverless command:
serverless
As of version 4, the Serverless Framework requires all users to have an account and log in to use the product, so you'll be prompted to input your login information in a browser window. Don't worry, though, everything we'll be doing in this guide is well within their free tier—although you will still have to pay for the AWS resources used, so be sure to clean up at the end! (We'll cover the cleanup, too.)
Once logged in, you'll be prompted to select a template. Choose AWS / Node.js / HTTP API.
Then give your project a name. I'm calling mine orderup, because it stores order information.
Next, name the application: The Serverless Framework lets you organize multiple applications within a single project. We only need one for this demo, and I'm going to call it orderup here as well.
Finally, we need to input our AWS credentials. Since I'm using a temporary AWS playground account, I'm going to select Save AWS Credentials in a Local Profile, but choose the option that's best for you:
? AWS Credentials Set-Up Method: …
Create AWS IAM Role (Easy & Recommended)
❯ Save AWS Credentials in a Local Profile
Skip & Set Later (AWS SSO, ENV Vars)
And with our credentials configured, we can now work on our application!
The serverless.yml File
When we ran the serverless command, the Serverless Framework generated a project template within a new orderup directory. Move into this directory and review the contents:
cd orderup
ls
Notice the three generated files, a README, a handler.js that contains "Hello, world!" code, and the serverless.yml file that contains our serverless deployment information. This is the file we'll work with most extensively, and where we'll define our serverless components. But first, let's add our functions!
Add Function Code
We'll call our first function addOrder.mjs and input the following code:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
const tableName = process.env.TABLE_NAME;
export const handler = async (event, context) => {
let body;
let statusCode = 200;
const headers = {
"Content-Type": "application/json",
};
try {
if (event.routeKey === "PUT /orders") {
const requestJSON = JSON.parse(event.body);
await dynamo.send(
new PutCommand({
TableName: tableName,
Item: {
id: requestJSON.id,
pie: requestJSON.pie,
quantity: requestJSON.quantity,
customerName: requestJSON.customerName,
deliveryDate: requestJSON.deliveryDate,
},
})
);
body = `Received order ${requestJSON.id}`;
} else {
throw new Error(`Unsupported route: "${event.routeKey}"`);
}
} catch (err) {
statusCode = 400;
body = err.message;
} finally {
body = JSON.stringify(body);
}
return {
statusCode,
body,
headers,
};
};
Then create the second function, getOrders.mjs:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
const tableName = process.env.TABLE_NAME;
export const handler = async (event, context) => {
let body;
let statusCode = 200;
const headers = {
"Content-Type": "application/json",
};
try {
if (event.routeKey === "GET /orders") {
const result = await dynamo.send(
new ScanCommand({ TableName: tableName })
);
body = result.Items;
} else {
throw new Error(`Unsupported route: "${event.routeKey}"`);
}
} catch (err) {
statusCode = 400;
body = err.message;
} finally {
body = JSON.stringify(body);
}
return {
statusCode,
body,
headers,
};
};
We can now open up the serverless.yml file and make our edits.
Project Definitions
Currently, the default code in this file is set up to run the Hello, World function in the handler.js file, but we need to update it. Keep the org, app, and service definitions as-is, removing the commented text if you want:
org: catcantha # Ensure this is set to YOUR Serverless Framework org, not mine!
app: orderup
service: orderup
Provider Definitions
We'll need to make some additions to the provider definition, however. Notably, we're going to add a region, an environmental variable for the database name, and make some IAM role adjustments since our functions need to be able to add and remove items from a DynamoDB database:
provider:
name: aws
runtime: nodejs20.x
region: us-east-1 # Update to use your chosen region
environment:
TABLE_NAME: ${self:custom.tableName}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:DeleteItem
- dynamodb:Scan
Resource:
- arn:aws:dynamodb:us-east-1:*:table/${self:custom.tableName}
Resource Definitions
Now, notice this references a service we haven't yet defined: Our DynamoDB table. We can create this within our Serverless Framework definition. It's just a matter of adding a resources block to the end of our definition:
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.tableName}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
custom:
tableName: OrderTable
If you're familiar with AWS CloudFormation, this might look familiar: And that's because it shares much of the structure of a CloudFormation definition. And, in fact, upon deployment, the Serverless Framework will generate a CF template upon which it deploys. Notice, too, how we define the table name as a custom variable, and include key schema definitions, with id being our primary key.
Function Definitions
Finally, we can update our Lambda function definition. This is the part contained under the functions parameter, and currently deploys that Hello, World function. Remove everything but the functions: line—we're starting fresh!
functions:
From here, we'll define the first of our functions, the addOrder function. We'll define the handler based on the file name, and define the events that trigger this function. In this case, an HTTP PUT event made to the /orders endpoint. Notice how we don't have to define an API Gateway resource: It's all part of the function definition:
functions:
addOrder:
handler: addOrder.handler
events:
- http:
path: orders
method: put
We'll then do the same for our second function, this time setting it for a GET event to the /orders endpoint, and updating the name with respect to the file:
getOrders:
handler: getOrders.handler
events:
- http:
path: orders
method: get
Our complete file should now look like:
org: catcantha # Update to use your Serverless Framework org
app: orderup
service: orderup
provider:
name: aws
runtime: nodejs20.x
region: us-east-1 # Update to use your preferred region
environment:
TABLE_NAME: ${self:custom.tableName}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:DeleteItem
- dynamodb:Scan
Resource:
- arn:aws:dynamodb:us-east-1:*:table/${self:custom.tableName}
functions:
addOrder:
handler: addOrder.handler
events:
- http:
path: orders
method: put
getOrders:
handler: getOrders.handler
events:
- http:
path: orders
method: get
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.tableName}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
custom:
tableName: OrderTable
Deploy the Function
We can now test our work! Deploy the function by running:
serverless deploy
Once deployed, it will display the API endpoints deployed, as well as provide information regarding the name of our functions:
endpoints:
PUT - https://API-URL.execute-api.us-east-1.amazonaws.com/dev/orders
GET - https://API-URL.execute-api.us-east-1.amazonaws.com/dev/orders
functions:
addOrder: orderup-dev-addOrder (2.8 kB)
getOrders: orderup-dev-getOrders (2.8 kB)
Test the Function
Of course, we want to make sure everything works, too. To do this, we can create test data for both functions. Let's start with our PUT event. Save the following code to putEvent.json:
{
"routeKey": "PUT /orders",
"body": "{\"id\": \"1234\", \"pie\": \"apple\", \"quantity\": 2, \"customerName\": \"John Doe\", \"deliveryDate\": \"2025-11-10\"}"
}
We can then see if we our addOrder function functions, by running:
serverless invoke -f addOrder --path putEvent.json
Success! Our order has been received.
To test the GET endpoint, create getEvent.json and populate it with the following:
{
"routeKey": "GET /orders"
}
Then run:
serverless invoke -f getOrders --path getEvent.json
If you see the order we just input, then congratulations! Your functions work! You have officially deployed a fully functional serverless application with the Serverless Framework with AWS Lambda.
Cleanup
To clean up your work, be sure to run:
serverless remove
More about Serverless development
And if that left you itching to write your own serverless applications, but you don't know where to start, be sure to check out our Serverless Foundations Path, which will get you thinking like a serverless developer and on the path to creating well-architected Serverless apps!