DEV Community

Cover image for How to Build a Simple API Service with SST v3: A Step-by-Step Guide
Martin Patino
Martin Patino

Posted on • Edited on • Originally published at martinpatino.com

3

How to Build a Simple API Service with SST v3: A Step-by-Step Guide

Six months ago, I was deep in the trenches of SST v2, wrestling with its CloudFormation-based architecture and dealing with long deployment times. I wish I had known what I know now about SST v3 (codename SST Ion)—it would have saved me hours of frustration.

So, if my past self (or anyone else in the same boat) is reading this, here’s everything you need to know about why SST v3 is a game-changer and how it can make your life easier.

SST v2 vs. SST v3: What Changed?

SST v2 was built on AWS CDK and CloudFormation, meaning your entire infrastructure was defined within the SST ecosystem and compiled into CloudFormation templates. This came with limitations:

Slower deployments due to CloudFormation stacks

Vendor lock-in with AWS services

Complex stack management

Then came SST v3, which introduced Pulumi and Terraform, eliminating CloudFormation stacks. The result?

🚀 Faster deployments—no more waiting on CloudFormation!

🌎 Multi-cloud support—Cloudflare, AWS, and more

🛠️ Custom components—Use Pulumi to extend SST however you like

This shift removes vendor lock-in and gives you more flexibility than ever.

Why SST v3 Makes Microservices Easier

Imagine you have several microservices for your application, each requiring its own resources like S3 buckets, DynamoDB tables, or Lambda functions. With SST v3, you define all those resources in code, making it easy to add new services, remove outdated ones, and scale dynamically.

Key Benefits for Microservices Architecture

✔️ Infrastructure as Code (IaC) – Define all cloud resources programmatically
✔️ No more YAML – No need to manually configure CloudFormation templates
✔️ Faster scaling – Add/remove services with confidence
✔️ Less time spent debugging deployments

Now, instead of fighting YAML files, you’ll spend more time actually building features.

Let’s Start Building: Serverless API with SST

In this tutorial, we’ll build a serverless API that does the following:

Stores JSON data in DynamoDB when triggered

Uses a DynamoDB stream to process new entries

Sends an email notification based on the stored data

This demo will show how easy it is to integrate multiple AWS services using SST v3 and get your serverless application running efficiently.

Step 1: Set Up Your Project

First, create a new project directory and initialize it:

mkdir email-buddy-tracker && cd email-buddy-tracker 
bun init -y
Enter fullscreen mode Exit fullscreen mode

Now, initialize SST in your directory by running:

npx sst@latest init
Enter fullscreen mode Exit fullscreen mode

SST Initialization Prompts

You’ll be prompted with a few options:

Use SST templates? → Select default

Choose a provider? → Pick AWS for this demo (feel free to experiment with others later)

After the setup, you’ll see a new file created sst.config.ts

This file acts as the root configuration for your SST project.

