gs-sst-infra
SST v3 Infrastructure
Project Structure
project/
├── sst.config.ts # Main SST config
├── stacks/
│ ├── dynamodb.ts # Database stack
│ ├── nextjs.ts # Next.js deployment
│ └── api.ts # Lambda API stack
├── open-next.config.ts # Lambda streaming config
└── src/
└── backend/ # Clean Architecture backend
Main Config
// sst.config.ts
export default $config({
app(input) {
return {
name: 'my-app',
removal: input?.stage === 'prod' ? 'retain' : 'remove',
protect: ['prod'].includes(input?.stage ?? ''),
home: 'aws',
providers: { aws: { region: 'us-east-1' } },
}
},
async run() {
const { table } = await import('./stacks/dynamodb')
const { site } = await import('./stacks/nextjs')
return { url: site.url, tableName: table.name }
},
})
DynamoDB Stack
// stacks/dynamodb.ts
export const table = new sst.aws.Dynamo('Table', {
fields: {
pk: 'string', sk: 'string',
gsi1pk: 'string', gsi1sk: 'string',
},
primaryIndex: { hashKey: 'pk', rangeKey: 'sk' },
globalIndexes: {
gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },
},
transform: {
table: (args) => { args.billingMode = 'PAY_PER_REQUEST' },
},
})
Next.js Stack
// stacks/nextjs.ts
import { table } from './dynamodb'
export const site = new sst.aws.Nextjs('Site', {
path: 'apps/web',
link: [table],
environment: { TABLE_NAME: table.name },
domain: {
name: `${$app.stage === 'prod' ? '' : `${$app.stage}.`}myapp.com`,
dns: sst.aws.dns({ zone: 'myapp.com' }),
},
})
Lambda Handler with Clean Architecture
Lambda handlers follow the thin adapter pattern - resolve Use Case from DI and execute.
// src/functions/create-category.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda'
import { DIContainer, TOKENS, initializeDI } from '@/backend/di'
import { CreateCategoryUseCase } from '@/backend/application/category/use-cases'
import { DomainException } from '@/backend/domain/shared/exceptions'
// Initialize DI once per cold start
let initialized = false
export async function handler(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
// Initialize DI Container (cold start only)
if (!initialized) {
await initializeDI()
initialized = true
}
try {
const input = JSON.parse(event.body ?? '{}')
// Thin adapter: resolve → execute → return
const useCase = DIContainer.resolve<CreateCategoryUseCase>(
TOKENS.CreateCategoryUseCase
)
const result = await useCase.execute(input)
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
}
} catch (error) {
return mapErrorToResponse(error)
}
}
function mapErrorToResponse(error: unknown): APIGatewayProxyResultV2 {
if (error instanceof DomainException) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: error.message, code: error.code }),
}
}
console.error('Unhandled error:', error)
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Internal server error' }),
}
}
Lambda API Stack
// stacks/api.ts
import { table } from './dynamodb'
const api = new sst.aws.ApiGatewayV2('Api')
api.route('POST /categories', {
handler: 'src/functions/create-category.handler',
link: [table],
environment: { TABLE_NAME: table.name },
})
api.route('GET /categories/{id}', {
handler: 'src/functions/get-category.handler',
link: [table],
environment: { TABLE_NAME: table.name },
})
export { api }
OpenNext Config
// open-next.config.ts
import type { OpenNextConfig } from 'open-next/types/open-next'
const config: OpenNextConfig = {
default: {
override: { wrapper: 'aws-lambda-streaming' },
},
}
export default config
Commands
# Development
npx sst dev --stage dev
# Deploy
npx sst deploy --stage dev
npx sst deploy --stage prod
# Secrets
npx sst secret set AUTH_SECRET "value" --stage dev
npx sst secret list --stage dev
# Outputs
npx sst outputs --stage dev
Environment Stages
| Stage | Domain | Protection | Removal |
|---|---|---|---|
| dev | dev.app.com | No | Remove |
| test | test.app.com | No | Remove |
| prod | app.com | Yes | Retain |
CI/CD (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
inputs:
stage:
type: choice
options: [dev, test, prod]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- run: pnpm install --frozen-lockfile
- run: npx sst deploy --stage ${{ inputs.stage }}
Local Development
# DynamoDB Local
docker run -p 8000:8000 amazon/dynamodb-local
# Environment
TABLE_NAME=dev-Table
DYNAMODB_LOCAL=true
References
- Feature Architecture:
skills/feature-architecture/SKILL.md - Bun Lambda:
skills/bun-aws-lambda/SKILL.md
More from gilbertopsantosjr/fullstacknextjs
tanstack-react-query
TanStack React Query expert for data fetching and mutations in React applications. Use when working with useQuery, useMutation, cache invalidation, optimistic updates, query keys, or integrating server actions with React Query via @saas4dev/core hooks (useServerActionQuery, useServerActionMutation, useServerActionInfiniteQuery). Triggers on requests involving API data fetching, server state management, cache strategies, or converting fetch/useEffect patterns to React Query.
4gs-feature-architecture
Guide for implementing features in Clean Architecture OOP with Next.js. Use when planning new features, understanding the 4-layer structure (Domain, Application, Infrastructure, Presentation), or deciding where code should live.
3sst-infra
Guide for AWS serverless infrastructure using SST v3 (Serverless Stack). Use when configuring deployment, creating stacks, managing secrets, setting up CI/CD, or deploying Next.js applications to AWS Lambda with DynamoDB.
2zod-validation
Guide for Zod schema validation patterns in TypeScript. Use when creating validation schemas, defining types, validating forms, API inputs, or handling validation errors.
2feature-architecture
Guide for implementing features in a layered Next.js full-stack architecture. Use when planning new features, understanding the layer structure (Model, DAL, Service, Actions, Components, Pages), or deciding where code should live.
2fullstacknextjs
Collection of skills for full-stack Next.js development with Clean Architecture and AWS serverless. Backend uses Entities, Use Cases, Repository pattern, and DI Container. Frontend uses thin server action adapters.
1