Industry Insights 5 Building Full Stack Applications with SST

Industry Insights

sonatafy-glyph

Building Full Stack Applications with SST

by | Dec 9, 2024 | All, Programming, Software Development

About The Author Alejandro Ramírez

Alejandro Ramirez is a Director of Engineering with over 20 years of experience in Software Development, across a diverse range of industries and applications.

Infrastructure as Code (IaC) has become a critical practice for managing and provisioning cloud resources efficiently. For full stack applications, which involve both frontend and backend components, managing the underlying infrastructure can be complex. Enter SST (Serverless Stack Toolkit)—a powerful framework that simplifies the process by allowing you to define and manage your infrastructure entirely in code.

In this blog post, we’ll explore how to use SST for IaC in full stack applications, walking through the setup, development, and deployment processes. By the end, you’ll have a solid understanding of how SST can streamline your workflow and enhance your application’s scalability and maintainability.

Table of Contents

  • What is SST?
  • Why Use SST for Full Stack Applications?
  • Getting Started with SST
  • Defining Infrastructure with SST
  • Building a Full Stack Application
  • Deploying Your Application
  • Best Practices
  • Conclusion

What is SST?

SST (Serverless Stack Toolkit) is an open-source framework that leverages the AWS Cloud Development Kit (CDK) and extends its capabilities to simplify the development of serverless applications on AWS. SST allows you to define your cloud infrastructure using TypeScript, providing a type-safe, code-first approach to infrastructure management.

Key Features:

  • TypeScript-Based IaC: Use TypeScript to define AWS resources, benefiting from type safety and IntelliSense.
  • Live Lambda Development: Test and debug Lambda functions locally with real-time feedback.
  • High-Level Constructs: Simplify resource definitions with SST’s higher-level abstractions.
  • Multi-Environment Support: Manage different deployment stages seamlessly.

Why Use SST for Full Stack Applications?

Building full stack applications involves orchestrating various AWS services like Lambda, API Gateway, DynamoDB, Cognito, and S3. Managing these resources manually or through disparate configurations can be error-prone and time-consuming.

Benefits of Using SST:

  • Unified Codebase: Manage both frontend and backend infrastructure in a single codebase.
  • Simplified Resource Management: High-level constructs reduce boilerplate and complexity.
  • Faster Development Cycle: Live Lambda development accelerates testing and debugging.
  • Scalability: Easily scale your application by adjusting code, not configurations.
  • Consistency Across Environments: Ensure that development, staging, and production environments are consistent.

Getting Started with SST

Prerequisites

  • Node.js: Install the latest LTS version from the official website.
  • AWS Account: Create an AWS account with appropriate permissions.
  • AWS CLI: Install and configure the AWS Command Line Interface.

Installation

Install the SST CLI globally:
npm install --location=global sst

Initializing a New Project

Create a new SST project:
mkdir my-fullstack-app
cd my-fullstack-app
npx sst init --template=typescript-starter
This command initializes a new SST project using the TypeScript starter template.

Defining Infrastructure with SST

SST uses stacks to define your infrastructure. Each stack is a collection of AWS resources that you can manage as a single unit.


Project Structure

Your SST project will have the following structure:
  • stacks/: Contains your infrastructure code.
  • packages/: Contains your application code (Lambda functions, frontend app).
  • sst.config.ts: The main configuration file for SST.

Defining a Backend API

Let’s create an API using API Gateway and Lambda.
stacks/MyStack.ts
import * as sst from "@serverless-stack/resources";

export default class MyStack extends sst.Stack {
  constructor(scope: sst.App, id: string, props?: sst.StackProps) {
    super(scope, id, props);

    // Create a HTTP API
    const api = new sst.Api(this, "Api", {
      routes: {
        "GET /": "packages/functions/src/lambda.handler",
      },
    });

    // Show the API endpoint in the output
    this.addOutputs({
      ApiEndpoint: api.url,
    });
  }
}

Creating a Lambda Function

packages/functions/src/lambda.ts
export async function handler(event: any) {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Hello from Lambda!" }),
  };
}

Setting Up a Database

Let’s add a DynamoDB table to store data.
stacks/MyStack.ts
// ...

// Create a DynamoDB table
const table = new sst.Table(this, "Table", {
  fields: {
    pk: sst.TableFieldType.STRING,
  },
  primaryIndex: { partitionKey: "pk" },
});

// Grant the Lambda function read/write permissions to the table
api.attachPermissions([table]);

Adding Authentication

Set up user authentication using Amazon Cognito.
stacks/MyStack.ts
// ...

// Create a Cognito User Pool
const auth = new sst.Auth(this, "Auth", {
  cognito: {
    userPool: {
      signInAliases: { email: true },
    },
  },
});

