To get a simple backend live quickly, serverless is the way to go. Instead of provisioning and managing servers, we can wrap our Express app, run it inside AWS Lambda, and expose it through API Gateway.
In this guide, weβll learn how to deploy a minimal Express API to AWS in a minutes β±οΈ using AWS SAM (Serverless Application Model).
The best part? No need to manage any servers infra - AWS will do it for us.
πPrerequisites and Setup
Before starting, ensure you have the following tools installed and configured:
1οΈβ£ Node.js 18.x+ and npm - Download from nodejs.org
2οΈβ£ AWS CLI - Install and run aws configure
with your AWS credentials
3οΈβ£ AWS SAM CLI - Install from github.com/aws/aws-sam-cli
or use brew install aws-sam-cli
on macOS
4οΈβ£ Active AWS account with Lambda/API Gateway permissions (first 1M requests/month are free)
5οΈβ£ Verify setup by running:aws sts get-caller-identity
and sam --version
π Basic project structure
express-sample-api/
βββ api/
β βββ controllers/
β βββ services/
β βββ middlewares/
β βββ routes/
β
βββ app.ts
βββ handler.ts # entry point for Lambda
βββ package.json
βββ template.yml # AWS SAM template (Lambda + API Gateway)
βββ samconfig.toml # Deployment config (stack name, region, etc.)
πWrapping Express with serverless-http
The bridge between Express and AWS Lambda is serverless-http
It converts incoming API Gateway events into Express-compatible requests. With this, we can run Express just like on a server β but serverlessly.
For that:
1οΈβ£ Run npm i serverless-http
2οΈβ£ Inside handler.ts add:
import serverless from "serverless-http";
import app from "./app";
const { NODE_ENV } = process.env;
export const handler = async (event, context) => {
console.log(`App running in AWS Lambda. Environment: ${NODE_ENV}`);
const lambdaHandler = serverless(app);
return lambdaHandler(event, context);
};
3οΈβ£ Basic app.ts (CORS, Auth and other logic is not the subject of this article, but still a little reminder - please DON'T forget to setup it for real API. You can find the detailed guide in my other article)
import express from "express";
import apiRoutes from "./routes/index";
const { PORT, NODE_ENV, LAMBDA_TASK_ROOT } = process.env;
const runningOnLambda = !!LAMBDA_TASK_ROOT; //this variable is AWS default, no need for manual setup
const app = express();
app.use("/api", apiRoutes);
if (!runningOnLambda) {
app.listen(PORT, () => {
console.log(
`Local server running on http://localhost:${PORT}. Environment:${NODE_ENV}`
);
});
}
export default app;
ποΈ Infrastructure with AWS SAM
With AWS SAM, we define IaC (Infra as code) in template.yml:
1οΈβ£ Define format version and environment variables:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
Environment:
Type: String
Default: test
AllowedValues:
- development
- production
- test
DefaultAwsRegion:
Type: String
Default: eu-west-1 #your AWS region (e.g. us-east-1)
Globals:
Function:
Runtime: nodejs18.x
Environment:
Variables:
NODE_ENV: !Ref Environment #that's how we map values from samconfig.toml to app level environment variable names
DEFAULT_AWS_REGION: !Ref DefaultAwsRegion
2οΈβ£ REST API Gateway that forwards requests to the Lambda:
ExpressSampleGateway:
Type: AWS::Serverless::Api
Properties:
Name: !Sub "ExpressSampleGateway_${Environment}"
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,OPTIONS,PUT,HEAD,PATCH,DELETE'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'*'" #CORS config should be specified here or on the app level, note that the star is used here because CORS is controlled on app level in my case
3οΈβ£ A Lambda function IAM role:
ExpressSampleLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "ExpressSampleLambdaRole_${Environment}"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole #Here we attached minimum required role which is AWS managed, but in case your app need access to other AWS services (e.g. DB, S3, Secrets Manager) - you have to explicitely specify them in another policy.
4οΈβ£ A Lambda function that uses handler.handler as the entry point:
ExpressSampleLambda:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "ExpressSampleLambda_${Environment}"
CodeUri: dist/ #after build command all the files will be inside dist folder (relevant only for TS, while for JS pass just dot - '.')
Role: !GetAtt ExpressSampleLambdaRole.Arn
Handler: handler.handler #our entry file/method
Timeout: 180 #3 min max execution time, while max allowed value might be up to 15 min
MemorySize: 512 #MB RAM
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref ExpressSampleGateway #Here we attach our API gateway as a trigger to Lambda
Path: /{proxy+}
Method: ANY #here we can limit it only to GET or POST methods if needed
Stage: !Ref Environment
5οΈβ£ Outputs that print the API URL once deployed:
Outputs:
ApiEndpoint:
Description: API Gateway endpoint URL
Value: !Sub https://${ExpressSampleGateway}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/
SAM then expands this into CloudFormation under the hood β no console clicking, everything is automated.
FYI - all that can be done in AWS Console (UI) as well
βοΈ Deployment configuration
The samconfig.toml file holds our deployment settings so we donβt have to re-enter them every time:
version = 0.1
[default.build.parameters]
cached = true
parallel = true
[test]
[test.deploy]
[test.deploy.parameters]
stack_name = "express-serverless-sample-test"
resolve_s3 = true
s3_prefix = "express-serverless-sample-test"
region = "eu-west-1"
confirm_changeset = false
capabilities = "CAPABILITY_NAMED_IAM"
parameter_overrides = "Environment=\"test\" DefaultAwsRegion=\"eu-west-1\"" #here we specify the values for our environment variables
image_repositories = []
disable_rollback = false
[prod]
[prod.deploy]
[prod.deploy.parameters]
stack_name = "express-serverless-sample-prod"
resolve_s3 = true
s3_prefix = "express-serverless-sample-prod"
region = "eu-west-1"
confirm_changeset = false
capabilities = "CAPABILITY_NAMED_IAM"
parameter_overrides = "Environment=\"production\" DefaultAwsRegion=\"eu-west-1\"" #here we specify the values for our environment variables
image_repositories = []
disable_rollback = false
π Build & deploy
Deploying is just 3 steps:
-
npm run build
- optional, needed only in case you use TS -
sam build
π οΈ -
sam deploy --config-env test
(e.g. prod - for production) π€
When itβs done, youβll see the API URL in the terminal - outputs section. Test it immediately with curl, Postman, or browser π.
π Verify in the AWS Console
After deployment, confirm everything works:
1οΈβ£ API Gateway β APIs β ExpressSampleGateway_test
Go to the Stages tab β test.
Copy the Invoke URL π
2οΈβ£ Lambda β Functions β ExpressSampleLambda_test
Open the Monitor tab π
Check for recent invocations
For logs and debugging, head over to AWS CloudWatch Logs π.
Got questions? Drop them in the comments!
Top comments (0)