Production ready Typescript Serverless project configuration
Tags: typescript, tutorial, serverless, lambda
Published: 2021-03-16 12:54:17

Before we get started - if you're just here because you want a plug 'n' play starter, [here's a project generator](https://www.npmjs.com/package/generator-sls-node) and [here's an example repository](https://github.com/TastefulElk/serverless-typescript-starter-example)! 🚀

Getting Typescript to run is usually pretty simple. You just follow a few lines of instructions from the first google link. Getting the project from that point to something that you can deploy with confidence is often surprisingly hard though. To get there, you'll at the very least want to have testing and linting set up. Tree shaking is another thing you'll probably want to be able to drastically reduce your bundle sizes. 

This is going to be a walkthrough of my opinionated approach to achieving a simple but extensible setup that accomplishes these simple goals for a Serverless AWS project.

### Project Initialization

Let's start off by creating a new project.  Make sure you're in an empty directory and run `npm init -y`. We're going to need a few tools to set this up: 

- [Serverless](https://www.npmjs.com/package/serverless) & [Typescript](https://www.npmjs.com/package/typescript)
- [ESLint](https://www.npmjs.com/package/eslint) for linting
- [Jest](https://www.npmjs.com/package/jest) for testing
- [Webpack](https://www.npmjs.com/package/webpack) for bundling and optimizing
- [ts-loader](https://www.npmjs.com/package/ts-loader) as a Typescript loader for webpack
- [ts-jest](https://www.npmjs.com/package/ts-jest) as a Typescript preprocessor for jest

Let's add those as devDependencies right of the bat as well

```bash
yarn add eslint jest @types/jest serverless serverless-webpack ts-jest ts-loader typescript webpack -D
```

### Application Code & Tests

We'll need some example Typescript code to run in our app. I prefer placing the application code in an `src` folder and the test code in a `tests` folder to keep them separate.

```ts
// src/myFunction.ts
export const handler = async (event: any, context: any) => {
  return 'Hello from handler!';
};
```

```ts
// tests/myFunction.test.ts
import { handler } from '../src/myFunction';

describe('[myFunction]', () => {
  it('should not crash', async () => {
    await handler({}, {});
  });
});
```

In order to transpile and run the Typescript code, we'll need a `tsconfig.json`. We can generate one to start from by running `yarn tsc --init`. Let's change both the `target` and `lib` to `"ES2020"` as we want to be able to use modern ECMAScript features. 

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "lib": ["ES2020"], /* Specify library files to be included in the compilation. */
    "strict": true, /* Enable all strict type-checking options. */
    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "skipLibCheck": true, /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  }
}
```

All we need now to be able to start developing and testing our app is to add a Jest configuration file. The default settings for Jest are pretty sensible to we don't have to care about most of those right now except for setting `preset` to `"ts-jest"` and the `testEnvironment` to `"node"`. 

```js
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};
```

We can now finally run our tests with `yarn jest` to confirm that everything seems to work so far.

![passing tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1udyla0i9fisuepzh54.png)

Excellent, our tests are passing! 🎉

### Linting

We'll be using ESLint for static code analysis and code style enforcement. ESLint configurations can quickly become a mess and it's by nature very subjective so let's keep it simple here. When we run `yarn eslint --init` we'll get to answer a few questions about our preferences. In the image below I chose to create a small custom set of rules rather than using one of the "popular style guides" but it's completely up to you.

![ESLint init wizard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6bewn3nzjyp9wux9ifs8.png)

This is how mine ended up looking:

```js
// .eslintrc.js
module.exports = {
  env: {
    es2021: true,
    node: true,
  },
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint'],
  rules: {
    indent: ['error', 2],
    'linebreak-style': ['error', 'unix'],
    quotes: ['error', 'single'],
    semi: ['error', 'always'],
  },
  ignore: ['webpack.config.js'],
};
```

The AirBnb style guide has a few conflicts with our Typescript setup out of the box so if you choose to extend AirBnb style guide you'll have to do these steps as well:

- add `"jest"` to the `env` configuration
- add `'plugin:import/typescript'` to `extends` & `import` to `plugins` to get rid of module resolution errors
- disable explicit file extension rule by adding `'import/extensions': 'off'` to `rules`

### Packaging & Deploying

Lastly, we need to configure Webpack and Serverless in order to build and deploy our app. For the Webpack configuration, we can use a pretty simple setup where we specify `ts-loader` to be used as the loader for Typescript files.

```js
// webpack.config.js
const path = require('path');
const slsw = require('serverless-webpack');

module.exports = {
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  resolve: {
    extensions: ['.ts', 'tsx']
  },
  target: 'node',
  module: {
    rules: [
      {
        test: /\.(tsx?)$/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
      },
    ],
  },
};
```

and finally, for the Serverless configuration we'll, in addition to some basic information about our app, add the following:

- `serverless-webpack` to the list of plugins
- Set `package.individually` to `true` in order to bundle each function separately so that we can reduce the bundle size for each function by only including exactly what's needed to run it
- As we've been using Yarn we should set the Webpack packager to `yarn` so that it'll respect our lock file.

```yaml
// serverless.yml
service: sls-ts

plugins:
  - serverless-webpack

package:
  individually: true

custom:
  webpack:
    packager: yarn

provider:
  name: aws
  region: eu-north-1
  runtime: nodejs14.x

functions:
  myFunction:
    handler: src/myFunction.handler
```

That's it! Hit `yarn serverless deploy` to deploy the project! We can verify that it works by running `yarn serverless invoke -f myFunction`

![invoked function responded successfully](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pdjmgkyx91z75ox0658s.png)

## Wrapping up

Project configuration is highly subjective and the complexity often grows as the projects grows but I hope this can serve as a starting point to build upon!

If you enjoyed this guide and want to see more, follow me on Twitter at [@TastefulElk](https://twitter.com/tastefulelk) where I frequently write about serverless tech, AWS and developer productivity!

Happy hacking! 🚀

#### Resources

- [Project generator](https://www.npmjs.com/package/generator-sls-node)
- [Example repository](https://github.com/TastefulElk/serverless-typescript-starter-example)