Blocks

Group related resources into one named unit.

A Block is an optional grouping for Anvil resources. You write a class that extends anvil.Block, create the resources it needs inside its constructor, and then use the class like any other resource. Blocks are optional: flat top-level resources work the same way. In the project below, SharedResourceBlock creates an S3 bucket and a DSQL database, and anvil.config.ts uses it like any other resource: it creates a Lambda and an HTTP API, connects the Lambda to the Block's database, and exports the API URL.

block-demo/anvil.config.ts · 42 lines

import { App } from '@anvil-cloud/sdk';
import * as anvil from '@anvil-cloud/sdk';
import { SharedResourceBlock } from './blocks/sharedResourceBlock';

export default new App({
  defaults: {
    tags: {
      costCenter: 'platform-eng',
      project: 'block-demo',
    },
  },
  providers: {
    aws: { region: 'ap-southeast-2' },
  },
  run(ctx) {
    const shared = new SharedResourceBlock('shared');

    const getItems = new anvil.aws.Lambda('get-items', {
      runtime: 'nodejs24.x',
      handler: 'handlers/getItems.handler',
      environment: {
        DSQL_ENDPOINT: shared.database.endpoint,
      },
    });
    shared.database.grantConnect(getItems, {
      schemas: [{ schema: 'app', roleName: 'app_read' }],
    });

    const api = new anvil.aws.HttpApi('api', {
      routes: [
        {
          method: 'GET',
          path: '/items',
          consumer: { lambda: { arn: getItems.arn } },
        },
      ],
    });

    ctx.export('apiUrl', api.url);
    ctx.export('bucketName', shared.assets.bucketName);
  },
});

block-demo/anvil.config.py · 30 lines

import anvil_cloud as anvil

from blocks.shared_resource_block import SharedResourceBlock


def infra(ctx: anvil.Context):
    shared = SharedResourceBlock("shared")

    get_items = anvil.aws.Lambda("get-items",
        runtime="nodejs24.x",
        handler="handlers/getItems.handler",
        environment={"DSQL_ENDPOINT": shared.database.endpoint},
    )
    shared.database.grant_connect(get_items,
        schemas=[{"schema": "app", "role_name": "app_read"}],
    )

    api = anvil.aws.HttpApi("api",
        routes=[{
            "method": "GET",
            "path": "/items",
            "consumer": {"lambda_": {"arn": get_items.arn}},
        }],
    )

    ctx.export("api_url", api.url)
    ctx.export("bucket_name", shared.assets.bucket_name)


anvil.App(run=infra)

block-demo/main.go · 59 lines

package main

import (
    "block-demo/blocks"

    "github.com/anvil-cloud/anvil/sdk/go/anvil"
    anvilaws "github.com/anvil-cloud/anvil/sdk/go/anvil/aws"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
    anvil.Run(anvil.AppConfig{
        Run: func(ctx *anvil.Context) error {
            shared, err := blocks.NewSharedResourceBlock(ctx.PulumiCtx(), "shared")
            if err != nil {
                return err
            }

            getItems, err := anvilaws.NewLambda(ctx.PulumiCtx(), "get-items", &anvilaws.LambdaArgs{
                Runtime:     anvilaws.LambdaRuntime_Nodejs24_x,
                Handler:     pulumi.String("handlers/getItems.handler"),
                Environment: pulumi.StringMap{"DSQL_ENDPOINT": shared.Database.Endpoint},
            })
            if err != nil {
                return err
            }

            _, err = anvilaws.NewDSQLConnect(ctx.PulumiCtx(), "get-items-connect", &anvilaws.DSQLConnectArgs{
                ClusterArns:    shared.Database.ClusterArns,
                TargetRoleArn:  getItems.RoleArn,
                TargetName:     pulumi.String("get-items"),
                DbRoles:        pulumi.StringArray{pulumi.String("app_read")},
                RolesTableName: shared.Database.RolesTableName,
            })
            if err != nil {
                return err
            }

            api, err := anvilaws.NewHttpApi(ctx.PulumiCtx(), "api", &anvilaws.HttpApiArgs{
                Routes: anvilaws.HttpApiRouteArray{
                    anvilaws.HttpApiRouteArgs{
                        Method: anvilaws.HttpApiMethod("GET"),
                        Path:   pulumi.String("/items"),
                        Consumer: anvilaws.HttpApiRouteConsumerArgs{
                            Lambda: &anvilaws.HttpApiLambdaConsumerArgs{Arn: getItems.Arn},
                        },
                    },
                },
            })
            if err != nil {
                return err
            }

            ctx.Export("apiUrl", api.Url)
            ctx.Export("bucketName", shared.Assets.BucketName)
            return nil
        },
    })
}

Anvil's Lambda runtimes are Node.js only (nodejs24.x, nodejs22.x), so the function code stays in TypeScript in every language. Only the infrastructure is written in each language.

What a Block gives you

  • Parenting. Resources created inside the constructor belong to the Block. In TypeScript and Python this is automatic. In Go, pass r.Opts() to each child.
  • stage and project. Every Block can read the current stage and project name, so names can include the stage without hardcoding it.
  • Published outputs. A Block can expose selected values with this.output(name, value). Other code can reach them through ctx.ref (see App & Context).
  • Automatic tags. Default tags from App apply to resources inside a Block too.

Outputs

Only publish values from resources the Block owns. A Block is a container for its own resources, not a relay for resources it doesn't create. Published outputs are how other parts of the program read the Block's results without reaching into its internals.