Best Serverless Frameworks for Node.js in 2026
·PkgPulse Team
TL;DR
SST for full-stack TypeScript on AWS; Serverless Framework for multi-cloud legacy deployments. SST (~200K weekly downloads) is the modern TypeScript-first framework that deploys to AWS with live function development, type-safe resource binding, and a growing ecosystem. Serverless Framework (~2M downloads) is the older multi-provider tool with thousands of plugins. AWS CDK (~500K) is AWS's official infrastructure-as-code with full TypeScript. For new AWS-native apps in 2026, SST is the compelling choice.
Key Takeaways
- Serverless Framework: ~2M weekly downloads — multi-provider, 1K+ plugins, widely deployed
- SST: ~200K downloads — TypeScript-first, live Lambda dev, resource binding, AWS-native
- AWS CDK: ~500K downloads — low-level AWS infrastructure, TypeScript/Python/Java
- SST v3 — Ion release uses Pulumi under the hood, faster deployments
- Local development — SST live lambda lets you test against real AWS services instantly
Serverless Framework
# serverless.yml — multi-provider configuration
service: my-api
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
DATABASE_URL: ${ssm:/myapp/${self:provider.stage}/database-url}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
Resource: !GetAtt UsersTable.Arn
functions:
createUser:
handler: src/users/create.handler
events:
- httpApi:
path: /users
method: POST
getUser:
handler: src/users/get.handler
events:
- httpApi:
path: /users/{id}
method: GET
processQueue:
handler: src/queue/processor.handler
events:
- sqs:
arn: !GetAtt ProcessingQueue.Arn
batchSize: 10
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}-users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
plugins:
- serverless-offline # Local dev
- serverless-esbuild # TypeScript build
- serverless-prune-plugin # Clean old deployments
// src/users/create.ts — Lambda handler
import type { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { z } from 'zod';
const db = new DynamoDBClient({});
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const body = schema.parse(JSON.parse(event.body ?? '{}'));
await db.send(new PutItemCommand({
TableName: process.env.USERS_TABLE,
Item: {
id: { S: crypto.randomUUID() },
name: { S: body.name },
email: { S: body.email },
createdAt: { S: new Date().toISOString() },
},
}));
return { statusCode: 201, body: JSON.stringify({ success: true }) };
} catch (err) {
return { statusCode: 400, body: JSON.stringify({ error: String(err) }) };
}
};
SST v3 (Modern AWS)
// sst.config.ts — TypeScript infrastructure
import { SSTConfig } from 'sst';
import { Api, Table, Bucket, NextjsSite, Cron } from 'sst/constructs';
export default {
config(input) {
return {
name: 'my-app',
region: 'us-east-1',
};
},
stacks(app) {
app.stack(function API({ stack }) {
const table = new Table(stack, 'Users', {
fields: { id: 'string', email: 'string' },
primaryIndex: { partitionKey: 'id' },
globalIndexes: {
EmailIndex: { partitionKey: 'email' },
},
});
const bucket = new Bucket(stack, 'Uploads');
const api = new Api(stack, 'Api', {
routes: {
'POST /users': 'packages/functions/src/users/create.handler',
'GET /users/{id}': 'packages/functions/src/users/get.handler',
'POST /upload': 'packages/functions/src/upload.handler',
},
// Type-safe resource binding!
bind: [table, bucket],
});
// Next.js site with SSR
const site = new NextjsSite(stack, 'Web', {
path: 'packages/web',
bind: [api, table],
});
// Cron job
new Cron(stack, 'Cleanup', {
schedule: 'rate(1 day)',
job: { function: 'packages/functions/src/cleanup.handler' },
});
stack.addOutputs({
ApiUrl: api.url,
SiteUrl: site.url,
});
});
},
} satisfies SSTConfig;
// SST resource binding — type-safe, no env vars needed
// packages/functions/src/users/create.ts
import { Table } from 'sst/node/table';
import { Bucket } from 'sst/node/bucket';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// SST injects the correct table name/bucket name at runtime
// Table.Users.tableName — automatically bound
// Bucket.Uploads.bucketName — automatically bound
const db = new DynamoDBClient({});
export const handler = async (event) => {
await db.send(new PutItemCommand({
TableName: Table.Users.tableName, // Type-safe, no process.env string
Item: { /* ... */ },
}));
};
# SST commands
npx sst dev # Start live Lambda dev (changes deploy instantly)
npx sst deploy # Deploy to AWS
npx sst deploy --stage prod # Deploy to production
npx sst remove # Tear down all resources
npx sst console # Open SST Console (logs, DynamoDB viewer, etc.)
When to Choose
| Scenario | Pick |
|---|---|
| New AWS project, TypeScript | SST |
| Multi-cloud (AWS + GCP + Azure) | Serverless Framework |
| Complex AWS infrastructure | AWS CDK |
| Legacy Serverless Framework project | Keep SF (migration not worth it) |
| Full-stack (Lambda + Next.js + DynamoDB) | SST |
| Need Serverless Console | Serverless Framework |
| Fine-grained AWS control | AWS CDK |
Compare serverless framework package health on PkgPulse.
See the live comparison
View serverless vs. sst on PkgPulse →