constructs
constructs is the tree and composition model underneath AWS CDK and other infrastructure-as-code libraries. A Construct is a node with a scope and sibling-unique ID; nodes can contain children, metadata, context, validations, and ordering dependencies. The package does not create cloud resources or synthesize a deployment format by itself. It gives framework authors and reusable infrastructure libraries a common object graph on which higher-level resources can be built.
Install constructs because a construct framework requires it or because you are building that framework, not because you merely want infrastructure as code. Its tree API is compact and well maintained, but by itself it cannot provision or synthesize anything.
Use it if
- You are authoring reusable AWS CDK constructs and need the exact base class expected by CDK libraries
- You are building an infrastructure framework that needs a hierarchical graph with stable paths, context, metadata, and validation
- You publish a jsii library and need the same construct model exposed to TypeScript, Python, Java, .NET, and Go consumers
- You need deployment-order relationships between individual constructs or groups of construct trees
- You only want to define AWS resources: install aws-cdk-lib, which uses constructs and supplies the actual service resources and synthesis behavior
- You expect a general component framework for application code: the API is shaped around persistent desired state, scope trees, synthesis metadata, and deployment ordering
- You cannot align major versions across your infrastructure packages: CDK libraries declare compatible constructs ranges, and mixing v3 and v10 types creates install or type conflicts
- You need a modern ESM-only dependency: version 10.8.1 publishes a CommonJS main entry and declarations but no module or exports entry in its package metadata
- You want IDs to remain opaque user strings: IDs containing slash or newline are rewritten with double hyphens, paths are structural, and the 42-character addr deliberately ignores scopes named Default
Setup reality
For direct TypeScript use, npm install constructs is sufficient. Version 10.8.1 has no runtime or peer dependencies, includes declarations, and supports all maintained Node.js releases. Most application developers should not install it alone: aws-cdk-lib and third-party CDK packages already depend on its types, and you normally extend Construct inside a CDK App or Stack. Check every infrastructure package's peer-dependency range before choosing a major. npm still exposes a latest-3 tag, while current AWS CDK v2 libraries are built around constructs v10; installing the wrong line produces duplicate-type and peer-resolution trouble that looks unrelated to your code. The package is CommonJS rather than a native ESM export map, though TypeScript and Node interop generally handle the named imports. IDs must be unique among siblings and slash or newline characters are changed to double hyphens. Context must be set before adding children because constructors may read it immediately. lock() recursively prevents more children, and duplicate IDs throw during construction. If a monorepo or symlink creates two installed copies, instanceof Construct can fail; the API reference explicitly says to use Construct.isConstruct(). Standalone trees do nothing until your framework walks them and turns their state, metadata, dependencies, and validation errors into an output document.
Patterns
Define a reusable constructdefine-construct
import { Construct } from 'constructs';
interface QueueGroupProps {
readonly queueName: string;
}
class QueueGroup extends Construct {
constructor(scope: Construct, id: string, props: QueueGroupProps) {
super(scope, id);
this.node.addMetadata('queueName', props.queueName);
}
}Calling super(scope, id) registers the instance in the tree immediately, so sibling IDs must already be unique.
Create a standalone root and childrencreate-root-tree
import { Construct, RootConstruct } from 'constructs';
const root = new RootConstruct('App');
const network = new Construct(root, 'Network');
const database = new Construct(root, 'Database');A RootConstruct has no parent scope; this tree has no deployment behavior until another framework interprets it.
Read structural IDs, paths, and addressesinspect-paths
console.log(database.node.id); // Database
console.log(database.node.path); // App/Database
console.log(database.node.addr); // c8 followed by 40 hexadecimal charactersaddr is deterministic but not a unique identity; scopes named Default are excluded from its hash.
Find direct children safelyfind-child
const maybeDb = root.node.tryFindChild('Database');
if (maybeDb) console.log(maybeDb.node.path);
const requiredNetwork = root.node.findChild('Network');findChild() throws when the direct child is absent; tryFindChild() returns undefined and neither searches descendants.
Traverse the construct treewalk-tree
import { ConstructOrder } from 'constructs';
const nodes = root.node.findAll(ConstructOrder.PREORDER);
for (const construct of nodes) {
console.log(construct.node.path);
}findAll() includes the host construct itself; choose PREORDER or POSTORDER when processing order affects output.
Set context before creating childrenshare-context
root.node.setContext('environment', 'production');
const service = new Construct(root, 'Service');
const environment = service.node.getContext('environment');
const optionalRegion = service.node.tryGetContext('region');Set context first. A child's constructor may read it, and getContext() throws when no value exists while tryGetContext() returns undefined.
Attach diagnostic metadataadd-metadata
service.node.addMetadata(
'com.example:owner',
{ team: 'platform' },
{ stackTrace: true },
);
console.log(service.node.metadata);Construct metadata is not CloudFormation resource metadata; frameworks decide whether and where it is synthesized.
Add and remove an ordering dependencyorder-constructs
service.node.addDependency(database);
console.log(service.node.dependencies);
service.node.removeDependency(database);The target framework defines what ordering means; removeDependency() must receive the same object used when the dependency was added.
Depend on several disjoint construct treesgroup-dependencies
import { DependencyGroup } from 'constructs';
const prerequisites = new DependencyGroup(network, database);
service.node.addDependency(prerequisites);A DependencyGroup represents all dependency roots as one IDependable and can be extended later with group.add().
Register and run validationvalidate-tree
service.node.addValidation({
validate: () => service.node.id.length < 3
? ['service id must be at least three characters']
: [],
});
const errors = service.node.validate();Adding a validation does not run it automatically; the framework or your code must call validate() and handle the returned messages.
Detect constructs across package copiesdetect-construct
import { Construct } from 'constructs';
if (Construct.isConstruct(value)) {
console.log(value.node.path);
}Use isConstruct() instead of instanceof Construct because symlinked monorepos can load multiple class copies.
Prevent later structural changeslock-tree
root.node.lock();
// Any later new Construct(root, 'LateChild') will throw.lock() applies recursively to the current descendants and is irreversible; finish tree construction first.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| aws-cdk-lib | npm | Choose it when you want deployable AWS resources and higher-level constructs rather than only the underlying tree model |
| cdktf | npm | Choose it when the target is Terraform providers and Terraform state while keeping a construct-oriented programming style |
| @pulumi/pulumi | npm | Choose it when you want a complete multi-cloud infrastructure SDK with its own resource graph and deployment engine |