Skip to main content

SST v3 vs Serverless Framework vs AWS CDK: Node.js IaC in 2026

·PkgPulse Team

SST v3 vs Serverless Framework vs AWS CDK: Node.js IaC in 2026

TL;DR

Deploying Node.js to AWS Lambda, API Gateway, and S3 requires an infrastructure-as-code layer. SST v3 (Ion, now Pulumi-based) is the developer-first choice — opinionated, fast sst dev live Lambda debugging, automatic type-safe resource bindings, and a growing component ecosystem. Serverless Framework v4 is the classic choice — massive plugin ecosystem, 10 years of battle-testing, declarative YAML, but now has commercial licensing for teams. AWS CDK v2 is the AWS-native choice — TypeScript constructs, the most powerful and flexible option, but with a steeper learning curve and no developer experience improvements out of the box. For developer experience and speed: SST v3. For maximum plugin ecosystem and YAML declarativeness: Serverless Framework. For maximum AWS flexibility and enterprise control: AWS CDK.

Key Takeaways

  • SST v3 (Ion) is now built on Pulumi — moved from CDK in 2024, enabling multi-cloud deployments
  • Serverless Framework v4 introduced commercial licensing — free for individuals, paid for teams (>$0 revenue)
  • SST's sst dev provides live Lambda debugging — breakpoints, hot reloading, without the deploy cycle
  • AWS CDK GitHub stars: ~12k — the most comprehensive AWS IaC tool
  • SST GitHub stars: ~22k — fastest-growing opinionated serverless framework
  • All three deploy to AWS Lambda — the decision is about DX, abstractions, and ecosystem
  • SST's type bindings auto-generate TypeScript types from your infrastructure configuration

SST v3 (Ion): Developer Experience First

SST Ion (v3) rebuilt the framework on Pulumi instead of CDK, enabling faster deployments and multi-cloud support. The signature feature is sst dev — a live development mode where Lambda functions run locally but are invoked by real AWS triggers.

Installation

npx sst@latest create my-app
cd my-app
npm install

sst.config.ts — Declarative Infrastructure

// sst.config.ts — TypeScript infrastructure configuration
/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "my-saas-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
    };
  },

  async run() {
    // S3 bucket
    const bucket = new sst.aws.Bucket("UserAssets", {
      public: true,  // Public read
    });

    // RDS PostgreSQL
    const database = new sst.aws.Postgres("AppDatabase", {
      scaling: {
        min: "0 ACU",   // Scale to zero when idle
        max: "4 ACU",
      },
    });

    // Function with resource bindings
    const api = new sst.aws.Function("ApiHandler", {
      handler: "src/api/handler.handler",
      link: [bucket, database],  // Automatic type-safe bindings
      environment: {
        STAGE: $app.stage,
      },
    });

    // API Gateway
    new sst.aws.ApiGatewayV2("Api", {
      routes: {
        "GET /users": api,
        "POST /users": api,
        "GET /users/{id}": api,
      },
    });

    // Next.js app
    new sst.aws.Nextjs("Web", {
      path: "apps/web",
      link: [database, bucket],  // Web app also has type-safe access
    });

    return {
      bucketUrl: bucket.url,
    };
  },
});

Type-Safe Resource Bindings

// src/api/handler.ts — no hardcoded env vars, type-safe SST Resource
import { Resource } from "sst";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Pool } from "pg";

const s3 = new S3Client({});
const db = new Pool({ connectionString: Resource.AppDatabase.secretArn });

export const handler = async (event: APIGatewayProxyEvent) => {
  // TypeScript knows exactly what's available
  const bucketName = Resource.UserAssets.name;         // string
  const dbArn = Resource.AppDatabase.secretArn;         // string

  await s3.send(new PutObjectCommand({
    Bucket: bucketName,
    Key: "uploads/file.jpg",
    Body: Buffer.from("content"),
  }));

  const { rows } = await db.query("SELECT * FROM users LIMIT 10");

  return {
    statusCode: 200,
    body: JSON.stringify({ users: rows }),
  };
};

