Skip to main content

Extending Booster with Rockets!

You can extend Booster by creating rockets (Booster Framework extensions). A rocket is just a node package that implements the public Booster rocket interfaces. You can use them for:

  1. Extend your infrastructure: You can write a rocket that adds provider resources to your application stack.
  2. Runtime extensions: Add new annotations and interfaces, which combined with infrastructure extensions, could implement new abstractions on top of highly requested use cases.

If you want to create a rocket that supports several cloud providers or want to provide extra decorators and functionality on top of the infrastructure extensions, you'll probably need to distribute it as a set of separate packages. In this scenario we recommend using a monorepo management tool like Microsoft Rush to maintail them all together in a single repository, but this is not a requirement. Your packages will work perfectly fine if you maintain them in separate repositories.

Create an Infrastructure Rocket package to extend the default Booster-provided infrastructure

A rocket is an npm package that extends your current Booster architecture. The structure is simple, and it mainly has 2 methods: mountStack and unmountStack. We'll explain what they are shortly.

Infrastructure Rocket interfaces are provider-dependant because each provider defines their own way to manage context, so Infrastructure Rockets must import the corresponding booster infrastructure package for their chosen provider:

  • For AWS: @boostercloud/framework-provider-aws-infrastructure
  • For Azure: @boostercloud/framework-provider-azure-infrastructure
  • For Local (dev environment): @boostercloud/framework-provider-local-infrastructure

Notice that, as the only thing you'll need from that package is the InfrastructureRocket interface, it is preferable to import it as a dev dependency to avoid including such a big package in your deployed lambdas.

So let's start by creating a new package and adding the provider-depdendent dependency as well as the typescript and @boostercloud/framework-types packages:

mkdir rocket-your-rocket-name-aws-infrastructure
cd rocket-your-rocket-name-aws-infrastructure
npm init
...
npm install --save-dev @boostercloud/framework-provider-aws-infrastructure @boostercloud/framework-types typescript

In the case of AWS we use the AWS CDK for TypeScript, so you'll also need to import the AWS CDK package:

npm install --save-dev @aws-cdk/core

The basic structure of an Infrastructure Rocket project is quite simple as you can see here:

rocket-your-rocket-name-aws-infrastructure
├── package.json
├── src
├── index.ts
└── your-main-class.ts

<your-main-class>.ts can be named as you want and this is where we define the mountStack and unmount methods.

import { RocketUtils } from '@boostercloud/framework-provider-aws-infrastructure'
import { BoosterConfig } from '@boostercloud/framework-types'
import { Stack } from '@aws-cdk/core'
import { YourRocketParams } from '.'

export class YourMainClass {
public static mountStack(params: YourRocketParams, stack: Stack, config: BoosterConfig): void {
/* CDK code to expand your Booster infrastructure */
}
public static unmountStack(params: YourRocketParams, utils: RocketUtils): void {
/* Optional code that runs before removing the stack */
}
}

Let's look in more detail these two special functions:

  • mountStack: Whenever we are deploying our Booster application (boost deploy) this method will also be run. It receives two params:

    • stack: An initialized AWS CDK stack that you can use to add new resources. Check out the Stack API in the official CDK documentation. This is the same stack instance that Booster uses to deploy its resources, so your resources will automatically be deployed along with the Booster's ones on the same stack.
    • config: It includes properties of the Booster project (e.g. project name) that come in handy for your rocket.
  • unmountStack: This function executes when you run the boost nuke command, just before starting the deletion of the cloud resources. When you nuke your Booster application, resources added by your rocket are automatically destroyed with the rest of the application stack. However, in certain cases, you may need extra steps during the deletion process. The unmountStack function serves this purpose. For example, in AWS, you must first empty any S3 buckets before deleting your stack. You can achieve this within the unmountStack method.

In addition to your main rocket class, you'll need an index.ts file that default exports an object that conforms to the InfrastructureRocket interface:

export interface InfrastructureRocket {
mountStack: (stack: Stack, config: BoosterConfig) => void
unmountStack?: (utils: RocketUtils) => void
}

You'll have to implement a default exported function that accepts a parameters object and returns an initialized InfrastructureRocket object:

import { InfrastructureRocket } from '@boostercloud/framework-provider-aws-infrastructure'
import { YourMainClass } from './your-main-class';

export interface YourRocketParams {
param1: string
}

const YourRocketInitializator = (params: YourRocketParams): InfrastructureRocket => ({
mountStack: SomePrivateObject.mountStack.bind(null, params),
unmountStack: SomePrivateObject.unmountStack.bind(null, params),
})

export default YourRocketInitializator

Note that Infrastructure Rockets must not be part of the Booster application code to prevent including the CDK and other unnecessary dependencies in the deployed lambdas. This is due to strict code size restrictions on most platforms. To address this, Infrastructure Rockets are dynamically loaded by Booster, using package names as strings in the application config file:

src/config/production.ts:

Booster.configure('development', (config: BoosterConfig): void => {
config.appName = 'my-store'
config.providerPackage = '@boostercloud/framework-provider-aws'
config.rockets = [
{
packageName: 'rocket-your-rocket-name-aws-infrastructure', // Your infrastructure rocket package name
parameters: {
// A custom object with the parameters needed by your infrastructure rocket initializer
hello: 'world',
},
},
]
})

