| name | creating-subpackages |
| description | Patterns and standards for creating new AWS service subpackages in the monorepo. Use when adding new service packages like Lambda, DynamoDB, Step Functions, etc. |
Creating Subpackages Guide
This guide documents the patterns established when creating the Aurora package, which should be followed for all new service subpackages.
Package Structure
packages/
└── <service-name>/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│ ├── index.ts # Package exports
│ ├── constructs/
│ │ └── <resource>-<variant>.ts # Factory functions
│ ├── types/
│ │ ├── <resource>-base.ts # Base/shared types
│ │ └── <resource>-<variant>.ts # Variant-specific types
│ └── util/
│ └── <resource>-helpers.ts # Shared utilities
├── test/
│ └── <resource>-<variant>.test.ts
└── docs/
└── <resource>-guide.md
Factory Function Pattern
No ID Parameter
Factory functions should NOT take an id parameter. Use the resource name from props as the construct ID.
export const createFunction = (scope: Construct, id: string, props: FunctionProps) => {
return new Function(scope, id, { ... });
};
export const createFunction = (scope: Construct, props: FunctionProps): FunctionResources => {
return new Function(scope, props.functionName, { ... });
};
Resource ID Naming
Use the resource name from props for ALL resource IDs to prevent collisions:
export const createFunction = (scope: Construct, props: FunctionProps): FunctionResources => {
const role = new Role(scope, `${props.functionName}-role`, { ... });
const logGroup = new LogGroup(scope, `${props.functionName}-logs`, { ... });
const func = new Function(scope, props.functionName, {
functionName: props.functionName,
role,
logGroup,
});
return { function: func, role, logGroup };
};
Type Definitions
Base Configuration Type
Create a base type for shared configuration across variants:
export type ResourceBaseConfig = {
resourceName: string;
environment?: string;
tags?: Record<string, string>;
};
export type ResourceResources = {
resource: PrimaryResource;
role?: IRole;
logGroup?: ILogGroup;
};
Variant-Specific Types
Extend the base for specific variants:
import {ResourceBaseConfig} from './resource-base';
export type VariantResourceProps = ResourceBaseConfig & {
variantOption: string;
};
Avoid Unnecessary Type Wrappers
Don't use Omit<> for properties that don't exist:
export const CONFIG: Omit<ResourceProps, 'nonExistentProp'> = { ... };
export const CONFIG: ResourceProps = { ... };
Automatic Resource Creation
Constructs should create necessary supporting resources automatically:
export const createFunction = (scope: Construct, props: FunctionProps): FunctionResources => {
const role =
props.existingRole ||
new Role(scope, `${props.functionName}-role`, {
roleName: props.roleName || `${props.functionName}-execution-role`,
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
});
const logGroup = new LogGroup(scope, `${props.functionName}-logs`, {
logGroupName: `/aws/lambda/${props.functionName}`,
retention: props.logRetention || RetentionDays.ONE_WEEK,
removalPolicy: props.removalPolicy || RemovalPolicy.DESTROY,
});
const func = new Function(scope, props.functionName, {
functionName: props.functionName,
role,
logGroup,
});
return {function: func, role, logGroup};
};
Default Values and Validation
Provide Sensible Defaults
const commonConfig = {
timeout: props.timeout || Duration.seconds(30),
memorySize: props.memorySize || 256,
retries: props.retries ?? 2,
logRetention: props.enableDetailedLogging ? props.logRetention || RetentionDays.ONE_MONTH : RetentionDays.ONE_WEEK,
};
Validate Required Dependencies
if (props.enableFeature && !props.requiredConfig) {
throw new Error(`requiredConfig must be set when enableFeature is true`);
}
Example Stack Pattern
Config Resolver for Local Overrides
export interface LocalConfig {
vpcId?: string;
subnetIds?: string[];
}
export class ConfigResolver {
private static localConfig: LocalConfig | undefined;
private static localConfigLoaded = false;
private static loadLocalConfig(): LocalConfig | undefined {
if (!this.localConfigLoaded) {
try {
const {LOCAL_CONFIG} = require('../../environments.local');
this.localConfig = LOCAL_CONFIG;
} catch {
this.localConfig = undefined;
}
this.localConfigLoaded = true;
}
return this.localConfig;
}
private static resolve<T extends LocalConfig>(baseConfig: T): T {
const localConfig = this.loadLocalConfig();
if (localConfig) {
return {...baseConfig, ...localConfig};
}
return baseConfig;
}
public static getDevConfig(): ResourceProps {
return this.resolve(DEV_CONFIG);
}
}
Config Files with Placeholders
export const DEV_CONFIG: ResourceProps = {
resourceName: 'my-resource-dev',
vpcId: 'vpc-xxxxxxxxxxxxxxxxx',
subnetIds: ['subnet-xxxxxxxxxxxxxxxxx', 'subnet-yyyyyyyyyyyyyyyyy'],
enableDetailedLogging: true,
retentionPolicy: RemovalPolicy.DESTROY,
deletionProtection: false,
};
Example Stack
export class ResourceDevStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const config = ConfigResolver.getDevConfig();
const {resource, role} = createResource(this, {
...config,
});
}
}
Package Exports
Structure the index.ts to export everything consumers need:
export {createResourceVariantA} from './constructs/resource-variant-a';
export {createResourceVariantB} from './constructs/resource-variant-b';
export type {
ResourceBaseConfig,
ResourceResources,
VariantAResourceProps,
VariantBResourceProps,
ParameterGroupConfig,
MonitoringConfig,
} from './types';
export {createParameterGroup, createMonitoringAlarm} from './util/resource-helpers';
Testing Requirements
Test Structure
describe('Resource Variant A', () => {
let app: App;
let stack: Stack;
beforeEach(() => {
app = new App({
context: {
},
});
stack = new Stack(app, 'TestStack', {
env: {account: '123456789012', region: 'us-east-1'},
});
});
test('creates resource with basic configuration', () => {
const {resource} = createResourceVariantA(stack, {
resourceName: 'test-resource',
});
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::Service::Resource', 1);
template.hasResourceProperties('AWS::Service::Resource', {
Property: 'ExpectedValue',
});
});
test('creates supporting resources automatically', () => {
createResourceVariantA(stack, {
resourceName: 'test-resource',
});
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::IAM::Role', 1);
template.resourceCountIs('AWS::Logs::LogGroup', 1);
});
});
Documentation Standards
TSDoc Requirements
Common Patterns
Environment-Based Configuration
const isProd = props.environment === 'prod';
const config = {
enableBackups: isProd,
multiAZ: isProd,
deletionProtection: isProd,
removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
};
Conditional Resource Creation
const kmsKey =
props.createKmsKey && !props.existingKmsKey
? new Key(scope, `${props.resourceName}-kms`, {
enableKeyRotation: true,
removalPolicy: RemovalPolicy.RETAIN,
})
: props.existingKmsKey;
Helper Functions
export const createParameterGroup = (scope: Construct, props: ParameterGroupConfig): ParameterGroup => {
return new ParameterGroup(scope, `${props.name}-params`, {
description: props.description,
parameters: props.parameters || {},
});
};
AWS Service-Specific Patterns
Valid Values Documentation
Document valid values for service-specific properties:
instanceType: InstanceType;
Service Limits and Defaults
if (props.timeout && props.timeout.toSeconds() > 900) {
throw new Error('Lambda timeout cannot exceed 900 seconds');
}
const monitoringInterval = props.enableEnhancedMonitoring
? props.monitoringInterval || Duration.seconds(60)
: undefined;
Integration with Root Project
Add to bin/environment.ts
export type ProjectEnvironment = EnvironmentConfig & {
myResource?: Partial<MyResourceProps>;
};
export const integrationEnvironments: ProjectEnvironment[] = [
{
...devEnv,
myResource: {
resourceName: 'my-resource-dev',
},
},
];
Add to bin/app.ts
import {MyResourceStack} from '../examples/<service>/stacks/my-resource-stack';
integrationEnvironments.forEach(env => {
if (env.myResource) {
new MyResourceStack(app, `my-resource-${env.name}`, envProps);
}
});
Checklist for New Subpackages