SST v3 vs Serverless Framework vs AWS CDK: Node.js IaC in 2026
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 devprovides 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)),
};
};
Popular Plugins
# 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
| Feature | SST v3 (Ion) | Serverless Framework v4 | AWS CDK v2 |
|---|---|---|---|
| Language | TypeScript | YAML + JS hooks | TypeScript |
| Backend engine | Pulumi | CloudFormation | CloudFormation |
| Live dev mode | ✅ sst dev (real AWS) | ✅ sls offline (emulated) | ❌ No official dev mode |
| Type-safe resource bindings | ✅ Auto-generated | ❌ Manual env vars | Partial (manual) |
| Plugin ecosystem | Growing | ✅ 1,000+ plugins | ✅ Constructs library |
| Next.js support | ✅ sst.aws.Nextjs | Via plugins | ❌ Custom construct needed |
| Multi-cloud | ✅ Pulumi backend | AWS only | AWS only |
| Learning curve | Low | Low | High |
| Commercial licensing | MIT | Teams: paid | Apache 2.0 |
| GitHub stars | 22k | 46k | 12k |
| AWS service coverage | Curated | Comprehensive | Full AWS |
When to Use Each
Choose SST v3 if:
- Developer experience is the priority —
sst devlive 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.