sst dev — Live Lambda Development

# Start live development — Lambda runs locally, invoked by real AWS events
sst dev

# What happens:
# 1. SST deploys a "stub" Lambda to AWS (instant, no code upload)
# 2. When AWS invokes the Lambda, request is forwarded to your local machine
# 3. Your local code executes with full debugger support
# 4. Response is sent back to AWS

# Result: Real API Gateway → Real DynamoDB → Your local Lambda code
# Breakpoints work. console.log streams in real-time.
# No Docker needed. No local Lambda emulation. Real AWS.

Serverless Framework v4: The Classic Choice

Serverless Framework has deployed billions of Lambda functions since 2015. Version 4 introduced a commercial licensing model but remains the most plugin-rich option.

Installation

npm install -g serverless@v4
serverless create --template aws-nodejs-typescript --path my-service

serverless.yml Configuration

# serverless.yml
service: my-saas-app

frameworkVersion: "4"

provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  stage: ${opt:stage, 'dev'}
  environment:
    STAGE: ${self:provider.stage}
    DB_URL: ${ssm:/my-app/${self:provider.stage}/db-url}
    S3_BUCKET: ${self:custom.bucketName}

  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
          Resource: !Sub "arn:aws:s3:::${self:custom.bucketName}/*"
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:Query
          Resource: !GetAtt UsersTable.Arn

custom:
  bucketName: my-app-${self:provider.stage}-assets
  webpack:
    webpackConfig: webpack.config.js
    includeModules: true

functions:
  getUser:
    handler: src/handlers/user.get
    events:
      - httpApi:
          path: /users/{id}
          method: get

  createUser:
    handler: src/handlers/user.create
    events:
      - httpApi:
          path: /users
          method: post

  processQueue:
    handler: src/handlers/queue.process
    events:
      - sqs:
          arn: !GetAtt UserQueue.Arn
          batchSize: 10

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: users-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

    UserQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: user-queue-${self:provider.stage}

Handler with TypeScript

// src/handlers/user.ts
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";

const dynamo = new DynamoDBClient({ region: process.env.AWS_REGION });

export const get: APIGatewayProxyHandlerV2 = async (event) => {
  const userId = event.pathParameters?.id;

  if (!userId) {
    return { statusCode: 400, body: JSON.stringify({ error: "Missing user ID" }) };
  }

  const result = await dynamo.send(new GetItemCommand({
    TableName: `users-${process.env.STAGE}`,
    Key: marshall({ id: userId }),
  }));

  if (!result.Item) {
    return { statusCode: 404, body: JSON.stringify({ error: "User not found" }) };
  }

  return {
    statusCode: 200,
    body: JSON.stringify(unmarshall(result.Item)),
  };
};
# serverless.yml — popular plugins ecosystem
plugins:
  - serverless-esbuild         # TypeScript/ESM bundling
  - serverless-offline         # Local API Gateway emulation
  - serverless-dynamodb-local  # Local DynamoDB for testing
  - serverless-prune-plugin    # Delete old Lambda versions
  - serverless-domain-manager  # Custom domains
  - serverless-lift            # Higher-level constructs (SQS, S3 website)

AWS CDK v2: Maximum AWS Power

AWS CDK (Cloud Development Kit) is Amazon's official IaC framework. You write TypeScript constructs that synthesize into CloudFormation templates. Maximum flexibility, maximum verbosity.

Installation

npm install -g aws-cdk
npm install aws-cdk-lib constructs
cdk init app --language typescript

CDK Stack Definition

