DEV Community

Yevheniia
Yevheniia

Posted on

πŸš€ Deploy a Node.js Express API to AWS with SAM, Lambda, and API Gateway β€” in Minutes, Zero servers to manage

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.)
Enter fullscreen mode Exit fullscreen mode

πŸ”Œ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);
};


Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ 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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}/ 
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

πŸš€ Build & deploy

Deploying is just 3 steps:

  1. npm run build - optional, needed only in case you use TS
  2. sam buildπŸ› οΈ
  3. 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)