AWS CDK: Boilerplating to Build Node.js Apps in TypeScript
Tags: cdk, lambda, aws, typescript
Published: 2020-06-27 11:31:56


# AWS CDK: Boilerplating to Build Node.js Apps in TypeScript

 [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/latest/guide/home.html) enables provisioning of infrastructure using traditional programming languages you're famiilar with including TypeScript and Python. When I first started building in CDK, I remember looking for some quick start boilerplate code to quickly get started and build a serverless application in Node.js (TypeScript).

However, some issues I encountered trying gettings started includes:

 - There wasn't many templates to start with
 - Many code examples were in JavaScript and not TypeScript
 - There were also some unique issues around bundling and deployment which I wished I knew about earlier.

Here's how my projects are setup to get me up and running quickly. Here's the  [GitHub](https://github.com/JasonLyy/cdk-ts-boilerplate) for a TL;DR and a basic example for what will be described below.

## Getting Started 
Run this command inside a empty directory to setup a CDK project in that folder:

    cdk init --language typescript

While we are here, lets setup some Linting and Formatting using ESLint and Prettier ([Credits](https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project))

    npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
    npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier
 
 Lets update `package.json` for linting by adding `"lint": "eslint '*/**/*.{js,ts}' --quiet --fix"` to `scripts`.
 
 Lets also create a `.eslintrc.js` file: 
 
    module.exports = {
	    parser: "@typescript-eslint/parser", // Specifies the ESLint parser
	    parserOptions: {
	      ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
	      sourceType: "module", // Allows for the use of imports
	    },
	    extends: [
	      "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
	      "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
	      "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
	    ],
	    rules: {
	      // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
	      // e.g. "@typescript-eslint/explicit-function-return-type": "off",
	    },
    };
   And finally, lets create a `.prettierrc.js` file:
  
    module.exports = {
	    semi: true,
	    trailingComma: "all",
	    singleQuote: true,
	    printWidth: 120,
	    tabWidth: 4
    };
  
## Quick Project Refactor
Now I personally don't like how the CDK project is initially structured so I refactored it to distinctly seperate infrastructure code and actual code. 

Lets create a new folder called `infra` which will hold all the infrastructure code. We will move the file in `bin`to `infra` (I also renamed it to `app.ts`) and the file in `lib` to `infra`. Once we've done that we can remove both the `bin` and `lib` folder.

Go to `cdk.json` and update `"app": "npx ts-node bin/[your_orignal_file_in_bin].ts"` to `"app": "npx ts-node infra/app.ts"`.

Now the actual code and other non-infrastructure related files will be located in `src`. So lets create a `src` folder. Inside that `src` folder, lets also create a folder called `lambda` which will hold all lambda functions (if the project uses Lambda). Each Lambda will be represented inside that folder as `lambda_name/index.ts`.

While we are here, lets also update the `.gitignore` for later by removing `*.js`, and adding `dist`.

## Setting Up CDK Deploy in package.json
CDK will throw you an error if you try to deploy with mismatching versions of `aws-cdk` or if CDK dependencies you use have different versions (e.g. `"@aws-cdk/aws-sns": "^1.47.0"` and `"@aws-cdk/aws-lambda": "^1.46.0"`). This can cause some issues in some CI/CD pipelines where you have to run `npm install -g aws-cdk`.

A quick fix to those issues is to add something like this to your `package.json` file: `"deploy": "cdk deploy your_stack_name"` to ensure that it uses the CDK defined in your `package.json` already. Also everytime you deploy, you must ensure that any CDK dependencies are also of the same version.

## CDK Bundling

Although CDK intrastructure is built in TypeScript, it is actually executed using `ts-node` to generate the infrastructure CloudFormation. However, CDK by default does not transpile TypeScript to JavaScript or bundles code together (even if you're just using JavaScript). This can become a problem if you're building your Lambdas in TypeScript or your Lambda may reference other files in different directories or require other files.

Recently, CDK released the [aws-lambda-nodejs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html) construct which solves the exact problem described above. However, this module is still considered as 'experimental' and under 'active development'. Another solution is to use Webpack to transpile and bundle TypeScript.

To get started with [Webpack](https://webpack.js.org/) lets install some dependencies.  To get started lets run: 

	npm install --save-dev webpack webpack-cli rimraf builtin-modules ts-loader

`rimraf` is used to delete transpiled files and other CDK generated files which we might want everytime we run `npm run build`. `builtin-modules` will be used later to reduce deployment sizes. `ts-loader` is used to transpile TypeScript.

Now add this line below to your `package.json` scripts:

	"build": "rimraf dist && webpack"

This will remove `dist` where the transpiled files will go to later via webpack before running webpack.

Now lets create a `webpack.config.js` file with the following settings: 

	const path = require('path');
	const fs = require('fs');
	const nodeBuiltins = require('builtin-modules');

	const lambdaDir = 'src/lambda';
	const lambdaNames = fs.readdirSync(path.join(__dirname, lambdaDir));

	const entry = lambdaNames.reduce((entryPoints, lambdaName) => {
	    const tsPath = path.join(__dirname, lambdaDir, `${lambdaName}/index.ts`);
	    const jsPath = path.join(__dirname, lambdaDir, `${lambdaName}/index.js`);

	    const isTsFile = fs.existsSync(tsPath);
	    const isJsFile = fs.existsSync(jsPath);
	    if (isTsFile) {
	        entryPoints[lambdaName] = tsPath;
	    } else if (isJsFile) {
	        entryPoints[lambdaName] = jsPath;
	    }

	    return entryPoints;
	}, {});

	const externals = ['aws-sdk'].concat(nodeBuiltins).reduce((externals, moduleName) => {
	    externals[moduleName] = moduleName;
	    return externals;
	}, {});

	module.exports = {
	    entry,
	    externals,
	    module: {
	        rules: [
	            {
	                test: /\.ts$/,
	                use: {
	                    loader: 'ts-loader',
	                    options: { onlyCompileBundledFiles: true },
	                },
	                exclude: /node_modules/,
	            },
	        ],
	    },
	    output: {
	        path: path.join(__dirname, 'dist', lambdaDir),
	        libraryTarget: 'commonjs',
	        filename: '[name]/index.js',
	    },
	    target: 'node',
	    optimization: {
	        minimize: false,
	    },
	    devtool: 'inline-cheap-module-source-map',
	};

This webpack is configured to bundle for each Lambda (the entry point) based on the following pattern: `src/lambda/[lamba_name]/index.ts`. It will also support Lambdas which are not TypeScript by also looking for `src/lambda/[lamba_name]/index.js`. TypeScript Lambdas will be bundled via `ts-loader`.
 
In the Lambda Execution Environment, it will already have `aws-sdk` and other built in node modules so they will be excluded from the bundle (to reduce deployment size).

The bundle will be outputted to the `dist` folder in the exact same pattern as above. Also minimisation is disabled and [source mapping](https://webpack.js.org/configuration/devtool/) using `inline-cheap-module-source-map` is enabled (to map transpiled code back to TypeScript lines) as it is running on the backend.

To get Lambda to work with source mapping you must include source map support in every Lambda To do this, you first must install `source-map-support` by running:

    npm install source-map-support
Then for each Lambda you must add `import  'source-map-support/register';` to the top of every file.

# Conclusion
As you develop with CDK, you'll find that your project components and structure varies depending on your needs. I hope this gives you a good starting point to get something running when working with a relatively new tool like CDK.