Your rocket implementation will have access to the stack (CDK in AWS or Terraform in Azure) just after Booster has finished to add all its default resources, so while the most common scenario to implement a rocket is to create additional resources, it's also possible to inspect or alter the Booster stack. If you're considering creating and maintaining your own fork of one of the default provider runtime implementations, it could be easier to create a rocket instead.

Provide new abtractions with custom decorators

Rockets can be utilized to extend the Booster framework by providing additional decorators that offer new abstractions. When creating a decorator as part of your rocket, you should deliver it as a package that, once compiled, does not have any infrastructure dependencies, so if your rocket provides both infrastructure and runtime extensions, it's advisable to deliver it as a pair of packages or more.

A common pattern when creating decorators for Booster is to use a singleton object to store metadata about the decorated structures. This singleton object stores data generated during the decorator's execution, which can then be accessed from other parts of the user's project, the rocket's infrastructure package or even other rockets. This data can be used during deployment to generate extra tables, endpoints, or other resources.

To create a new custom decorator for the Booster framework with singleton storage, follow these steps:

  1. Create a new npm package for your rocket. This package should not have any infrastructure dependencies once compiled.
$ mkdir my-booster-rocket
$ cd my-booster-rocket
$ npm init
  1. Add typescript as a dependency
$ npm install typescript --save-dev
  1. Create a src directory to hold your decorator code:
$ mkdir src
  1. Inside the src directory, create a new TypeScript file for your singleton object, e.g., RocketSingleton.ts:
$ touch src/RocketSingleton.ts
  1. Implement your singleton object to store your metadata, for instance, a list of special classes that we will "mark" for later:
// src/RocketSingleton.ts
export class RocketSingleton {
public static specialClasses: Function[] = [];

private constructor() {}

public static addSpecialClass(target: Function): void {
RocketSingleton.specialClasses.push(target)
}
}
  1. Create a new TypeScript file for your custom decorator, e.g., MyCustomDecorator.ts:
$ touch src/MyCustomDecorator.ts
  1. Implement your custom decorator using the singleton object:
// src/MyCustomDecorator.ts
import { RocketSingleton } from "./RocketSingleton"

export function MyCustomDecorator(): (target: Function) => void {
return (target: Function) => {
// Implement your decorator logic here.
console.log(`MyCustomDecorator applied on ${target.name}`)
RocketSingleton.addSpecialClass(target)
}
}
  1. Export your decorator from the package's entry point, e.g., index.ts:
// src/index.ts
export * from './MyCustomDecorator';
export * from './RocketSingleton';

Now you have a custom decorator that can be used within the Booster framework. Users can install your rocket package and use the decorator in their Booster applications:

$ npm install my-booster-rocket
// src/MySpecialClass.ts
import { MyCustomDecorator, RocketSingleton } from 'my-booster-rocket';

@MyCustomDecorator()
class MySpecialClass {
// Application logic here
}

console.log(RocketSingleton.specialClasses) // [ [Function: MySpecialClass] ]

This example demonstrates how to create a custom decorator with a singleton object for storing data and package it as a rocket for use with the Booster framework. Following this pattern will allow you to extend Booster with new abstractions and provide additional functionality for users. The singleton object can be used to store and retrieve data across different parts of the user's project, enabling features such as generating extra tables or endpoints during deployment. This approach ensures a consistent and flexible way to extend the Booster framework while maintaining ease of use for developers.

Naming recommendations

There are no restrictions on how you name your rocket packages, but we propose the following naming convention to make it easier to find your extensions in the vast npm library and find related packages (code and infrastructure extensions cannot be distributed in the same package).

  • rocket-{rocket-name}-{provider}: A rocket that adds runtime functionality or init scripts. This code will be deployed along with your application code to the lambdas.
  • rocket-{rocket-name}-{provider}-infrastructure: A rocket that provides infrastructure extensions or implements deploy hooks. This code will only be used on developer's or CI/CD systems machines and won't be deployed to lambda with the rest of the application code.

Notice that some functionalities, for instance an S3 uploader, might require both runtime and infrastructure extensions. In these cases, the convention is to use the same name rocket-name and add the suffix -infrastructure to the infrastructure rocket. It's recommended, but not required, to manage these dependent packages in a monorepo and ensure that the versions match on each release.

If you want to support the same functionality in several providers, it could be handy to also have a package named rocket-{rocket-name}-{provider}-core where you can have cross-provider code that you can use from all the provider-specific implementations. For instance, a file uploader rocket that supports both AWS and Azure could have an structure like this:

  • rocket-file-uploader-core: Defines abstract decorators and interfaces to handle uploaded files.
  • rocket-file-uploader-aws: Implements the API calls to S3 to get the uploaded files.
  • rocket-file-uploader-aws-infrastructure: Adds a dedicated S3 bucket.
  • rocket-file-uploader-azure: Implements the API calls to Azure Storage to get the uploaded files.
  • rocket-file-uploader-azure-infrastructure: Configures file storage.

Booster Rockets list

Here you can check out the official Booster Rockets developed at this time: