Production-Ready CDK - Project Structure

Production-Ready CDK - Project Structure

In my last post, I announced I am starting a new blog post series on Cloud Development Kits. You can find more about my purpose & plan here.

In this first chapter of the series, we will begin building our project hands-on while explaining the tooling and the decisions.

The primary tool of this post is Projen! In the CDK community, it is a popular tool these days. But for others, it might be the first time they hear it.

Projen is a project configuration management tool. To make it more concrete, what AWS CDK is to AWS is, what Projen is to your Git Project. So as they call it: it is a CDK for software projects. We can manage all project configuration from a simple, single Javascript file and synthesize project files such as package.json, .gitingore, .eslintrc.json.

The concept sounds familiar. We have seen and used other utility tools like Cookiecutter or Yeoman before. The main issue with those tools is they are for templating, only for one use. After weeks/months of using those tools, your projects will look very different, and there is nothing to do about it. Whereas with Projen, we can create the project and keep managing and configuring it actively since it is not one-time use.

Why is it popular with the CDK community, and how did the project start? Because Mr. Elad Ben-Israel, the leading creator of AWS CDK, started Projen and showcased it at the first CDK Day in 2020. Then the project grew quickly and almost became the new standard for the CDK projects. While writing this, I saw that it even became an AWS project.

My personal experience and why do I prefer it?

I see that after starting using AWS CDK, the number of AWS CDK projects usually increases sharply after some time. I once worked in an environment where we had +50 AWS CDK repositories. The configurations, pipelines, versioning were all over the place. We fixed it and made them look similar, but not all the projects were developed or maintained at the same rate. As the months passed, we had the same issue, and we didn’t have a clever and consistent way of managing our projects.

I also tried templating engines, mostly Cookiecutter, years ago. But unfortunately, the template becomes obsolete rapidly, and almost always, people are not on the same page regarding the project configuration. Besides that, I tried the Bedrock Pattern but didn’t find it applicable to my projects.

Plus, it is hard to get correct and consistent with the project structure. There are so many things to think about. To make it more concrete, here is the list of files/features we usually need from a Typescript project, which is a lot:

  • The heart of the project: package.json
  • Typescript Compiler configuration: tsconfig.json
  • Dependencies
  • Linter
  • Unit testing & coverage
  • Version bumps & changelog
  • CI builds
  • Automated releases
  • Security patches
  • License
  • Npm workflow scripts

Luckily the opinionated projects that come ready with Projen contain months of experience, trial, and error. For me, Projen solved the problems I mentioned and made our configuration management much more straightforward, thanks to these. Lastly, although I highly recommend it, I should warn you it might sometimes be challenging to fix errors because it is a pretty new tool, and the community is not at its peak yet. So we need a bit of patience, that’s all.


Enough with the story; let’s start with the implementation. Here are the prerequisites to be able to use AWS CDK and Projen:

  • AWS Account & IAM User or Role that you can assume
  • Node.js: recommend version 16; version 14 should also be fine
  • IDE of your choice
  • Git

I assume you configured all and AWS CLI & git & npm(from Node.js installation) working as expected. So let’s execute the following commands to create the project:

$ mkdir prod-ready-cdk && cd prod-ready-cdk
$ git init
$ npx projen new awscdk-app-ts

Projen file

Now, we have a project ready to be detailed. First, we will be working with the .projenrc.js file to configure the project. It should look like this first:

const { AwsCdkTypeScriptApp } = require('projen');
const project = new AwsCdkTypeScriptApp({
  cdkVersion: '1.95.2',
  defaultReleaseBranch: 'main',
  name: 'prod-ready-cdk',

  // cdkDependencies: undefined,  /* Which AWS CDK modules (those that start with "@aws-cdk/") this app uses. */
  // deps: [],                    /* Runtime dependencies of this module. */
  // description: undefined,      /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],                 /* Build dependencies for this module. */
  // packageName: undefined,      /* The "name" in package.json. */
  // release: undefined,          /* Add release management to this project. */

See, it comes up with AWS CDK v1. We need to change the CDK version to v2 and provide more fields. (Warning: I needed to change the first two lines as well)

const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
  authorAddress: '',
  authorName: 'Kemal Cagin Gulsen',
  cdkVersion: '2.8.0',
  defaultReleaseBranch: 'main',
  name: 'prod-ready-cdk',
  description: 'A CDK project for my blog posts',
  repositoryUrl: '',
  keywords: [
    'AWS CDK',

  // cdkDependencies: undefined,  /* Which AWS CDK modules (those that start with "@aws-cdk/") this app uses. */
  // deps: [],                    /* Runtime dependencies of this module. */
  // description: undefined,      /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],                 /* Build dependencies for this module. */
  // packageName: undefined,      /* The "name" in package.json. */
  // release: undefined,          /* Add release management to this project. */

For changes to take effect, we need to rerun Projen:

$ npx projen

You can see the changes in the package.json, mainly for the AWS CDK dependencies. With CDK v2, we don’t need to add dependencies per AWS Service, "aws-cdk-lib": "^2.8.0" is all we need.

Furthermore, you might need to bootstrap AWS CDK again using the cdk bootstrap command since CDK v2 uses the modern bootstrap stack. This modern way will help us because the modern bootstrap stack is a prerequisite for CDK Pipelines.

Next, we synthesize the CDK app using the command:

$ npx projen synth

instead of cdk synth. But, we see that it doesn’t work since we changed the CDK version. So, let’s update the dependencies and add a Hello World Lambda while on src/main.ts.

Tip: instead of using npx projen ... every time, we can have an alias like alias pj="npx projen" to make it shorter.

AWS CDK App: A Hello World Lambda Function

Let’s use src/main.ts for our Lambda stack for now and refactor it in the next episodes. After I fix the imports and add the lambda function, it looks like this:

import { App, Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class LambdaStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps = {}) {
    super(scope, id, props);

    new lambda.Function(this, 'ExampleFunction', {
      functionName: 'example-lambda',
      code: lambda.Code.fromAsset('lambda'),
      handler: 'hello.handler',
      runtime: lambda.Runtime.NODEJS_14_X,

// for development, use account/region from cdk cli
const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,

const app = new App();

new LambdaStack(app, 'lambda-stack-dev', { env: devEnv });
// new LambdaStack(app, 'lambda-stack-prod', { env: prodEnv });


And of course, add our Lambda source code, lambda/hello.js.

exports.handler = function(event, context) {
  console.log('Hello, Cloudwatch!');
  context.succeed('Hello, World!');

Then finally, after we npx projen synth (or shorter, pj synth), we will have the smallest AWS CDK App ready to be deployed. You know the drill; then we do npx projen deploy and check the lambda created on AWS.


Final touches, let’s add our first unit test.

import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { LambdaStack } from '../src/main';

test('Lambda created', () => {
  const app = new cdk.App();
  const stack = new LambdaStack(app, 'LambdaStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::Lambda::Function', 1);

To run tests, we can use the command npx projen test.

Watch mode

It is a feature that can be very handy when writing CDK code. AWS CDK Team introduced this mode with AWS CDK v2. Every time we save a file and change the synthesized cdk output, the watch mode calls cdk deploy command. Therefore, our stacks deployed on AWS reflect our code, and we don’t need to use cdk/projen commands every time we deploy. As a result, we save time, and deployments are faster for the development environment.

To enable it, we can use npx projen watch.

For other commands, you can check package.json. And for the list of options, you can check the API Reference. However, these days we have a more good-looking option in the Construct Hub.

Github Project

After configuring the Projen file and AWS CDK App, we can create a new repository on Github and push the code to the repository. I have mine: cagingulsen/prod-ready-cdk. Then used the following commands:

$ git remote add origin
$ git push -u origin main

Then we can commit the latest changes and push them to Github. You can check the code here.

We made the introduction for our CDK journey and mainly focused on Projen. But of course, this is not the final version of our project configuration. We will add more settings and use more Projen features in the future to ramp up. And indeed, we will have more CDK Constructs than just a Lambda Function.

Thank you for your time, and see you in the upcoming post on CDK Pipelines. Cheers!