TypeScript DynamoDB
This generator creates a new TypeScript DynamoDB project backed by Amazon DynamoDB, using ElectroDB for type-safe entity modelling. It generates the application code and infrastructure needed to provision and manage a DynamoDB table using AWS CDK or Terraform, with single-table design support and built-in local development via DynamoDB Local.
Generate a TypeScript DynamoDB Project
Section titled “Generate a TypeScript DynamoDB Project”Run this generator@aws/nx-plugin:ts#dynamodb
pnpm nx g @aws/nx-plugin:ts#dynamodb yarn nx g @aws/nx-plugin:ts#dynamodb npx nx g @aws/nx-plugin:ts#dynamodb bunx nx g @aws/nx-plugin:ts#dynamodb- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#dynamodb - Fill in the required parameters
- Click
Generate
Build your command8
Required
Options
Section titled “Options”nameRequiredstringName of the DynamoDB project to generate
directorystringDefault:packagesThe directory to store the project in.
frameworkenumDefault:electrodbThe framework to use for DynamoDB entities.
electrodbinfraenumDefault:dynamodbInfrastructure to provision for the DynamoDB table.
dynamodbnoneiacenumDefault:inheritThe preferred IaC provider. By default this is inherited from your initial selection.
inheritcdkterraformsubDirectorystringThe sub directory the project is placed in. By default this is the project name.
tableNamestringThe DynamoDB table name. Auto-generated if not specified.
preferInstallDependenciesbooleanDefault:trueWhether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end.
Generator Output
Section titled “Generator Output”The generator creates the following project structure in the <directory>/<name> directory:
Directorysrc
- index.ts Project entry point and exports
- client.ts DynamoDB client singleton and table name resolution
Directoryentities
- example.ts Example ElectroDB entity definition
- index.ts Entity exports
- config.json Table configuration including GSI definitions and local development settings
- package.json Project manifest defining the project’s package name and dependencies
- project.json Project configuration and build targets
The local development scripts are shared across all DynamoDB projects (both TypeScript and Python) and generated once into:
Directorypackages/common/scripts/src/dynamodb
- create-local-table.ts Creates the DynamoDB table in the local DynamoDB Local instance
- pull-image.ts Pulls the DynamoDB Local image
- start-container.ts Starts the DynamoDB Local container
Infrastructure
Section titled “Infrastructure”Since this generator vends infrastructure as code based on your chosen iac, it will create a project in packages/common which includes the relevant CDK constructs or Terraform modules.
The common infrastructure as code project is structured as follows:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs for infrastructure specific to a project/generator
- …
Directorycore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Directorypackages/common/constructs/src
Directoryapp
Directorydynamodb
- <name>.ts Infrastructure specific to your table
Directorycore
- dynamodb.ts Generic DynamoDB table construct
Directorypackages/common/terraform/src
Directoryapp
Directorydynamodb
Directory<name>
- <name>.tf Module specific to your table
Directorycore
Directorydynamodb
- dynamodb.tf Generic DynamoDB module
Architecture
Section titled “Architecture”The deployed project provisions the table itself, which any project it is connected to reads and writes:
Local Development
Section titled “Local Development”Starting Local DynamoDB
Section titled “Starting Local DynamoDB”The generator configures a dev target that starts a DynamoDB Local instance and creates the table. Use the project’s dev target:
pnpm nx dev <project-name>yarn nx dev <project-name>npx nx dev <project-name>bunx nx dev <project-name>This automatically:
- Pulls the DynamoDB Local image (
pull-imagetarget) - Starts a container
- Creates a local table with the indexes defined in
config.json
Data Modelling
Section titled “Data Modelling”The generated project uses ElectroDB for type-safe entity modelling on a single DynamoDB table, following DynamoDB’s single-table design. Add or update entity files under src/entities/, using the generated example entity as a starting point.
Example entity definition:
import { Entity } from 'electrodb';import { getDynamoDBClient, resolveTableName } from '../client.js';
export const createExampleEntity = async () => new Entity( { model: { entity: 'example', version: '1', service: 'MyTable', }, attributes: { id: { type: 'string', required: true, }, createdAt: { type: 'string', required: true, default: () => new Date().toISOString(), readOnly: true, }, updatedAt: { type: 'string', required: true, default: () => new Date().toISOString(), watch: '*', set: () => new Date().toISOString(), }, }, indexes: { primary: { pk: { field: 'pk', composite: ['id'], }, sk: { field: 'sk', composite: [], }, }, }, }, { client: getDynamoDBClient(), table: await resolveTableName() }, );For more details, see the ElectroDB entity documentation.
Using the DynamoDB Client
Section titled “Using the DynamoDB Client”The generated src/client.ts exports two key utilities:
getDynamoDBClient()— returns a cached singletonDynamoDBClient. WhenLOCAL_DEV=true, connects to the local DynamoDB Local instance; otherwise creates an AWS client using the default credential chain.resolveTableName()— returns the DynamoDB table name. WhenLOCAL_DEV=true, returns the local table name constant; otherwise fetches the name from AWS AppConfig using theRUNTIME_CONFIG_APP_IDenvironment variable and caches it for subsequent calls.
Stopping Local DynamoDB
Section titled “Stopping Local DynamoDB”Stopping dev (e.g. with Ctrl+C) automatically removes the DynamoDB Local container, but preserves the named volume so your data persists across restarts.
Adding/Removing Global Secondary Indexes
Section titled “Adding/Removing Global Secondary Indexes”GSIs are defined in config.json at the project root under the tableConfig.globalSecondaryIndexes key. Add an entry for each GSI, following the single-table design naming convention for GSI keys:
{ ... "tableConfig": { "globalSecondaryIndexes": [ { "indexName": "gsi1pk-gsi1sk-index", "partitionKey": "gsi1pk", "sortKey": "gsi1sk" }, { "indexName": "gsi2pk-gsi2sk-index", "partitionKey": "gsi2pk", "sortKey": "gsi2sk" } ] }}The sortKey field is optional for hash-key-only GSIs.
This config file is the single source of truth read by all consumers:
- Local development —
devreadsconfig.jsonand creates or updates the local table to match the GSI list - CDK — the construct reads
config.jsonat synth time, so GSI changes are reflected on the nextcdk deploy - Terraform — the module reads
config.jsonat plan/apply time
One GSI per Deployment
Section titled “One GSI per Deployment”Connecting to the Table
Section titled “Connecting to the Table”In any TypeScript project, import entity factories from your DynamoDB package and use them directly:
import { createExampleEntity } from '@my-scope/my-table';
const entity = await createExampleEntity();const result = await entity.query.primary({ id: '123' }).go();Behind the scenes, createExampleEntity() calls resolveTableName() to fetch the table name from AWS AppConfig at runtime.
Deploying your Table
Section titled “Deploying your Table”The DynamoDB generator creates CDK or Terraform infrastructure based on your selected iac.
The CDK construct is created in common/constructs. Example usage:
import { MyTable } from '@my-scope/common-constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const table = new MyTable(this, 'Table'); }}This provisions a DynamoDB table with:
pk(partition key) andsk(sort key), bothStringtype- Global Secondary Indexes as defined in
config.json - On-demand (
PAY_PER_REQUEST) billing - Customer-managed KMS encryption with automatic key rotation
- Point-in-time recovery enabled
- Deletion protection enabled
- Table name registered in Runtime Config under the
dynamodbnamespace in AWS AppConfig
The Terraform module is created in common/terraform. Example usage:
module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table"}This provisions a DynamoDB table with:
pk(partition key) andsk(sort key), bothStringtype- Global Secondary Indexes as defined in
config.json - On-demand (
PAY_PER_REQUEST) billing - Customer-managed KMS encryption with automatic key rotation
- Point-in-time recovery enabled
- Deletion protection enabled, plus a
prevent_destroylifecycle guard - Table name registered in Runtime Config under the
dynamodbnamespace in AWS AppConfig
The core/runtime-config/appconfig module exposes the dynamodb namespace by default, so the table name is deployed without further configuration. If you pass namespaces to that module explicitly, keep dynamodb in the list — otherwise no configuration profile is created for it and the generated table client cannot resolve the table name.
Deletion Protection
Section titled “Deletion Protection”The table is protected by two independent guards, so that turning off either one alone cannot delete your data:
deletionProtection, enforced by DynamoDB.RemovalPolicy.RETAIN, enforced by CloudFormation, which leaves the table in place when it is removed from the stack.
deletion_protection_enabled, enforced by DynamoDB.lifecycle { prevent_destroy = true }on the table incommon/terraform/src/core/dynamodb/dynamodb.tf, enforced by Terraform, which fails any plan that would destroy the table.
Deleting the Table
Section titled “Deleting the Table”Disable protection for environments where table deletion is expected, such as short-lived development or preview stacks.
import { RemovalPolicy } from 'aws-cdk-lib';import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { deletionProtection: false, removalPolicy: RemovalPolicy.DESTROY,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" deletion_protection_enabled = false}prevent_destroy must be a literal — Terraform does not allow it to reference a variable — so it cannot be turned off from main.tf. Also remove the lifecycle block from the table in common/terraform/src/core/dynamodb/dynamodb.tf:
resource "aws_dynamodb_table" "table" { # ...
lifecycle { prevent_destroy = true }}Billing Mode
Section titled “Billing Mode”The table defaults to on-demand (PAY_PER_REQUEST) billing. Switch to provisioned capacity for predictable, high-throughput workloads.
import { BillingMode } from 'aws-cdk-lib/aws-dynamodb';import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { billingMode: BillingMode.PROVISIONED, readCapacity: 5, writeCapacity: 5,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" billing_mode = "PROVISIONED"}Point-in-time Recovery
Section titled “Point-in-time Recovery”Point-in-time recovery is enabled by default, allowing you to restore the table to any point in the last 35 days.
Disable Point-in-time Recovery
Section titled “Disable Point-in-time Recovery”import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: false },});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" point_in_time_recovery_enabled = false}Encryption
Section titled “Encryption”The table is encrypted with a customer-managed KMS key by default, created automatically for you. Switch to an AWS managed key, the AWS owned key, or bring your own KMS key, if you manage encryption differently.
Use an AWS Managed Key
Section titled “Use an AWS Managed Key”Uses the shared aws/dynamodb KMS key that AWS manages on your behalf. It’s visible in your account’s KMS console and billed per request, but there’s no key for you to create, rotate or delete.
import { TableEncryption } from 'aws-cdk-lib/aws-dynamodb';import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { encryption: TableEncryption.AWS_MANAGED,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" encryption = "AWS_MANAGED"}Use the AWS Owned Key
Section titled “Use the AWS Owned Key”Uses a key fully owned and managed by AWS — free, with no key visible in your account at all. The simplest option when you don’t need a customer- or account-visible key for compliance reasons.
import { TableEncryption } from 'aws-cdk-lib/aws-dynamodb';import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { encryption: TableEncryption.DEFAULT,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" encryption = "DEFAULT"}Switching away from CUSTOMER_MANAGED
Section titled “Switching away from CUSTOMER_MANAGED”On an already-deployed table, changing encryption away from CUSTOMER_MANAGED (to either AWS_MANAGED or DEFAULT) in a single terraform apply fails: Terraform destroys the customer-managed key before updating the table, and DynamoDB then rejects the update because the key is already pending deletion.
Work around it by updating the table’s encryption directly via the AWS CLI first, then letting Terraform catch up and clean up the orphaned key:
# For AWS_MANAGED:aws dynamodb update-table --table-name <table-name> \ --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=alias/aws/dynamodb
# For DEFAULT:aws dynamodb update-table --table-name <table-name> --sse-specification Enabled=false
# Then wait for this to report ENABLED (or for SSEDescription to disappear, for DEFAULT):aws dynamodb describe-table --table-name <table-name> --query Table.SSEDescription.StatusThen update encryption in your Terraform config and run terraform apply as normal — Terraform now only needs to destroy the already-unused key, with nothing left depending on it.
Use Your Own KMS Key
Section titled “Use Your Own KMS Key”Provide an existing customer-managed key instead of having one created for you. The key must already grant the DynamoDB service the permissions it needs in its own key policy.
import { Key } from 'aws-cdk-lib/aws-kms';import { MyTable } from '@my-scope/common-constructs';
const key = Key.fromKeyArn(this, 'Key', 'arn:aws:kms:us-east-1:111111111111:key/my-key-id');
const table = new MyTable(this, 'Table', { encryptionKey: key,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" kms_key_arn = "arn:aws:kms:us-east-1:111111111111:key/my-key-id"}Encryption Key Rotation
Section titled “Encryption Key Rotation”When the table creates its own customer-managed KMS key (the default, and only when you haven’t provided your own key), that key has automatic key rotation enabled by default. Disable it if your security policy manages rotation externally.
Disable Encryption Key Rotation
Section titled “Disable Encryption Key Rotation”import { MyTable } from '@my-scope/common-constructs';
const table = new MyTable(this, 'Table', { enableKeyRotation: false,});module "my_table" { source = "../../common/terraform/src/app/dynamodb/my-table" enable_key_rotation = false}Connections
Section titled “Connections”Use the connection generator to integrate this project with others in your workspace. The following connections involve this project: