Skip to content

Components

Components are the building blocks of your app.

Every SST app is made up of components. These are logical units that represent features in your app; like your frontends, APIs, databases, or queues.

There are two types of components in SST:

  1. Built-in components — High level components built by the SST team
  2. Provider components — Low level components from the providers

Let’s look at them below.


Background

Most providers like AWS are made up of low level resources. And it takes quite a number of these to put together something like a frontend or an API. For example, it takes around 70 low level AWS resources to create a Next.js app on AWS.

As a result, Infrastructure as Code has been traditionally only been used by DevOps or Platform engineers.

To fix this, SST has components that can help you with the most common features in your app.


Built-in

The built-in components in SST, the ones you see in the sidebar, are designed to make it really easy to create the various parts of your app.

For example, you don’t need to know a lot of AWS details to deploy your Next.js frontend:

sst.config.ts
new sst.aws.Nextjs("MyWeb");

And because this is all in code, it’s straightforward to configure this further.

sst.config.ts
new sst.aws.Nextjs("MyWeb", {
domain: "my-app.com",
path: "packages/web",
imageOptimization: {
memory: "512 MB"
},
buildCommand: "npm run build"
});

You can even take this a step further and completely transform how the low level resources are created. We’ll look at this below.

Currently SST has built-in components for two cloud providers.


AWS

The AWS built-in components are designed to make it easy to work with AWS.

These components are namespaced under sst.aws.* and listed under AWS in the sidebar. Internally they use Pulumi’s AWS Classic provider.


Cloudflare

These components are namespaced under sst.cloudflare.* and listed under Cloudflare in the sidebar. Internally they use Pulumi’s Cloudflare provider.


Constructor

To add a component to your app, you create an instance of it by passing in a couple of args. For example, here’s the signature of the Function component.

new sst.aws.Function(name: string, args: FunctionArgs, opts?: pulumi.ComponentResourceOptions)

Each component takes the following:

  • name: The name of the component. This needs to be unique across your entire app.
  • args: An object of properties that allows you to configure the component.
  • opts?: An optional object of properties that allows you to configure this component in Pulumi.

Here’s an example of creating a Function component:

sst.config.ts
const function = new sst.aws.Function("MyFunction", {
handler: "src/lambda.handler"
});

Name

There are two guidelines to follow when naming your components:

  1. Component names are global across your entire app.
  2. Use PascalCase for the component name. For example, MyFunction instead of myFunction or my_function.

These make it possible to link resources together in your SST app.


Args

Each component takes a set of args that allow you to configure it. These args are specific to each component. For example, the Function component takes FunctionArgs.

Most of these args are optional, meaning that most components need very little configuration to get started. Typically, the most common configuration options are lifted to the top-level. To further configure the component, you’ll need to use the transform prop.

Args usually take primitive types. However, they also take a special version of a primitive type. It’ll look something like Input<string>. We’ll look at this in detail below.


Transform

Most components take a transform prop as a part of their constructor or methods. It’s an object that takes callbacks that allow you to transform how that component’s infrastructure is created.

For example, here’s what the transform prop looks like for the Function component:

  • function: A callback to transform the underlying Lambda function
  • logGroup: A callback to transform the Lambda’s LogGroup resource
  • role: A callback to transform the role that the Lambda function assumes

The type for these callbacks is similar. Here’s what the role callback looks like:

RoleArgs | (args: RoleArgs, opts: pulumi.ComponentResourceOptions, name: string) => void

So this takes either a RoleArgs object or a function that takes RoleArgs. Where args, opts, and name are the arguments for the role constructor passed to Pulumi.

This allows you to either:

  • Pass in your own RoleArgs object.

    {
    transform: {
    role: {
    name: "MyRole"
    }
    }
    }

    This is merged with the original RoleArgs that were going to be passed to the component.

  • Or, pass in a callback that takes the current RoleArgs and mutates it.

    {
    transform: {
    role: (args) => {
    args.name = `${args.name}-MyRole`;
    opts.retainOnDelete = true;
    }
    }
    }

$transform

