Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

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

Please set an alt value for this image...
  • AWS
  • Cloud
  • Intermediate

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!