home / skills / gilbertopsantosjr / fullstacknextjs / gs-sst-infra

gs-sst-infra skill

/skills/gs-sst-infra

This skill guides AWS serverless infrastructure with SST v3, covering DynamoDB, Next.js deployment, clean-architecture Lambda handlers, and CI/CD configuration.

npx playbooks add skill gilbertopsantosjr/fullstacknextjs --skill gs-sst-infra

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.7 KB
---
name: gs-sst-infra
description: Guide for AWS serverless infrastructure using SST v3. Covers DynamoDB, Next.js deployment, Lambda handlers with Clean Architecture adapter pattern, and CI/CD configuration.
---

# 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

```typescript
// 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

```typescript
// 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

```typescript
// 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.

```typescript
// 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

```typescript
// 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

```typescript
// 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

```bash
# 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)

```yaml
# .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

```bash
# 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`

Overview

This skill is a practical guide for building AWS serverless infrastructure with SST v3. It covers a production-ready structure: DynamoDB configuration, Next.js deployment with OpenNext streaming, Lambda handlers using a thin adapter Clean Architecture pattern, and CI/CD via GitHub Actions. The content focuses on clear patterns, environment stages, and local development tips.

How this skill works

The guide defines stacks for DynamoDB, Next.js, and API routes in SST configuration, exposing outputs like site URL and table name. Lambda handlers are implemented as thin adapters that initialize a DI container on cold start, resolve use cases, and map domain exceptions to HTTP responses. Next.js is deployed with streaming Lambda wrappers, and CI/CD uses a GitHub Actions workflow that assumes an AWS role and runs npx sst deploy.

When to use it

  • Building a serverless web app with Next.js and AWS Lambda streaming
  • Needing a single DynamoDB table with primary and global secondary indexes
  • Implementing Clean Architecture with DI and thin Lambda adapters
  • Setting up stage-aware domains and environment-specific protection
  • Automating deployments with GitHub Actions and SST

Best practices

  • Use a single DynamoDB table with thoughtfully designed PK/GSI keys to minimize costs and simplify queries
  • Keep Lambda handlers thin: resolve use case from DI, execute, and map errors to responses
  • Initialize dependency injection once per cold start to reduce runtime overhead
  • Set billingMode to PAY_PER_REQUEST for bursty serverless workloads
  • Protect and retain prod resources while allowing dev/test stages to be removed

Example use cases

  • Public Next.js storefront with server-side rendering and streamed responses using Lambda streaming
  • CRUD APIs backed by a single DynamoDB table and organized by clean use-case modules
  • Multi-stage deployments (dev/test/prod) with stage-specific domains and resource protection
  • Local development using DynamoDB Local and environment flags for faster iteration
  • CI-driven deployments that assume an AWS role and run SST deploy in GitHub Actions

FAQ

How are environment-specific domains configured?

The Next.js stack sets the domain dynamically based on stage, prefixing non-prod stages and resolving DNS through a provided zone.

How do Lambdas handle domain errors and domain exceptions?

Handlers catch DomainException instances and return 400 responses with a code; other errors log and return a 500 Internal Server Error.