Similar to the component transform, we have the global $transform. This allows you to transform how a component of a given type is created.

For example, to set a default runtime for all function components.

sst.config.ts
$transform(sst.aws.Function, (args, opts) => {
args.runtime = "nodejs18.x";
})

Here, args and opts are what you’d pass to the Function component. Recall the signature of the Function component:

sst.config.ts
new sst.aws.Function(name: string, args: FunctionArgs, opts?: pulumi.ComponentResourceOptions)

Read more about the global $transform.


Properties

An instance of a component exposes a set of properties. For example, the Function component exposes the following propertiesarn, name, url, and nodes.

const functionArn = function.arn;

These can be used to output info about your app or can be used as args for other components.

These are typically primitive types. However, they can also be a special version of a primitive type. It’ll look something like Output<string>. We’ll look at this in detail below.


Some of these properties are also made available via resource linking. This allows you to access them in your functions and frontends in a typesafe way.

For example, a Function exposes its name through its links.


Nodes

The nodes property that a component exposes gives you access to the underlying infrastructure. This is an object that contains references to the underlying Pulumi components that are created.

For example, the Function component exposes the following nodesfunction, logGroup, and role.


Outputs

The properties of a component are typically of a special type that looks something like, Output<primitive>.

These are values that are not available yet and will be resolved as the deploy progresses. However, these outputs can be used as args in other components.

This makes it so that parts of your app are not blocked and all your resources are deployed as concurrently as possible.

For example, let’s create a function with a url.

sst.config.ts
const myFunction = new sst.aws.Function("MyFunction", {
url: true,
handler: "src/lambda.handler"
});

Here, myFunction.url is of type Output<string>. We want to use this function url as a route in our router.

sst.config.ts
new sst.aws.Router("MyRouter", {
routes: {
"/api": myFunction.url
}
});

The route arg takes Input<string>, which means it can take a string or an output. This creates a dependency internally. So the router will be deployed after the function has been. However, other components that are not dependent on this function can be deployed concurrently.

You can read more about Input and Output types on the Pulumi docs.


Apply

Since outputs are values that are yet to be resolved, you cannot use them in regular operations. You’ll need to resolve them first.

For example, let’s take the function url from above. We cannot do the following.

sst.config.ts
const newUrl = myFunction.url + "/foo";

This is because the value of the output is not known at the time of this operation. We’ll need to resolve it.

The easiest way to work with an output is using .apply. It’ll allow you to apply an operation on the output and return a new output.

sst.config.ts
const newUrl = myFunction.url.apply((value) => value + "/foo");

In this case, newUrl is also an Output<string>.


Helpers

To make it a little easier to work with outputs, we have the following global helper functions.


$concat

This let’s you do.

sst.config.ts
const newUrl = $concat(myFunction.url, "/foo");

Instead of the apply.

sst.config.ts
const newUrl = myFunction.url.apply((value) => value + "/foo");

Read more about $concat.


$interpolate

This let’s you do.

sst.config.ts
const newUrl = $interpolate`${myFunction.url}/foo`;

Instead of the apply.

sst.config.ts
const newUrl = myFunction.url.apply((value) => value + "/foo");

Read more about $interpolate.


$jsonParse

This is for outputs that are JSON strings. So instead of doing this.

sst.config.ts
const policy = policyStr.apply((policy) =>
JSON.parse(policy)
);

You can.

sst.config.ts
const policy = $jsonParse(policyStr);

Read more about $jsonParse.


$jsonStringify

Similarly, for outputs that are JSON objects. Instead of doing a stringify after an apply.

sst.config.ts
const policy = policyObj.apply((policy) =>
JSON.stringify(policy)
);

You can.

sst.config.ts
const policy = $jsonStringify(policyObj);

Read more about $jsonStringify.


$resolve

And finally when you are working with a list of outputs and you want to resolve them all together.

sst.config.ts
$resolve([bucket.name, worker.url]).apply(([bucketName, workerUrl]) => {
console.log(`Bucket: ${bucketName}`);
console.log(`Worker: ${workerUrl}`);
})

Read more about $resolve.