// lib/my-app-stack.ts
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigw from "aws-cdk-lib/aws-apigatewayv2";
import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class MyAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // S3 bucket
    const bucket = new s3.Bucket(this, "AssetsBucket", {
      bucketName: `my-app-${this.account}-assets`,
      publicReadAccess: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // DynamoDB table
    const usersTable = new dynamodb.Table(this, "UsersTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Lambda function with automatic bundling
    const apiHandler = new NodejsFunction(this, "ApiHandler", {
      entry: "src/api/handler.ts",
      runtime: lambda.Runtime.NODEJS_22_X,
      environment: {
        USERS_TABLE: usersTable.tableName,
        ASSETS_BUCKET: bucket.bucketName,
        STAGE: this.node.tryGetContext("stage") ?? "dev",
      },
      bundling: {
        minify: true,
        externalModules: ["@aws-sdk/*"],
      },
    });

    // Grant permissions
    usersTable.grantReadWriteData(apiHandler);
    bucket.grantReadWrite(apiHandler);

    // HTTP API
    const api = new apigw.HttpApi(this, "HttpApi", {
      corsPreflight: {
        allowHeaders: ["Content-Type", "Authorization"],
        allowMethods: [apigw.CorsHttpMethod.GET, apigw.CorsHttpMethod.POST],
        allowOrigins: ["https://yourapp.com"],
      },
    });

    const lambdaIntegration = new integrations.HttpLambdaIntegration(
      "ApiIntegration",
      apiHandler
    );

    api.addRoutes({
      path: "/users",
      methods: [apigw.HttpMethod.GET, apigw.HttpMethod.POST],
      integration: lambdaIntegration,
    });

    api.addRoutes({
      path: "/users/{id}",
      methods: [apigw.HttpMethod.GET, apigw.HttpMethod.DELETE],
      integration: lambdaIntegration,
    });

    // Output API URL
    new cdk.CfnOutput(this, "ApiUrl", { value: api.url! });
  }
}

CDK Deployment

# Bootstrap (once per AWS account/region)
cdk bootstrap aws://ACCOUNT_ID/us-east-1

# Synthesize CloudFormation template
cdk synth

# Compare what will change
cdk diff

# Deploy
cdk deploy --context stage=dev

# Destroy
cdk destroy

Feature Comparison

FeatureSST v3 (Ion)Serverless Framework v4AWS CDK v2
LanguageTypeScriptYAML + JS hooksTypeScript
Backend enginePulumiCloudFormationCloudFormation
Live dev modesst dev (real AWS)sls offline (emulated)❌ No official dev mode
Type-safe resource bindings✅ Auto-generated❌ Manual env varsPartial (manual)
Plugin ecosystemGrowing✅ 1,000+ plugins✅ Constructs library
Next.js supportsst.aws.NextjsVia plugins❌ Custom construct needed
Multi-cloud✅ Pulumi backendAWS onlyAWS only
Learning curveLowLowHigh
Commercial licensingMITTeams: paidApache 2.0
GitHub stars22k46k12k
AWS service coverageCuratedComprehensiveFull AWS

When to Use Each

Choose SST v3 if:

  • Developer experience is the priority — sst dev live debugging is transformative
  • You're building Next.js, Astro, or SvelteKit on AWS
  • You want automatic TypeScript types from your infrastructure config
  • Your team is building a modern SaaS and wants fast iteration

Choose Serverless Framework if:

  • You need specific plugins not available in other frameworks
  • Your team knows YAML and prefers declarative configuration
  • You have existing Serverless Framework v1/v2/v3 code to maintain
  • Individual developers (commercial licensing is free for individuals)

Choose AWS CDK if:

  • Maximum AWS service coverage and control is required
  • Your organization has CloudFormation expertise and governance requirements
  • You need to compose your own constructs for company-specific patterns
  • Multi-account, complex enterprise AWS architectures are in scope

Methodology

Data sourced from GitHub repositories (star counts as of February 2026), official documentation, npm weekly download statistics (January 2026), and community benchmarks on deployment speed from the serverless community Discord and HackerNews discussions. Feature availability verified against SST Ion v3, Serverless Framework v4, and AWS CDK v2 documentation.


Related: Coolify vs Caprover vs Dokku for self-hosted PaaS alternatives, or Temporal vs Restate vs Windmill for durable workflow orchestration on serverless.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.