Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

Build a full-stack serverless app with SST

7 min read 2011 107

Build Full-Stack Serverless App SST

Serverless computing has revolutionized the way we build and deploy modern web applications. With its ability to scale dynamically, offer high availability, and drastically reduce infrastructure costs, serverless computing has become the go-to solution for many developers looking to build highly scalable and reliable applications.

However, serverless applications can be challenging to build, especially when managing infrastructure and integrating different services. This is where serverless stack (SST) comes into play. SST is an open source framework that makes it easy to build modern, full-stack applications on AWS using serverless architecture.

In this article, we’ll walk through the process of building a full-stack serverless application with SST by creating a simple contact list application using React, Lambda functions, and DynamoDB.

Jump ahead:

What is SST?

SST is an open source framework for building and deploying serverless applications on Amazon Web Services. It is designed to be easy-to-use and flexible, supporting multiple programming languages and frameworks, including Node.js, Python, Java, and React.

SST provides tools and libraries that simplify the development process and enable us to build scalable and cost-effective serverless applications using AWS services like AWS Lambda, Amazon API Gateway, and Amazon S3. It includes a set of pre-built templates and examples, that can be used as a starting point for projects, and a robust set of testing and debugging tools for ensuring the quality and reliability of code.

One of the key benefits of using SST is its focus on serverless architecture, which enables developers to concentrate on writing code without worrying about infrastructure management. SST automatically provisions and configures the necessary AWS resources based on the application’s requirements; this saves time and reduces the chance of errors.

Getting started with SST

SST uses AWS credentials to run and deploy apps. So, to proceed with this guide, you’ll need to configure the AWS CLI.

To create and set up the SST app, run the following commands:

> npx [email protected] contacts
> cd contacts
> npm install

Now, let’s understand the breakdown of an SST app’s project layout. Here’s infomration on the SST app’s main directories:

  • stacks/: Contains the code that defines the infrastructure of the serverless app using the AWS CDK; we can create multiple stacks within this directory, each with its resources and dependencies
  • packages/functions/: Contains the Lambda functions’ code which is executed when the API is called; we can create multiple functions within this directory, each with its own dependencies and configuration
  • packages/core/: Contains the serverless app’s business logic and application code; this includes any modules, libraries, or utilities our functions rely on to perform their tasks

SST makes managing and scaling serverless apps easy by separating the infrastructure and application code into separate directories. We can update the infrastructure and code independently, making it easier to deploy changes and fix issues in our app. Separating the code into multiple packages makes it easier to manage dependencies and improve code organization.

To start the SST dev environment, run the following command:

> npx sst dev

The SST CLI will ask us for the name of the stage. A stage or an environment is a string used to namespace our deployments. We can use stages to deploy multiple app versions to different environments, such as development, staging, and production.

For simplicity, add your name as the stage name for local development. It will take around two to five minutes to deploy the app for the first time.

Once the deployment is complete, copy the API endpoint printed on the terminal console and open it in the browser to verify that the deployment was successful:

SST App Successful Setup

Understanding serverless stack capabilities

SST offers many benefits over traditional server-centric development:

  • Defines infrastructure using the AWS Cloud Development Kit (CDK): CDK allows us to express the infrastructure of our serverless app using code, making it easy to manage, version control, and automate
  • Enables testing app in real time using Live Lambda Development: This allows us to see how our application performs, make changes, and test again without needing to redeploy
  • Enables setting breakpoints and debugging in real time: SST integrates with VS Code, enabling us to quickly identify and fix issues in our code
  • Provides web-based app management dashboard: The dashboard may be used to view metrics, logs, and other important information about our application
  • Deploys to multiple environments and regions: SST allows for easy deployment to different environments and regions, making it easy to manage our serverless application in different environments, such as development, testing, and production
  • Provides higher-level constructs explicitly designed for serverless apps: These constructs abstract away the underlying infrastructure, making it easier to develop, test, and deploy serverless applications
  • Configures Lambda functions: SST supports multiple programming languages, including JavaScript, TypeScript (using esbuild), Go, Python, C#, and F#, making it easy to develop serverless applications using our preferred language. SST also has support for AWS resources like DynamoDB, Cognito, S3, etc.

Creating the DynamoDB table

Now that the deployment is verified and the app is up and running, let’s delete the starter files so we can start from scratch. Next, run the following commands to remove the template files:

> npx sst remove API
> rm stacks/MyStack.ts packages/core/src/time.ts packages/functions/src/lambda.ts

Create a StorageStack inside the stacks folder to create and manage the DynamoDB resource:

import { StackContext, Table } from "sst/constructs";

export function StorageStack({ stack, app }: StackContext) {
  const table = new Table(stack, "Contacts", {
    fields: {
      id: "string",  
      name: "string",
      email: "string",
    },
    primaryIndex: { partitionKey: "id", sortKey: "email" },
  });

  return {
    table,
  };
}

In SST we can use the Table construct to create a DynamoDB table, and define the table’s primary key and any secondary indexes. In tha bove code, we create the Contacts table with three fields: id, name, and email. The table is then exported from the file to be referenced in other resources.

Next, add the StorageStack in the sst.config file:

import { SSTConfig } from "sst";
import { StorageStack } from "./stacks/StorageStack";

export default {
  config(_input) {
    return {
      name: "contacts",
      region: "ap-south-1",
    };
  },
  stacks(app) {
    app.stack(StorageStack)
  }
} satisfies SSTConfig;

Now, run the npx sst dev command again to deploy the changes. Once the deployment is complete, check the DynamoDB dashboard in the AWS console to verify the changes:

DynamoDB Table Dashboard

Building the backend serverless APIs

Let’s start with serverless functions by building an API to create a contact object in DynamoDB.