export default $config({
  app(input) {
    return {
      name: "email-buddy-tracker",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
      // Define your infrastructure and logic here
  },
});

Enter fullscreen mode Exit fullscreen mode

Step 2: Add AWS Resources

To support our API, we’ll need to create a DynamoDB table, an email component, and an HTTP API using SST.

First, install the required dependencies:

bun add electrodb hono @aws-sdk/client-sesv2
Enter fullscreen mode Exit fullscreen mode
  1. ElectroDB: Simplifies single-table design patterns for DynamoDB.
  2. Hono: A lightweight, high-performance web framework for building your Lambda-based API.
  3. AWS SDK: Interact with AWS services (SES, DynamoDB, etc.).

Update sst.config.ts to Define Resources

Modify your sst.config.ts to create necessary AWS components:

export default $config({
  app(input) {
    return {
      name: "email-buddy-tracker",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
    // Define AWS resources
    const userRecordsTable = new sst.aws.Dynamo("UserRecords", {
      fields: {
        pk: "string",
        sk: "string"
      },
      primaryIndex: { hashKey: "pk", rangeKey: "sk" }
    });

    const email = new sst.aws.Email("MyEmail", {
      sender: "martin@gizmodlabs.com",
    });

    new sst.aws.Function("HonoApi", {
      url: true, // Enables public URL for API access
      handler: "src/index.handler",
      link: [userRecordsTable, email],
    });
  },
});

Enter fullscreen mode Exit fullscreen mode

Step 3: Create the API Handler

Now, let’s create the root API handler using Hono.js, a lightweight and high-performance web framework.

Create a src directory and add an index.ts file:

import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export const handler = handle(app)
Enter fullscreen mode Exit fullscreen mode

With this setup, your API is ready to go!

With that setup you should be able to run sst dev or add bun run dev .

You should see something like this below

Image description

Step 4: Define a User Entity & a Dynamic Email Sender

Building scalable and efficient serverless applications requires a robust data model and a reliable email system. In this step, we'll leverage ElectroDB to define a User entity and use AWS SES for sending emails dynamically. This setup will ensure flexibility, maintainability, and scalability incase you want to take this further.

Defining the User Entity with ElectroDB

Instead of directly using the AWS SDK for DynamoDB, ElectroDB simplifies working with a single-table design. We’ll use SST resources to reference the table dynamically, eliminating hardcoded resource names and ensuring seamless deployment across different environments.

User Entity Code Implementation

import DynamoDB from "aws-sdk/clients/dynamodb";
import { Entity } from "electrodb";
import { Resource } from "sst"; // Dynamically reference the SST resource

const client = new DynamoDB.DocumentClient();

export const UserRecord = new Entity(
  {
    model: {
      entity: "UserRecords",
      version: "1",
      service: "user",
    },
    attributes: {
      email: { type: "string" },
      firstName: { type: "string" },
      lastName: { type: "string", required: true },
      createdAt: { type: "string", default: () => new Date().toISOString() },
    },
    indexes: {},
  },
  { client, table: Resource.UserRecords.name }
);
Enter fullscreen mode Exit fullscreen mode

Configuring a Dynamic Email Sender

To avoid a static "from" email address, we'll use SST resources to reference it dynamically. Before sending emails, ensure that your domain or email is verified in AWS SES, or you’ll get stuck in the dreaded SES sandbox.

Email Sending Function

import { Resource } from "sst";
import { SESv2Client, SendEmailCommand, type SendEmailCommandOutput } from "@aws-sdk/client-sesv2";

const client = new SESv2Client();

export const sendEmail = (email: string, body: string): Promise<SendEmailCommandOutput> => {
    return client.send(
        new SendEmailCommand({
            FromEmailAddress: Resource.MyEmail.sender, // Dynamically get sender email
            Destination: { ToAddresses: [email] },
            Content: {
                Simple: {
                    Subject: { Data: "Hello from SST" },
                    Body: { Text: { Data: body } }
                }
            }
        })
    );
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Making Our API Functional

Now, let's wire everything together! Our API will handle POST requests, create a UserRecord in DynamoDB, and send a welcome email.

API Handler for User Creation & Email Sending

import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'
import { UserRecord } from './core/entity/user'
import { sendEmail } from './core/sendEmail'

const app = new Hono()

app.post('/', async (ctx) => {
    const body = await ctx.req.json();

    // Create a new user record in DynamoDB
    const record = await UserRecord.create({
        email: body.email,
        firstName: body.firstName,
        lastName: body.lastName,
        createdAt: new Date().toISOString()
    });

    // Send a welcome email
    const sentEmail = await sendEmail(body.email, "Welcome to SST and following my blog around SST!");

    return ctx.json({
        emailMessageId: sentEmail.MessageId || null,
        dbRecord: record
    });
});

export const handler = handle(app);
Enter fullscreen mode Exit fullscreen mode

Testing the API

Run the service with:

bun run dev
# or
sst dev
Enter fullscreen mode Exit fullscreen mode

Now, use Postman, Insomnia, or cURL to send a POST request to http://localhost:3000 with a sample payload:

{
  "email": "martin@gizmodlabs.com",
  "firstName": "John",
  "lastName": "Doe"
}
Enter fullscreen mode Exit fullscreen mode

Image description

If everything works correctly, you should receive:
HTTP 200 Response

SES MessageId confirming the email was sent

New DynamoDB User Record

You can verify by checking your inbox and the DynamoDB console in AWS.

Final Thoughts: Is It Worth It?

Yes! By using SST, ElectroDB, and AWS SES, you gain:

  • Scalability: Effortlessly handle more users without reworking the architecture.
  • Maintainability: Dynamic references prevent hardcoded resource names, reducing errors with SST resources.
  • Developer Experience: Faster iteration cycles with SST v3—no more CloudFormation headaches.

So, go ahead—build microservices, serverless apps, and AI-driven tools without the frustration of traditional cloud setups. And remember: if slow deployments make you cry, switch to SST v3—your sanity will thank you.

Example Repo: https://github.com/thisguymartin/email-buddy-tracker

Keep a eye out for more serverless content.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post