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. The names of SST’s built-in components and components extended with Linkable.wrap need to be global across your entire app.

    This allows Resource Linking to look these resources up at runtime.

  2. Optionally, use PascalCase for the component name.

    For example, you might name your bucket, MyBucket and use Resource Linking to look it up with Resource.MyBucket.

    However this is purely cosmetic. You can use kebab case. So my-bucket, and look it up using Resource['my-bucket'].


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

This takes either:

  • A RoleArgs object. For example:

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

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

  • A function that takes RoleArgs. Here’s the function signature:

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

    Where args, opts, and name are the arguments for the Role constructor passed to Pulumi.

    So you can pass in a callback that takes the current RoleArgs and mutates it.

    {
    transform: {
    role: (args, opts) => {
    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, set a default runtime for your functions.

sst.config.ts
$transform(sst.aws.Function, (args, opts) => {
// Set the default if it's not set by the component
args.runtime ??= "nodejs18.x";
});

This sets the runtime for any Function component that’ll be created after this call.

The reason we do the check for args.runtime is to allow components to override the default. We do this by only setting the default if the component isn’t specifying its own runtime.

sst.config.ts
new sst.aws.Function("MyFunctionA", {
handler: "src/lambdaA.handler"
});
new sst.aws.Function("MyFunctionB", {
handler: "src/lambdaB.handler",
runtime: "nodejs20.x"
});

So given the above transform, MyFunctionA will have a runtime of nodejs18.x and MyFunctionB will have a runtime of nodejs20.x.

The args and opts in the $transform callback 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 an 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 lets 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 lets 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.


Versioning

SST components evolve over time, sometimes introducing breaking changes. To maintain backwards compatibility, we implement a component versioning scheme.

For example, we released a new version the Vpc that does not create a NAT Gateway by default. To roll this out the previous version of the Vpc component was renamed to Vpc.v1.

Now if you were using the original Vpc component, update SST, and deploy; you’ll get an error during the deploy saying that there’s a new version of this component.

This allows you to decide what you want to do with this component.


Continue with the old version

If you prefer to continue using the older version of a component, you can rename it.

sst.config.ts
const vpc = new sst.aws.Vpc("MyVpc");
const vpc = new sst.aws.Vpc.v1("MyVpc");

Now if you deploy again, SST knows that you want to stick with the old version and it won’t error.


Update to the latest version

Instead, if you wanted to update to the latest version, you’ll have to rename the component.

sst.config.ts
const vpc = new sst.aws.Vpc("MyVpc");
const vpc = new sst.aws.Vpc("MyNewVpc");

Now if you redeploy, it’ll remove the previously created component and recreate it with the new name and the latest version.

This is because from SST’s perspective it looks like the MyVpc component was removed and a new component called MyNewVpc was added.

Since these are being recreated you’ve to be aware that there might be a period of time when that resource is not around. This might cause some downtime, depending on the resource.