// Allow authenticated users to invoke the API
auth.attachPermissionsForAuthUsers([api]);

// Export the auth resources
this.addOutputs({
  UserPoolId: auth.cognitoUserPool?.userPoolId || "",
  UserPoolClientId: auth.cognitoUserPoolClient?.userPoolClientId || "",
});

Hosting a Frontend Application

Assuming you have a React application in packages/frontend, you can deploy it using SST’s StaticSite construct.
stacks/MyStack.ts
// ...

// Deploy the React app
const site = new sst.StaticSite(this, "ReactSite", {
  path: "packages/frontend",
  buildCommand: "npm run build",
  buildOutput: "build",
});

// Output the URL of the deployed site
this.addOutputs({
  SiteUrl: site.url,
});

Building a Full Stack Application

Let’s tie everything together by building a simple note-taking application.

Setting Up the Backend

  • Create API routes: Define RESTful endpoints for CRUD operations.
  • Implement Lambda functions: Write functions to handle requests and interact with DynamoDB.
stacks/MyStack.ts
// Define API routes
const api = new sst.Api(this, "Api", {
  routes: {
    "GET    /notes": "packages/functions/src/listNotes.main",
    "POST   /notes": "packages/functions/src/createNote.main",
    "GET    /notes/{id}": "packages/functions/src/getNote.main",
    "PUT    /notes/{id}": "packages/functions/src/updateNote.main",
    "DELETE /notes/{id}": "packages/functions/src/deleteNote.main",
  },
});

// Attach permissions
api.attachPermissions([table]);

Example Lambda Function

packages/functions/src/createNote.ts
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();

export async function main(event: any) {
  const data = JSON.parse(event.body);
  const params = {
    TableName: process.env.TABLE_NAME!,
    Item: {
      pk: data.userId,
      noteId: data.noteId,
      content: data.content,
      createdAt: Date.now(),
    },
  };

  try {
    await dynamoDb.put(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Could not create note" }),
    };
  }
}

Setting Up the Frontend

  • Implement UI Components: Build forms and lists to interact with your API.
  • Configure Authentication: Integrate Cognito for user sign-up and sign-in.
  • Connect to API: Use the API endpoint and authentication tokens to make secure requests.
Example Configuration
// src/config.js
export default {
  apiGateway: {
    REGION: "your-region",
    URL: "your-api-endpoint",
  },
  cognito: {
    REGION: "your-region",
    USER_POOL_ID: "your-user-pool-id",
    APP_CLIENT_ID: "your-app-client-id",
  },
};

Updating the StaticSite Construct

Pass environment variables to your frontend:
stacks/MyStack.ts
const site = new sst.StaticSite(this, "ReactSite", {
  path: "packages/frontend",
  buildCommand: "npm run build",
  buildOutput: "build",
  environment: {
    REACT_APP_API_URL: api.url,
    REACT_APP_REGION: scope.region,
    REACT_APP_USER_POOL_ID: auth.cognitoUserPool?.userPoolId || "",
    REACT_APP_USER_POOL_CLIENT_ID:
      auth.cognitoUserPoolClient?.userPoolClientId || "",
  },
});

Deploying Your Application

Local Development

Run the SST development server:
sst dev
This command:
  • Deploys your stack to a local AWS environment.
  • Watches for code changes and updates resources in real-time.
  • Provides a local endpoint for testing.

Production Deployment

Deploy your application to AWS:
sst deploy --stage prod
Replace prod with the desired stage name.

Best Practices

Organize Your Code

  • Modularize: Split your stacks and functions into logical units.
  • Reuse Components: Create reusable constructs for common patterns.

Use Environment Variables

  • Configuration Management: Use environment variables to manage configurations across different environments.

Implement Testing

  • Unit Tests: Write tests for your Lambda functions.
  • Integration Tests: Test interactions between services.

Leverage SST Features

  • Permissions Management: Use attachPermissions to manage IAM permissions effectively.
  • Monitoring: Utilize SST’s integration with AWS CloudWatch for logging and monitoring.

Conclusion

Using SST for Infrastructure as Code in full stack applications offers a seamless and efficient development experience. By unifying your application and infrastructure code, SST not only simplifies resource management but also accelerates the development cycle with features like live Lambda debugging.

Key Takeaways:

  • Simplified Infrastructure Management: High-level constructs make defining AWS resources straightforward.
  • Accelerated Development: Live Lambda development reduces the feedback loop.
  • Scalability and Maintainability: Code-first approach ensures your application can grow and adapt over time.
  • Consistent Environments: Manage multiple stages with ease, ensuring consistency across deployments.
Dive deeper by exploring the official SST documentation and start building scalable, serverless full stack applications today.