Patterns and standards for creating new AWS service subpackages in the monorepo. Use when adding new service packages like Lambda, DynamoDB, Step Functions, etc.
This guide documents the patterns established when creating the Aurora package, which should be followed for all new service subpackages.
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 functions should NOT take an id parameter. Use the resource name from props as the construct ID.
// ❌ Old pattern - DO NOT USE
export const createFunction = (scope: Construct, id: string, props: FunctionProps) => {
return new Function(scope, id, { ... });
};
// ✅ New pattern - USE THIS
export const createFunction = (scope: Construct, props: FunctionProps): FunctionResources => {
return new Function(scope, props.functionName, { ... });
};
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,
// ... other config
});
return { function: func, role, logGroup };
};
Create a base type for shared configuration across variants:
// types/<resource>-base.ts
export type ResourceBaseConfig = {
/** Resource name - used as construct ID and for resource naming */
resourceName: string;
/** Common config properties */
environment?: string;
tags?: Record<string, string>;
// ... other common properties
};
export type ResourceResources = {
/** The primary resource created */
resource: PrimaryResource;
/** Related resources that may be useful to consumers */
role?: IRole;
logGroup?: ILogGroup;
};
Extend the base for specific variants:
// types/<resource>-<variant>.ts
import {ResourceBaseConfig} from './resource-base';
export type VariantResourceProps = ResourceBaseConfig & {
/** Variant-specific properties */
variantOption: string;
};
Don't use Omit<> for properties that don't exist:
// ❌ Don't do this
export const CONFIG: Omit<ResourceProps, 'nonExistentProp'> = { ... };
// ✅ Do this
export const CONFIG: ResourceProps = { ... };
Constructs should create necessary supporting resources automatically:
export const createFunction = (scope: Construct, props: FunctionProps): FunctionResources => {
// Create role automatically if not provided
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')],
});
// Create log group automatically
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,
// ... other config
});
return {function: func, role, logGroup};
};
const commonConfig = {
timeout: props.timeout || Duration.seconds(30),
memorySize: props.memorySize || 256,
retries: props.retries ?? 2, // Use ?? for number defaults
// Conditional defaults based on other props
logRetention: props.enableDetailedLogging ? props.logRetention || RetentionDays.ONE_MONTH : RetentionDays.ONE_WEEK,
};
if (props.enableFeature && !props.requiredConfig) {
throw new Error(`requiredConfig must be set when enableFeature is true`);
}
// examples/<service>/config/config-resolver.ts
export interface LocalConfig {
vpcId?: string;
subnetIds?: string[];
// ... other overridable values
}
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);
}
}
// examples/<service>/config/<resource>-dev.ts
export const DEV_CONFIG: ResourceProps = {
resourceName: 'my-resource-dev',
// Use placeholder values for sensitive/environment-specific config
vpcId: 'vpc-xxxxxxxxxxxxxxxxx',
subnetIds: ['subnet-xxxxxxxxxxxxxxxxx', 'subnet-yyyyyyyyyyyyyyyyy'],
// Dev-appropriate settings
enableDetailedLogging: true,
retentionPolicy: RemovalPolicy.DESTROY,
deletionProtection: false,
};
// examples/<service>/stacks/<resource>-dev-stack.ts
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,
});
}
}
Structure the index.ts to export everything consumers need:
// src/index.ts
// Construct functions
export {createResourceVariantA} from './constructs/resource-variant-a';
export {createResourceVariantB} from './constructs/resource-variant-b';
// Types - organize by category
export type {
// Base types
ResourceBaseConfig,
ResourceResources,
// Variant types
VariantAResourceProps,
VariantBResourceProps,
// Helper types
ParameterGroupConfig,
MonitoringConfig,
} from './types';
// Utilities - only export what's useful to consumers
export {createParameterGroup, createMonitoringAlarm} from './util/resource-helpers';
describe('Resource Variant A', () => {
let app: App;
let stack: Stack;
beforeEach(() => {
app = new App({
context: {
/* mock 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',
// ... required props
});
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);
});
});
/**
* Creates a [Service] [Resource] with [key features].
*
* @remarks
* This construct creates a production-ready resource with:
* - Feature 1
* - Feature 2
* - Feature 3
*
* @param scope - The construct scope
* @param props - Configuration properties
* @returns Resources including the primary resource and supporting resources
*
* @example
* ```typescript
* import { createResource } from '@cdk-constructs/<service>';
* import { Duration } from 'aws-cdk-lib';
*
* const { resource, role } = createResource(this, {
* resourceName: 'my-resource',
* timeout: Duration.seconds(30),
* memorySize: 512,
* });
* ```
*
* @see {@link ResourceProps} for configuration options
* @see https://docs.aws.amazon.com/service/resource.html
* @public
*/
const isProd = props.environment === 'prod';
const config = {
// Expensive features gated to prod
enableBackups: isProd,
multiAZ: isProd,
// Safe defaults for non-prod
deletionProtection: isProd,
removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
};
// Create optional resources based on configuration
const kmsKey =
props.createKmsKey && !props.existingKmsKey
? new Key(scope, `${props.resourceName}-kms`, {
enableKeyRotation: true,
removalPolicy: RemovalPolicy.RETAIN,
})
: props.existingKmsKey;
// util/resource-helpers.ts
export const createParameterGroup = (scope: Construct, props: ParameterGroupConfig): ParameterGroup => {
return new ParameterGroup(scope, `${props.name}-params`, {
description: props.description,
parameters: props.parameters || {},
});
};
Document valid values for service-specific properties:
/**
* Instance type for the resource.
*
* @remarks
* Valid instance classes:
* - T3: Burstable (MICRO, SMALL, MEDIUM, LARGE, XLARGE, XLARGE2)
* - M6G: General purpose Graviton2 (LARGE and up)
* - R6G: Memory optimized Graviton2 (LARGE and up, no MEDIUM)
*/
instanceType: InstanceType;
// Validate against service limits
if (props.timeout && props.timeout.toSeconds() > 900) {
throw new Error('Lambda timeout cannot exceed 900 seconds');
}
// Use service-specific defaults
const monitoringInterval = props.enableEnhancedMonitoring
? props.monitoringInterval || Duration.seconds(60) // RDS default
: undefined;
export type ProjectEnvironment = EnvironmentConfig & {
// ... existing configs
myResource?: Partial<MyResourceProps>;
};
export const integrationEnvironments: ProjectEnvironment[] = [
{
...devEnv,
myResource: {
resourceName: 'my-resource-dev',
},
},
];
import {MyResourceStack} from '../examples/<service>/stacks/my-resource-stack';
integrationEnvironments.forEach(env => {
// ... existing stacks
if (env.myResource) {
new MyResourceStack(app, `my-resource-${env.name}`, envProps);
}
});
Omit<> type wrappersSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer