sst-dev
SST.dev Development Skill
This skill provides comprehensive guidance for working with SST (Serverless Stack), covering infrastructure patterns, best practices, and common use cases.
Core Philosophy
SST embraces these principles:
- Type-safe infrastructure: Full TypeScript support from infrastructure to runtime
- Resource bindings: Type-safe access to resources via
Resource - Convention over configuration: Sensible defaults, customize when needed
- Developer experience: Fast feedback loops, excellent local development
Key Concepts
Resource Bindings
The most powerful SST feature - type-safe resource access:
// In sst.config.ts
const bucket = new sst.aws.Bucket("MyBucket");
const api = new sst.aws.Function("MyApi", {
handler: "src/api.handler",
link: [bucket] // Link the bucket
});
// In src/api.ts
import { Resource } from "sst";
export async function handler() {
// Type-safe access!
await s3.putObject({
Bucket: Resource.MyBucket.name,
// ...
});
}
Key points:
- Use
linkto connect resources - Access via
Resource.[ResourceName] - Full TypeScript autocomplete and type safety
- No environment variables needed
Infrastructure as Code
Define resources in sst.config.ts:
export default $config({
app(input) {
return {
name: "my-app",
removal: input?.stage === "production" ? "retain" : "remove",
};
},
async run() {
// Define your infrastructure
const bucket = new sst.aws.Bucket("Uploads");
const api = new sst.aws.Function("Api", {
handler: "src/api.handler",
link: [bucket],
url: true // Enable function URL
});
return {
api: api.url,
bucket: bucket.name
};
},
});
Common Patterns
Pattern 1: Function with Database Access
// sst.config.ts
const db = new sst.aws.Dynamo("Database", {
fields: {
pk: "string",
sk: "string"
},
primaryIndex: { hashKey: "pk", rangeKey: "sk" }
});
const handler = new sst.aws.Function("Handler", {
handler: "src/handler.main",
link: [db]
});
// src/handler.ts
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export async function main(event) {
await client.send(new PutCommand({
TableName: Resource.Database.name,
Item: { pk: "user", sk: "123", data: "..." }
}));
}
Pattern 2: Remix with SST
// sst.config.ts
const bucket = new sst.aws.Bucket("Uploads");
const remix = new sst.aws.Remix("MyApp", {
link: [bucket], // Link resources to Remix
domain: "app.example.com"
});
// In Remix loader/action
import { Resource } from "sst";
export async function loader() {
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: Resource.Uploads.name,
Key: "file.pdf"
}));
return { url };
}
Pattern 3: API with Auth
const auth = new sst.aws.Auth("Auth", {
authenticator: "src/auth.handler"
});
const api = new sst.aws.ApiGatewayV2("Api", {
link: [auth],
transform: {
route: {
handler: {
link: [auth]
}
}
}
});
api.route("GET /private", "src/private.handler", {
auth: { iam: true }
});
Pattern 4: Environment-Specific Configuration
export default $config({
app(input) {
return {
name: "my-app",
removal: input?.stage === "production" ? "retain" : "remove",
};
},
async run() {
const isProd = $app.stage === "production";
const db = new sst.aws.Dynamo("Database", {
// Production settings
...(isProd && {
transform: {
table: {
pointInTimeRecovery: { enabled: true }
}
}
})
});
return { stage: $app.stage };
}
});
Resource Types
Compute
Function: Lambda functions with great DXRemix: Remix applicationsNextjs: Next.js applicationsAstro: Astro applications
Storage
Bucket: S3 buckets with automatic policiesDynamo: DynamoDB tables with typed indexes
APIs
ApiGatewayV2: HTTP/WebSocket APIsRouter: Route handling
Auth & Security
Auth: Authentication setupSecret: Secure secret management
Queues & Events
Queue: SQS queuesSnsTopic: SNS topicsEventBus: EventBridge buses
Best Practices
1. Use Resource Bindings Over Environment Variables
❌ Don't:
const tableName = process.env.TABLE_NAME!;
✅ Do:
const tableName = Resource.Database.name;
2. Keep Infrastructure Simple
❌ Don't over-engineer:
// Don't create unnecessary layers
const commonConfig = createConfigBuilder()
.withDefaults()
.withRetries()
.build();
✅ Do keep it simple:
const fn = new sst.aws.Function("Handler", {
handler: "src/handler.main",
timeout: "30 seconds"
});
3. Link Resources Appropriately
Only link what you need:
// If a function only needs the bucket, only link the bucket
const fn = new sst.aws.Function("ProcessUpload", {
handler: "src/process.handler",
link: [bucket] // Not the entire database, auth, etc.
});
4. Use Transforms for AWS-Specific Needs
When you need direct AWS resource access:
new sst.aws.Bucket("Uploads", {
transform: {
bucket: {
lifecycleConfiguration: {
rules: [{
expiration: { days: 30 },
status: "Enabled"
}]
}
}
}
});
5. Type Your Handlers Properly
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
export async function handler(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello" })
};
}
6. Organize Large Stacks
// sst.config.ts
async run() {
const storage = await import("./infra/storage");
const api = await import("./infra/api");
const web = await import("./infra/web");
const { bucket, database } = await storage.setup();
const { apiUrl } = await api.setup({ bucket, database });
const { siteUrl } = await web.setup({ apiUrl });
return { apiUrl, siteUrl };
}
Local Development
Running Locally
# Start SST dev mode
sst dev
# In another terminal, run your app
npm run dev
Console Access
# Open SST Console for your stage
sst console
# Deploy to a specific stage
sst deploy --stage production
Testing Resources Locally
SST automatically sets up local versions:
// Works the same locally and deployed
import { Resource } from "sst";
const tableName = Resource.Database.name; // Points to local or deployed based on context
Common Gotchas
1. Resource Name Changes
Renaming resources can cause issues. Use explicit IDs:
// Better: use explicit ID
const bucket = new sst.aws.Bucket("Uploads", {
// Explicit physical name if needed
});
2. Circular Dependencies
Avoid circular links:
❌ // Don't
const fnA = new sst.aws.Function("A", {
link: [fnB]
});
const fnB = new sst.aws.Function("B", {
link: [fnA]
});
✅ // Do: use SNS/SQS or store state in DB
3. Cold Starts
Lambda cold starts are real. Optimize:
// Keep warm-up code outside handler
const client = new DynamoDBClient({});
export async function handler(event) {
// Handler uses pre-initialized client
}
Migration and Updates
Updating SST
npm update sst
# or
pnpm update sst
Breaking Changes
Always check the changelog when upgrading major versions. SST provides migration guides for breaking changes.
Further Reading
- Official SST Docs: https://sst.dev/docs
- SST Examples: https://github.com/sst/examples
- SST Discord: Great for questions and support
More from tejovanthn/rasikalife
marketing-copy
Proven copywriting frameworks and best practices for creating compelling marketing content across different channels
18conform
Progressive enhancement form validation with Conform and Zod for Remix applications - type-safe forms that work without JavaScript
17email-templates
Transactional email design using React Email and integration with email providers like Resend or AWS SES
15frontend-design
UI/UX design principles and patterns for building intuitive, accessible, and beautiful web interfaces
14electrodb
ElectroDB patterns for DynamoDB single-table design with full type safety and optimized access patterns
13remix-patterns
Best practices, patterns, and conventions for working with Remix - the web framework that embraces web fundamentals and progressive enhancement
13