Configure or update AWS CDK TypeScript projects with an opinionated layout, required dependencies, scripts, and .env usage. Use when a repo should contain a CDK project. CDK app code should live in a `cdk/` folder off the root of the project with strict dependency and file structure conventions.
This is an opinionated view for how AWS CDK projects should be configured and maintained.
Before proceeding:
jeff-skill-install-nodejs skill.jeff-skill-install-prettier skill.cdk/ at repository root.envcdk/ in the project rootcdk/bin/ contains only a single app entrypoint (e.g., app.ts)cdk/lib/ typically contains a single stack file (e.g., stack.ts) (if you're explicitly asked to do something different, get confirmation about it first)cdk/lib/ (or cdk/lib/constructs if the project is huge with 30+ constructs and has multiple stacks)cdk/.envcdk/.env.exampleThese must be used for common build/deploy arguments instead of hardcoding in the code (e.g., API Gateway rate limits, AWS account, alarm periods, allowed Cognito origins, etc.).
Use vitest for unit tests
Use vitest.config.ts for test configurations (like coverage thresholds, test environment, etc...)
Unit tests must exist for all code. Include coverage reports and ensure coverage is at least 80%
Tests must assert configuration expectations, such as:
Use a dedicated test folder to scale cleanly:
cdk/test/ for unit testscdk/test/constructs/ for construct testscdk/test/stack/ for stack-level assertionsExample stack.test.ts:
import { describe, it, expect } from 'vitest';
import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { MyAppStack } from '../../lib/stack';
describe('MyAppStack', () => {
it('applies API Gateway rate limits', () => {
const app = new App();
const stack = new MyAppStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::ApiGateway::Stage', {
MethodSettings: [
{
ThrottlingRateLimit: 10,
ThrottlingBurstLimit: 20
}
]
});
});
});
Template.fromStack for deterministic assertions of CFN configlib/ and bin/ to avoid noiseOnly these are required by default. Any additional pinned dependency must be scrutinized or confirmed with the user.
Use a WebSearch to replace <latest stable> below with the actual latest version numbers. Do not skip or guess at version numbers, always use the WebSearch to verify.
"dependencies": {
"aws-cdk-lib": "<latest stable>",
"constructs": "<latest stable>",
"dotenv": "<latest stable>"
},
This is less strict but still be deliberate and do not introduce unnecessary dependencies.
Also always include source-map-support as a devDependencies for better error stack traces in CDK apps, it's only used for test/dev situations and won't be part of the production build so it should be a dev dependency:
"devDependencies": {
...
"source-map-support": "<latest stable>"
...
}
Prefer npx cdk or local cdk scripts; do not rely on global install.
The scripts section in package.json should include helpful commands like the following (always include a command that will build + deploy in one step, and a command that will clean + build + deploy in one step):
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"deploy": "npm run build && cdk deploy",
"diff": "cdk diff",
"synth": "cdk synth",
"destroy": "cdk destroy",
"clean": "rm -rf dist",
"clean:all": "rm -rf dist node_modules",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
App Entrypoint (cdk/bin/app.ts)
Only one app entrypoint is allowed. It should follow this pattern:
import * as cdk from 'aws-cdk-lib';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { MyAppStack } from '../lib/stack';
// This works whether you run from repo root or cdk/.
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
const app = new cdk.App();
new MyAppStack(app, 'MyAppStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
// ...other args too
}
});
Only one stack file is typical, but leave room for more if needed or requested (but always double check if really necessary as that is non-standard).
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class MyAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// refer to constructs part of the stack
}
}
Each construct should be in its own file.
Example lambda construct:
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
interface Lambda1ConstructProps {
myTable: dynamodb.Table;
}
export class Lambda1Construct extends Construct {
constructor(scope: Construct, id: string, props: Lambda1ConstructProps) {
super(scope, id);
}
}
Example DynamoDB construct:
import { Construct } from 'constructs';
export class DynamoDBConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// tables, indexes, etc...
}
}
It's ok to combine multiple Lambda functions in the same Lambda construct file if they are closely related in business purpose. Same idea applies to other resources (multiple DynamoDB tables can all live in the same construct file if they are closely related in business purpose). But typically do not mix resources in a single construct file (like mixing Lambda and DynamoDB resources in the same construct file).