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 |
Production Hardening and State Management
All three tools manage infrastructure state differently, and understanding this matters when things go wrong in production. SST v3 uses Pulumi's state backend — by default stored in S3, but configurable to Pulumi Cloud for team collaboration with locking. This is a significant shift from SST v2's CloudFormation state, meaning teams migrating from v2 to v3 cannot do a seamless in-place upgrade; resources must be re-created. Serverless Framework stores state as CloudFormation stacks, which integrates naturally with AWS's own tooling — aws cloudformation describe-stacks gives you the current deployed state without any Serverless-specific tooling. AWS CDK also targets CloudFormation, so drift detection, stack events, and rollback behavior are all handled by AWS natively. For disaster recovery planning, CDK and Serverless Framework have a decade of production war stories; SST v3 on Pulumi is newer but the underlying Pulumi engine is mature and used by enterprise customers.
Security Considerations and IAM Patterns
Each framework takes a different approach to IAM permissions. SST's link system automatically generates least-privilege IAM policies based on what resources a function is linked to — if you link a bucket, SST grants only the necessary S3 actions. Serverless Framework requires manual IAM statement configuration in serverless.yml, which is verbose but explicit. CDK's grantReadWriteData() pattern on constructs generates appropriate policies automatically, similar to SST's approach but for the full AWS service catalog. All three support VPC deployment for Lambda functions accessing private RDS or ElastiCache resources, though the configuration verbosity differs substantially — SST makes VPC nearly zero-config, while CDK gives you complete subnet and security group control. For secrets management, SST integrates with AWS Secrets Manager and SSM Parameter Store through the secret() primitive; both Serverless Framework and CDK require explicit SSM or Secrets Manager calls.
Migration Paths and Ecosystem Lock-in
Teams evaluating these tools should consider migration costs carefully. Serverless Framework's 1,000+ plugin ecosystem represents years of accumulated community investment, but plugins are only as maintained as their authors; some critical plugins like serverless-offline are community-maintained and occasionally lag behind framework versions. SST v3's move from CDK to Pulumi broke compatibility with v2 configurations entirely — a reminder that frameworks built on top of other tools inherit their breaking change risk. CDK is the most stable long-term bet since it's maintained by AWS itself and is unlikely to undergo architectural pivots, though the verbosity cost is real. For teams concerned about vendor lock-in at the framework level, CDK's direct CloudFormation output means you can always export and manage templates directly if needed.
TypeScript Integration Across Frameworks
The TypeScript story differs meaningfully between the three. SST v3 generates type bindings from your infrastructure config at build time — Resource.DatabaseName.secretArn is a typed string derived from your sst.config.ts, making it impossible to reference a resource that doesn't exist. CDK's TypeScript constructs are fully typed and published to npm with comprehensive JSDoc, making IDE autocomplete excellent across the entire AWS service catalog. Serverless Framework's YAML config is validated at deploy time, not at TypeScript compile time, meaning typos in function names or event configurations surface only during deployment. For teams using TypeScript for everything, SST and CDK offer a significantly better development experience — you get editor errors for misconfigured resources rather than failed deployments.
Cost Implications and Operational Overhead
The total cost of ownership extends beyond the tools themselves. SST v3 with Pulumi Cloud adds a managed state backend cost for teams beyond the free tier. Serverless Framework v4's commercial licensing targets teams generating revenue — the exact threshold ($0) means any production application technically requires a paid plan, though enforcement relies on the honor system currently. CDK is entirely free software with no licensing costs, though the operational overhead of building your own abstractions on top of raw constructs adds engineering time. Earthly Satellites, Depot, and similar adjacent tools can reduce the CI minutes burned on deployment pipelines regardless of which IaC tool you use. The hidden cost in all three is the time spent debugging failed deployments — SST's local development mode dramatically reduces this cycle for Lambda-heavy applications.
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.
See also: middy vs Lambda Powertools vs serverless-http 2026 and Best Serverless Frameworks for Node.js in 2026