Create and configure the ApiStack in the stacks folder with the following content:

import { Api, use, StackContext } from "sst/constructs";
import { StorageStack } from "./StorageStack";

export function ApiStack({ stack, app }: StackContext) {
  const { table } = use(StorageStack);

  // Create the API
  const api = new Api(stack, "Api", {
    defaults: {
      function: {
        bind: [table],
      },
    },
    routes: {
      "POST /contacts": "packages/functions/src/create.main",
    },
  });

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

  // Return the API resource
  return {
    api,
  };
}

Here, the Api construct is used to create an API. The ApiStack function uses the table object referencing the DynamoDB table created in the StorageStack.



We can use the bind property to bind the table with the API, enabling the API to access the table. The routes object maps the route path and the corresponding function that will be called when API is invoked.

Now, let’s build the Lambda function to create an entry in DynamoDB. First, run the following command to install the dependencies required by the serverless function:

npm i aws-lambda uuid 

Here’s the content of the functions file:

// packages/functions/src/create.ts
import * as uuid from 'uuid';
import AWS from "aws-sdk";
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const dynamoDb = new AWS.DynamoDB.DocumentClient();

export const main: APIGatewayProxyHandlerV2 = async (event) => {

  const data = JSON.parse(event?.body || '');

  const params = {
    TableName: Table.Contacts.tableName,
    Item: {
        id: uuid.v4(),
        email: data.email, 
        name: data.name , 
        createdAt: Date.now(), 
    },
  };

  await dynamoDb.put(params).promise();

  return {
      statusCode: 200,
      body: JSON.stringify(params.Item),
  };

}

In the above code, the request body is accessed using the event.body object. We can access the name of the table using the SST Table construct, Table.Contacts.tableName.

Now, add the routes to get the contacts list as well as the delete API that will enable the removal of contact entries from the DynamoDB table:

routes: {
  "POST /contacts": "packages/functions/src/create.main",
  "GET /contacts": "packages/functions/src/list.main",
  "DELETE /contacts/{id}": "packages/functions/src/delete.main"
},

Here’s the code for the list and delete handlers:

// packages/functions/src/list.ts

import AWS from "aws-sdk";
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const dynamoDb = new AWS.DynamoDB.DocumentClient();

export const main: APIGatewayProxyHandlerV2 = async (event) => {
  const params = {
    TableName: Table.Contacts.tableName,
  };
  const results = await dynamoDb.scan(params).promise();

  return {
    statusCode: 200,
    body: JSON.stringify(results.Items),
  };
}


// packages/functions/src/delete.ts

import AWS from "aws-sdk";
import { Table } from "sst/node/table";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const dynamoDb = new AWS.DynamoDB.DocumentClient();

export const main: APIGatewayProxyHandlerV2 = async (event) => {

  const data = JSON.parse(event?.body || '');

  const params = {
    TableName: Table.Contacts.tableName,
    Key: {
        id: event?.pathParameters?.id,
        email: data?.email
    }
  };
  await dynamoDb.delete(params).promise();

  return {
    statusCode: 200,
    body: JSON.stringify({status: true}),
  };
}

Setting up the React frontend

With our backend API and infrastructure deployed, we’re ready to focus on our application’s frontend. We’ll build a web app that communicates with our backend to provide a complete end-to-end solution.

From the packages folder, run the following command to create a React app and install the required dependencies:

> npx create-react-app web-app
> cd web-app
> npm install sst --save-dev

We want to load the environment variables from our backend server. To do this, we’ll use the sst package within the React app. It will detect and retrieve the environment variables from our SST app and integrate them into the React development environment during initialization.

In the package.json file, replace the start script with the following to bind the env variables:

"start": "sst env react-scripts start"

We’ll use the StaticSite construct to deploy the React app. In the stacks folder, create a new stack, WebAppStack, and add the following content:

// stacks/WebAppStack.ts

import { StaticSite, use, StackContext } from "sst/constructs";
import { ApiStack } from "./ApiStack";

export function WebAppStack({ stack, app }: StackContext) {
  const { api } = use(ApiStack);

  // Define our React app
  const site = new StaticSite(stack, "ReactSite", {
    path: "packages/web-app",
    buildOutput: "build",
    buildCommand: "npm run build",
    environment: {
      REACT_APP_API_URL: api.url
    },
  });

  // Show the url in the output
  stack.addOutputs({
    SiteUrl: site.url || "http://localhost:3000",
  });
}

Here, we configure the StaticSite construct to reference the packages/web-app/ directory that houses our React application. Then, we transmit the outputs from the ApiStack as environment variables in React.

This approach ensures we cannot hardcode these variables into our React app. Instead of manually specifying the values of these variables within the React code, we pass them as environment variables during the build or deployment process. These environment variables are then made accessible within the React app at runtime.

In the deployment process, the api.url could be replaced with the actual API endpoint URL corresponding to the deployed environment. This way, the React app can be seamlessly connected to the appropriate API without the need to modify the source code.

Next, add the WebAppStack in the sst.config file:

// sst.config.ts

...
stacks(app) {
  app.stack(StorageStack).stack(ApiStack).stack(WebAppStack);
}
...

Now, start the React app by running the following command from the web-app directory:

> npm run start

Once the frontend server is up and running, open your browser and check out the results:

Full-Stack React App SST

Adding Name Contact List Full-Stack Serverless App SST

Conclusion

Whether you’re building a simple web application or a complex enterprise solution, SST can help streamline the development and deployment process, while taking full advantage of the power and scalability of the AWS cloud. By leveraging SST’s inbuilt constructs and development tools, you can focus on building great applications that solve real-world problems, rather than worrying about the underlying infrastructure.

The complete code from this article is available on GitHub.

Get setup with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

Leave a Reply