Repository: sid88in/serverless-appsync-plugin
Pull request #614 (2023-09-22 01:16:41)

fix: use global caching TTL by default in resolver caching config
This fixes a regression introduced in `v2` where the per-resolver caching TTL defaults to `3600` even when a global caching TTL is defined.


Patch:
diff --git a/doc/dataSources.md b/doc/dataSources.md
index f52add7..7efff2e 100644
--- a/doc/dataSources.md
+++ b/doc/dataSources.md
@@ -131,7 +131,7 @@ appSync:
   dataSources:
     myEventBus:
       type: 'AMAZON_EVENTBRIDGE'
-      confing:
+      config:
         eventBusArn: !GetAtt MyEventBus.Arn
 ```
 
diff --git a/doc/general-config.md b/doc/general-config.md
index e91972d..eb9400a 100644
--- a/doc/general-config.md
+++ b/doc/general-config.md
@@ -51,6 +51,7 @@ appSync:
 - `xrayEnabled`: Boolean. Enable or disable X-Ray tracing.
 - `visibility`: Optional. `GLOBAL` or `PRIVATE`. **Changing this value requires the replacement of the API.**
 - `tags`: A key-value pair for tagging this AppSync API
+- `esbuild`: Custom esbuild options, or `false` See [Esbuild](#Esbuild)
 
 ## Schema
 
@@ -186,3 +187,22 @@ appSync:
 - `excludeVerboseContent`: Boolean, Optional. Exclude or not verbose content (headers, response headers, context, and evaluated mapping templates), regardless of field logging level. Defaults to `false`.
 - `retentionInDays`: Optional. Number of days to retain the logs. Defaults to [`provider.logRetentionInDays`](https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml#general-function-settings).
 - `roleArn`: Optional. The role ARN to use for AppSync to write into CloudWatch. If not specified, a new role is created by default.
+
+## Esbuild
+
+By default, this plugin uses esbuild in order to bundle Javascript resolvers. TypeScript files are also transpiled into compatible JavaScript. This option allows you to pass custom options that must be passed to the esbuild command.
+
+⚠️ Use these options carefully. Some options are not compatible with AWS AppSync. For more details about using esbuild with AppSync, see the [official guidelines](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#additional-utilities)
+
+Set this option to `false` to disable esbuild completely. You code will be sent as-is to AppSync.
+
+Example:
+
+Override the target and disable sourcemap.
+
+```yml
+appSync:
+  esbuild:
+    target: 'es2020',
+    sourcemap: false
+```
diff --git a/doc/resolvers.md b/doc/resolvers.md
index bc9d20a..7e3d714 100644
--- a/doc/resolvers.md
+++ b/doc/resolvers.md
@@ -33,11 +33,15 @@ appSync:
 - `caching`: [See below](#Caching)
 - `sync`: [See SyncConfig](syncConfig.md)
 
-## JavaScript vs VTL
+## JavaScript, VTL, or Direct Lambda
 
-When `code` is specified, the JavaScript runtime is used. When `request` and/or `response` are specified, the VTL runtime is used.
+When `code` is specified, the JavaScript runtime is used.
 
-If neither are specified, by default, the resolver is a PIPELINE JavaScript resolver, and the following minimalistic resolver handler is used.
+When `request` and/or `response` are specified, the VTL runtime is used.
+
+For [direct lambda](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html), set `kind` to `UNIT` and don't specify `request`, `response` or `code`. This only works with Lambda function data sources.
+
+If nothing is specified, by default, the resolver is a PIPELINE JavaScript resolver, and the following minimalistic code is used for the `before` and `after` handlers.
 
 ```js
 export function request() {
@@ -49,7 +53,86 @@ export function response(ctx) {
 }
 ```
 
-To use [direct lambda](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html), set `kind` to `UNIT` and don't specify `request` and `response` (only works with Lambda function data sources).
+Example of a UNIT JavaScript resolver.
+
+```yaml
+appSync:
+  resolvers:
+    Query.user:
+      kind: UNIT
+      dataSource: myDataSource
+      code: getUser.js
+```
+
+## Bundling
+
+AppSync requires resolvers to be bundled in one single file. By default, this plugin bundles your code with [esbuild](https://esbuild.github.io/), using the given path as the entry point.
+
+This means that you can import external libraries and utilities. e.g.
+
+```js
+import { Context, util } from '@aws-appsync/utils';
+import { generateUpdateExpressions, updateItem } from '../lib/helpers';
+
+export function request(ctx) {
+  const { id, ...post } = ctx.args.post;
+
+  const item = updateItem(post);
+
+  return {
+    operation: 'UpdateItem',
+    key: {
+      id: util.dynamodb.toDynamoDB(id),
+    },
+    update: generateUpdateExpressions(item),
+    condition: {
+      expression: 'attribute_exists(#id)',
+      expressionNames: {
+        '#id': 'id',
+      },
+    },
+  };
+}
+
+export function response(ctx: Context) {
+  return ctx.result;
+}
+```
+
+For more information, also see the [esbuild option](./general-config.md#Esbuild).
+
+## TypeScript support
+
+You can write JS resolver in TypeScript. Resolver files with the `.ts` extension are automatically transpiled and bundled using esbuild.
+
+```yaml
+resolvers:
+  Query.user:
+    kind: UNIT
+    dataSource: 'users'
+    code: 'getUser.ts'
+```
+
+```ts
+// getUser.ts
+import { Context, util } from '@aws-appsync/utils';
+
+export function request(ctx: Context) {
+  const {
+    args: { id },
+  } = ctx;
+  return {
+    operation: 'GetItem',
+    key: util.dynamodb.toMapValues({ id }),
+  };
+}
+
+export function response(ctx: Context) {
+  return ctx.result;
+}
+```
+
+For more information, also see the [esbuild option](./general-config.md#Esbuild).
 
 ## PIPELINE resolvers
 
@@ -80,6 +163,7 @@ You can even also define the Lambda function definition inline under the dataSou
 appSync:
   resolvers:
     Query.user:
+      kind: UNIT
       dataSource:
         type: 'AWS_LAMBDA'
         config:
diff --git a/package-lock.json b/package-lock.json
index 621ee40..06656a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "ajv-merge-patch": "^5.0.1",
         "aws-sdk": "^2.1265.0",
         "chalk": "^4.1.2",
+        "esbuild": "^0.17.11",
         "globby": "^11.1.0",
         "graphql": "^16.6.0",
         "lodash": "^4.17.21",
@@ -42,7 +43,6 @@
         "serverless": "^3.25.1",
         "ts-jest": "^27.1.5",
         "ts-node": "^10.9.1",
-        "ts-toolbelt": "^9.6.0",
         "typescript": "^4.9.3"
       },
       "peerDependencies": {
@@ -683,6 +683,336 @@
         "@jridgewell/sourcemap-codec": "^1.4.10"
       }
     },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz",
+      "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz",
+      "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz",
+      "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz",
+      "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz",
+      "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz",
+      "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz",
+      "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz",
+      "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz",
+      "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz",
+      "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz",
+      "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==",
+      "cpu": [
+        "loong64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz",
+      "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz",
+      "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz",
+      "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz",
+      "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.11.tgz",
+      "integrity": "sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz",
+      "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz",
+      "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz",
+      "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz",
+      "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz",
+      "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz",
+      "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/@eslint/eslintrc": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
@@ -4645,6 +4975,42 @@
         "es6-symbol": "^3.1.1"
       }
     },
+    "node_modules/esbuild": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz",
+      "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==",
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.17.11",
+        "@esbuild/android-arm64": "0.17.11",
+        "@esbuild/android-x64": "0.17.11",
+        "@esbuild/darwin-arm64": "0.17.11",
+        "@esbuild/darwin-x64": "0.17.11",
+        "@esbuild/freebsd-arm64": "0.17.11",
+        "@esbuild/freebsd-x64": "0.17.11",
+        "@esbuild/linux-arm": "0.17.11",
+        "@esbuild/linux-arm64": "0.17.11",
+        "@esbuild/linux-ia32": "0.17.11",
+        "@esbuild/linux-loong64": "0.17.11",
+        "@esbuild/linux-mips64el": "0.17.11",
+        "@esbuild/linux-ppc64": "0.17.11",
+        "@esbuild/linux-riscv64": "0.17.11",
+        "@esbuild/linux-s390x": "0.17.11",
+        "@esbuild/linux-x64": "0.17.11",
+        "@esbuild/netbsd-x64": "0.17.11",
+        "@esbuild/openbsd-x64": "0.17.11",
+        "@esbuild/sunos-x64": "0.17.11",
+        "@esbuild/win32-arm64": "0.17.11",
+        "@esbuild/win32-ia32": "0.17.11",
+        "@esbuild/win32-x64": "0.17.11"
+      }
+    },
     "node_modules/escalade": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -14522,12 +14888,6 @@
         "node": ">=0.3.1"
       }
     },
-    "node_modules/ts-toolbelt": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
-      "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
-      "dev": true
-    },
     "node_modules/tsconfig-paths": {
       "version": "3.14.1",
       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -15918,6 +16278,138 @@
         }
       }
     },
+    "@esbuild/android-arm": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz",
+      "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==",
+      "optional": true
+    },
+    "@esbuild/android-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz",
+      "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==",
+      "optional": true
+    },
+    "@esbuild/android-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz",
+      "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==",
+      "optional": true
+    },
+    "@esbuild/darwin-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz",
+      "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==",
+      "optional": true
+    },
+    "@esbuild/darwin-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz",
+      "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==",
+      "optional": true
+    },
+    "@esbuild/freebsd-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz",
+      "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==",
+      "optional": true
+    },
+    "@esbuild/freebsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz",
+      "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==",
+      "optional": true
+    },
+    "@esbuild/linux-arm": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz",
+      "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==",
+      "optional": true
+    },
+    "@esbuild/linux-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz",
+      "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==",
+      "optional": true
+    },
+    "@esbuild/linux-ia32": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz",
+      "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==",
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz",
+      "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==",
+      "optional": true
+    },
+    "@esbuild/linux-mips64el": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz",
+      "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==",
+      "optional": true
+    },
+    "@esbuild/linux-ppc64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz",
+      "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==",
+      "optional": true
+    },
+    "@esbuild/linux-riscv64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz",
+      "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==",
+      "optional": true
+    },
+    "@esbuild/linux-s390x": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz",
+      "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==",
+      "optional": true
+    },
+    "@esbuild/linux-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.11.tgz",
+      "integrity": "sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==",
+      "optional": true
+    },
+    "@esbuild/netbsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz",
+      "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==",
+      "optional": true
+    },
+    "@esbuild/openbsd-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz",
+      "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==",
+      "optional": true
+    },
+    "@esbuild/sunos-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz",
+      "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==",
+      "optional": true
+    },
+    "@esbuild/win32-arm64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz",
+      "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==",
+      "optional": true
+    },
+    "@esbuild/win32-ia32": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz",
+      "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==",
+      "optional": true
+    },
+    "@esbuild/win32-x64": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz",
+      "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==",
+      "optional": true
+    },
     "@eslint/eslintrc": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
@@ -19045,6 +19537,35 @@
         "es6-symbol": "^3.1.1"
       }
     },
+    "esbuild": {
+      "version": "0.17.11",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz",
+      "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==",
+      "requires": {
+        "@esbuild/android-arm": "0.17.11",
+        "@esbuild/android-arm64": "0.17.11",
+        "@esbuild/android-x64": "0.17.11",
+        "@esbuild/darwin-arm64": "0.17.11",
+        "@esbuild/darwin-x64": "0.17.11",
+        "@esbuild/freebsd-arm64": "0.17.11",
+        "@esbuild/freebsd-x64": "0.17.11",
+        "@esbuild/linux-arm": "0.17.11",
+        "@esbuild/linux-arm64": "0.17.11",
+        "@esbuild/linux-ia32": "0.17.11",
+        "@esbuild/linux-loong64": "0.17.11",
+        "@esbuild/linux-mips64el": "0.17.11",
+        "@esbuild/linux-ppc64": "0.17.11",
+        "@esbuild/linux-riscv64": "0.17.11",
+        "@esbuild/linux-s390x": "0.17.11",
+        "@esbuild/linux-x64": "0.17.11",
+        "@esbuild/netbsd-x64": "0.17.11",
+        "@esbuild/openbsd-x64": "0.17.11",
+        "@esbuild/sunos-x64": "0.17.11",
+        "@esbuild/win32-arm64": "0.17.11",
+        "@esbuild/win32-ia32": "0.17.11",
+        "@esbuild/win32-x64": "0.17.11"
+      }
+    },
     "escalade": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -26371,12 +26892,6 @@
         }
       }
     },
-    "ts-toolbelt": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
-      "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
-      "dev": true
-    },
     "tsconfig-paths": {
       "version": "3.14.1",
       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
diff --git a/package.json b/package.json
index b86c274..3b3f0e4 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "0.0.0-development",
   "description": "AWS AppSync support for the Serverless Framework",
   "main": "lib/index.js",
+  "types": "lib/types/index.d.ts",
   "repository": "https://github.com/sid88in/serverless-appsync-plugin",
   "license": "MIT",
   "files": [
@@ -33,7 +34,6 @@
     "serverless": "^3.25.1",
     "ts-jest": "^27.1.5",
     "ts-node": "^10.9.1",
-    "ts-toolbelt": "^9.6.0",
     "typescript": "^4.9.3"
   },
   "dependencies": {
@@ -45,6 +45,7 @@
     "ajv-merge-patch": "^5.0.1",
     "aws-sdk": "^2.1265.0",
     "chalk": "^4.1.2",
+    "esbuild": "^0.17.11",
     "globby": "^11.1.0",
     "graphql": "^16.6.0",
     "lodash": "^4.17.21",
diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts
index bdfc2fa..a942772 100644
--- a/src/getAppSyncConfig.ts
+++ b/src/getAppSyncConfig.ts
@@ -1,53 +1,13 @@
+import { AppSyncConfig } from './types';
 import {
   ApiKeyConfig,
-  AppSyncConfig,
+  AppSyncConfig as PluginAppSyncConfig,
   DataSourceConfig,
   PipelineFunctionConfig,
   ResolverConfig,
 } from './types/plugin';
-import { A, O } from 'ts-toolbelt';
 import { forEach, merge } from 'lodash';
 
-/* Completely replaces keys of O1 with those of O */
-type Replace<O extends object, O1 extends object> = O.Merge<
-  O,
-  O.Omit<O1, A.Keys<O>>
->;
-
-export type DataSourceConfigInput = O.Omit<DataSourceConfig, 'name'>;
-
-export type ResolverConfigInput = O.Update<
-  O.Update<
-    O.Optional<ResolverConfig, 'type' | 'field'>,
-    'dataSource',
-    string | DataSourceConfigInput
-  >,
-  'functions',
-  (string | FunctionConfigInput)[]
->;
-
-export type FunctionConfigInput = Replace<
-  { dataSource: string | DataSourceConfigInput },
-  O.Omit<PipelineFunctionConfig, 'name'>
->;
-
-export type AppSyncConfigInput = Replace<
-  {
-    schema?: string | string[];
-    apiKeys?: (ApiKeyConfig | string)[];
-    resolvers?:
-      | Record<string, ResolverConfigInput>[]
-      | Record<string, ResolverConfigInput>;
-    pipelineFunctions?:
-      | Record<string, FunctionConfigInput>[]
-      | Record<string, FunctionConfigInput>;
-    dataSources:
-      | Record<string, DataSourceConfigInput>[]
-      | Record<string, DataSourceConfigInput>;
-  },
-  O.Optional<AppSyncConfig, 'additionalAuthentications'>
->;
-
 const flattenMaps = <T>(
   input?: Record<string, T> | Record<string, T>[],
 ): Record<string, T> => {
@@ -74,7 +34,9 @@ const toResourceName = (name: string) => {
   return name.replace(/[^a-z_]/i, '_');
 };
 
-export const getAppSyncConfig = (config: AppSyncConfigInput): AppSyncConfig => {
+export const getAppSyncConfig = (
+  config: AppSyncConfig,
+): PluginAppSyncConfig => {
   const schema = Array.isArray(config.schema)
     ? config.schema
     : [config.schema || 'schema.graphql'];
@@ -129,6 +91,7 @@ export const getAppSyncConfig = (config: AppSyncConfigInput): AppSyncConfig => {
               if (typeof f === 'string') {
                 return f;
               }
+
               const name = `${toResourceName(typeAndField)}_${index}`;
               pipelineFunctions[name] = {
                 ...f,
diff --git a/src/index.ts b/src/index.ts
index 6d93c2e..89e15d1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -326,6 +326,11 @@ class ServerlessAppsyncPlugin {
       'before:appsync:domain:delete-record:run': async () =>
         this.initDomainCommand(),
       'appsync:domain:delete-record:run': async () => this.deleteRecord(),
+      finalize: () => {
+        writeText(
+          '\nLooking for a better AppSync development experience? Have you tried GraphBolt? https://graphbolt.dev',
+        );
+      },
     };
 
     // These hooks need the config to be loaded and
diff --git a/src/resources/JsResolver.ts b/src/resources/JsResolver.ts
index 3e1bca8..0cc7848 100644
--- a/src/resources/JsResolver.ts
+++ b/src/resources/JsResolver.ts
@@ -2,6 +2,7 @@ import { IntrinsicFunction } from '../types/cloudFormation';
 import fs from 'fs';
 import { Substitutions } from '../types/plugin';
 import { Api } from './Api';
+import { buildSync } from 'esbuild';
 
 type JsResolverConfig = {
   path: string;
@@ -18,8 +19,47 @@ export class JsResolver {
       );
     }
 
-    const requestTemplateContent = fs.readFileSync(this.config.path, 'utf8');
-    return this.processTemplateSubstitutions(requestTemplateContent);
+    return this.processTemplateSubstitutions(this.getResolverContent());
+  }
+
+  getResolverContent(): string {
+    if (this.api.config.esbuild === false) {
+      return fs.readFileSync(this.config.path, 'utf8');
+    }
+
+    // process with esbuild
+    // this will:
+    // - Bundle the code into one file if necessary
+    // - Transpile typescript to javascript if necessary
+    const buildResult = buildSync({
+      target: 'esnext',
+      sourcemap: 'inline',
+      sourcesContent: false,
+      treeShaking: true,
+      // custom config overrides
+      ...this.api.config.esbuild,
+      // These options are required and can't be changed
+      platform: 'node',
+      format: 'esm',
+      entryPoints: [this.config.path],
+      bundle: true,
+      write: false,
+      external: ['@aws-appsync/utils'],
+    });
+
+    if (buildResult.errors.length > 0) {
+      throw new this.api.plugin.serverless.classes.Error(
+        `Failed to compile resolver handler file '${this.config.path}': ${buildResult.errors[0].text}`,
+      );
+    }
+
+    if (buildResult.outputFiles.length === 0) {
+      throw new this.api.plugin.serverless.classes.Error(
+        `Failed to compile resolver handler file '${this.config.path}': No output files`,
+      );
+    }
+
+    return buildResult.outputFiles[0].text;
   }
 
   processTemplateSubstitutions(template: string): string | IntrinsicFunction {
diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts
index 249777d..e7f1102 100644
--- a/src/resources/Resolver.ts
+++ b/src/resources/Resolver.ts
@@ -31,13 +31,22 @@ export class Resolver {
       FieldName: this.config.field,
     };
 
-    const isJsResolver = !(
-      this.config.kind === 'UNIT' ||
-      'request' in this.config ||
-      'response' in this.config
-    );
-
-    if (!isJsResolver) {
+    const isVTLResolver = 'request' in this.config || 'response' in this.config;
+    const isJsResolver =
+      'code' in this.config || (!isVTLResolver && this.config.kind !== 'UNIT');
+
+    if (isJsResolver) {
+      if (this.config.code) {
+        Properties.Code = this.resolveJsCode(this.config.code);
+      } else {
+        // default for pipeline JS resolvers
+        Properties.Code = DEFAULT_JS_RESOLVERS;
+      }
+      Properties.Runtime = {
+        Name: 'APPSYNC_JS',
+        RuntimeVersion: '1.0.0',
+      };
+    } else if (isVTLResolver) {
       const requestMappingTemplates = this.resolveMappingTemplate('request');
       if (requestMappingTemplates) {
         Properties.RequestMappingTemplate = requestMappingTemplates;
@@ -58,7 +67,7 @@ export class Resolver {
       } else if (typeof this.config.caching === 'object') {
         Properties.CachingConfig = {
           CachingKeys: this.config.caching.keys,
-          Ttl: this.config.caching.ttl || this.config.caching.ttl || 3600,
+          Ttl: this.config.caching.ttl || this.api.config.caching?.ttl || 3600,
         };
       }
     }
@@ -85,18 +94,6 @@ export class Resolver {
         MaxBatchSize: this.config.maxBatchSize,
       };
     } else {
-      if (isJsResolver) {
-        if (this.config.code) {
-          Properties.Code = this.resolveJsCode(this.config.code);
-        } else if (!this.config.code) {
-          Properties.Code = DEFAULT_JS_RESOLVERS;
-        }
-        Properties.Runtime = {
-          Name: 'APPSYNC_JS',
-          RuntimeVersion: '1.0.0',
-        };
-      }
-
       Properties = {
         ...Properties,
         Kind: 'PIPELINE',
diff --git a/src/types/common.ts b/src/types/common.ts
new file mode 100644
index 0000000..b281563
--- /dev/null
+++ b/src/types/common.ts
@@ -0,0 +1,244 @@
+import { CfnWafRuleStatement, IntrinsicFunction } from './cloudFormation';
+
+export type IamStatement = {
+  Effect: 'Allow' | 'Deny';
+  Action: string[];
+  Resource: string | IntrinsicFunction | (string | IntrinsicFunction)[];
+};
+
+export type WafConfig = {
+  enabled?: boolean;
+  arn?: string;
+  name?: string;
+  defaultAction?: WafAction;
+  description?: string;
+  visibilityConfig?: VisibilityConfig;
+  rules?: WafRule[];
+};
+
+export type WafThrottleConfig =
+  | number
+  | {
+      name?: string;
+      action?: WafAction;
+      aggregateKeyType?: 'IP' | 'FORWARDED_IP';
+      limit?: number;
+      priority?: number;
+      forwardedIPConfig?: {
+        headerName: string;
+        fallbackBehavior: 'MATCH' | 'NO_MATCH';
+      };
+      scopeDownStatement?: CfnWafRuleStatement;
+      visibilityConfig?: VisibilityConfig;
+    };
+
+export type WafDisableIntrospectionConfig = {
+  name?: string;
+  priority?: number;
+  visibilityConfig?: VisibilityConfig;
+};
+
+export type WafAction = 'Allow' | 'Block';
+export type WafRuleAction = 'Allow' | 'Block' | 'Count' | 'Captcha';
+
+export type WafRuleThrottle = {
+  throttle: WafThrottleConfig;
+};
+
+export type WafRuleCustom = {
+  name: string;
+  priority?: number;
+  action?: WafRuleAction;
+  statement: CfnWafRuleStatement;
+  visibilityConfig?: VisibilityConfig;
+};
+
+export type WafRuleDisableIntrospection = {
+  disableIntrospection: WafDisableIntrospectionConfig;
+};
+
+export type WafRule =
+  | WafRuleThrottle
+  | WafRuleDisableIntrospection
+  | WafRuleCustom
+  | 'disableIntrospection'
+  | 'throttle';
+
+export type ApiKeyConfig = {
+  apiKeyId?: string;
+  name: string;
+  description?: string;
+  expiresAfter?: string | number;
+  expiresAt?: string;
+  wafRules?: WafRule[];
+};
+
+export type CognitoAuth = {
+  type: 'AMAZON_COGNITO_USER_POOLS';
+  config: {
+    userPoolId: string | IntrinsicFunction;
+    awsRegion?: string | IntrinsicFunction;
+    defaultAction?: 'ALLOW' | 'DENY';
+    appIdClientRegex?: string | IntrinsicFunction;
+  };
+};
+
+export type IamAuth = {
+  type: 'AWS_IAM';
+};
+
+export type LambdaAuth = {
+  type: 'AWS_LAMBDA';
+  config: LambdaConfig & {
+    identityValidationExpression?: string;
+    authorizerResultTtlInSeconds?: number;
+  };
+};
+
+export type OidcAuth = {
+  type: 'OPENID_CONNECT';
+  config: {
+    issuer: string;
+    clientId: string;
+    iatTTL?: number;
+    authTTL?: number;
+  };
+};
+
+export type ApiKeyAuth = {
+  type: 'API_KEY';
+};
+
+export type Auth = CognitoAuth | LambdaAuth | OidcAuth | ApiKeyAuth | IamAuth;
+
+export type DomainConfig = {
+  enabled?: boolean;
+  useCloudFormation?: boolean;
+  retain?: boolean;
+  name: string;
+  certificateArn?: string;
+  hostedZoneId?: string;
+  hostedZoneName?: string;
+  route53?: boolean;
+};
+
+export type SyncConfig = {
+  conflictDetection: 'VERSION' | 'NONE';
+  conflictHandler: 'OPTIMISTIC_CONCURRENCY' | 'AUTOMERGE' | 'LAMBDA';
+} & LambdaConfig;
+
+export type Substitutions = Record<string, string | IntrinsicFunction>;
+
+export type DsDynamoDBConfig = {
+  type: 'AMAZON_DYNAMODB';
+  config: {
+    tableName: string | IntrinsicFunction;
+    useCallerCredentials?: boolean;
+    serviceRoleArn?: string | IntrinsicFunction;
+    region?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+    versioned?: boolean;
+    deltaSyncConfig?: {
+      deltaSyncTableName: string;
+      baseTableTTL?: number;
+      deltaSyncTableTTL?: number;
+    };
+  };
+};
+
+export type DsEventBridgeConfig = {
+  type: 'AMAZON_EVENTBRIDGE';
+  config: {
+    serviceRoleArn?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+    eventBusArn: string | IntrinsicFunction;
+  };
+};
+
+export type DsRelationalDbConfig = {
+  type: 'RELATIONAL_DATABASE';
+  config: {
+    region?: string;
+    relationalDatabaseSourceType?: 'RDS_HTTP_ENDPOINT';
+    serviceRoleArn?: string | IntrinsicFunction;
+    dbClusterIdentifier: string | IntrinsicFunction;
+    databaseName?: string | IntrinsicFunction;
+    schema?: string;
+    awsSecretStoreArn: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+  };
+};
+
+export type DsOpenSearchConfig = {
+  type: 'AMAZON_OPENSEARCH_SERVICE';
+  config: {
+    domain?: string;
+    endpoint?: string | IntrinsicFunction;
+    region?: string | IntrinsicFunction;
+    serviceRoleArn?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+  };
+};
+
+export type LambdaConfig =
+  | {
+      functionName: string;
+      functionAlias?: string;
+    }
+  | {
+      functionArn: string | IntrinsicFunction;
+    }
+  | {
+      function: Record<string, unknown>;
+    };
+
+export type DsLambdaConfig = {
+  type: 'AWS_LAMBDA';
+  config: {
+    serviceRoleArn?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+  } & LambdaConfig;
+};
+
+export type DsHttpConfig = {
+  type: 'HTTP';
+  config: {
+    endpoint: string | IntrinsicFunction;
+    serviceRoleArn?: string | IntrinsicFunction;
+    iamRoleStatements?: IamStatement[];
+    authorizationConfig?: {
+      authorizationType: 'AWS_IAM';
+      awsIamConfig: {
+        signingRegion: string | IntrinsicFunction;
+        signingServiceName?: string | IntrinsicFunction;
+      };
+    };
+  };
+};
+
+export type DsNone = {
+  type: 'NONE';
+};
+
+export type VisibilityConfig = {
+  name?: string;
+  cloudWatchMetricsEnabled?: boolean;
+  sampledRequestsEnabled?: boolean;
+};
+
+export type LoggingConfig = {
+  level: 'ERROR' | 'NONE' | 'ALL';
+  enabled?: boolean;
+  excludeVerboseContent?: boolean;
+  retentionInDays?: number;
+  roleArn?: string | IntrinsicFunction;
+};
+
+export type CachingConfig = {
+  enabled?: boolean;
+  behavior: 'FULL_REQUEST_CACHING' | 'PER_RESOLVER_CACHING';
+  type?: string;
+  ttl?: number;
+  atRestEncryption?: boolean;
+  transitEncryption?: boolean;
+};
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..c8cfae9
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,95 @@
+import { BuildOptions } from 'esbuild';
+import {
+  ApiKeyConfig,
+  Auth,
+  Substitutions,
+  CachingConfig,
+  DomainConfig,
+  LoggingConfig,
+  WafConfig,
+  DsDynamoDBConfig,
+  DsEventBridgeConfig,
+  DsHttpConfig,
+  DsLambdaConfig,
+  DsNone,
+  DsOpenSearchConfig,
+  DsRelationalDbConfig,
+  SyncConfig,
+} from './common';
+export * from './common';
+
+export type AppSyncConfig = {
+  name: string;
+  schema?: string | string[];
+  authentication: Auth;
+  additionalAuthentications?: Auth[];
+  domain?: DomainConfig;
+  apiKeys?: (ApiKeyConfig | string)[];
+  resolvers?: Record<string, ResolverConfig>[] | Record<string, ResolverConfig>;
+  pipelineFunctions?:
+    | Record<string, PipelineFunctionConfig>[]
+    | Record<string, PipelineFunctionConfig>;
+  dataSources:
+    | Record<string, DataSourceConfig>[]
+    | Record<string, DataSourceConfig>;
+  substitutions?: Substitutions;
+  xrayEnabled?: boolean;
+  logging?: LoggingConfig;
+  caching?: CachingConfig;
+  waf?: WafConfig;
+  tags?: Record<string, string>;
+  visibility?: 'GLOBAL' | 'PRIVATE';
+  esbuild?: BuildOptions | false;
+};
+
+export type BaseResolverConfig = {
+  field?: string;
+  type?: string;
+  request?: string | false;
+  response?: string | false;
+  code?: string;
+  caching?:
+    | {
+        ttl?: number;
+        keys?: string[];
+      }
+    | boolean;
+  sync?: SyncConfig;
+  substitutions?: Substitutions;
+};
+
+export type ResolverConfig = UnitResolverConfig | PipelineResolverConfig;
+
+export type UnitResolverConfig = BaseResolverConfig & {
+  kind: 'UNIT';
+  dataSource: string | DataSourceConfig;
+  maxBatchSize?: number;
+};
+
+export type PipelineResolverConfig = BaseResolverConfig & {
+  kind?: 'PIPELINE';
+  functions: (string | PipelineFunctionConfig)[];
+};
+
+export type DataSourceConfig = {
+  description?: string;
+} & (
+  | DsHttpConfig
+  | DsDynamoDBConfig
+  | DsRelationalDbConfig
+  | DsOpenSearchConfig
+  | DsLambdaConfig
+  | DsEventBridgeConfig
+  | DsNone
+);
+
+export type PipelineFunctionConfig = {
+  dataSource: string | DataSourceConfig;
+  description?: string;
+  code?: string;
+  request?: string;
+  response?: string;
+  maxBatchSize?: number;
+  substitutions?: Substitutions;
+  sync?: SyncConfig;
+};
diff --git a/src/types/plugin.ts b/src/types/plugin.ts
index ea56c8b..f7e245d 100644
--- a/src/types/plugin.ts
+++ b/src/types/plugin.ts
@@ -1,4 +1,22 @@
-import { CfnWafRuleStatement, IntrinsicFunction } from './cloudFormation';
+import { BuildOptions } from 'esbuild';
+import {
+  Auth,
+  DomainConfig,
+  ApiKeyConfig,
+  LoggingConfig,
+  CachingConfig,
+  WafConfig,
+  SyncConfig,
+  DsHttpConfig,
+  DsDynamoDBConfig,
+  DsRelationalDbConfig,
+  DsOpenSearchConfig,
+  DsLambdaConfig,
+  DsEventBridgeConfig,
+  DsNone,
+  Substitutions,
+} from './common';
+export * from './common';
 
 export type AppSyncConfig = {
   name: string;
@@ -17,140 +35,15 @@ export type AppSyncConfig = {
   waf?: WafConfig;
   tags?: Record<string, string>;
   visibility?: 'GLOBAL' | 'PRIVATE';
+  esbuild?: BuildOptions | false;
 };
 
-export type IamStatement = {
-  Effect: 'Allow' | 'Deny';
-  Action: string[];
-  Resource: string | IntrinsicFunction | (string | IntrinsicFunction)[];
-};
-
-export type WafConfig = {
-  enabled?: boolean;
-  arn?: string;
-  name?: string;
-  defaultAction?: WafAction;
-  description?: string;
-  visibilityConfig?: VisibilityConfig;
-  rules?: WafRule[];
-};
-
-export type WafThrottleConfig =
-  | number
-  | {
-      name?: string;
-      action?: WafAction;
-      aggregateKeyType?: 'IP' | 'FORWARDED_IP';
-      limit?: number;
-      priority?: number;
-      forwardedIPConfig?: {
-        headerName: string;
-        fallbackBehavior: 'MATCH' | 'NO_MATCH';
-      };
-      scopeDownStatement?: CfnWafRuleStatement;
-      visibilityConfig?: VisibilityConfig;
-    };
-
-export type WafDisableIntrospectionConfig = {
-  name?: string;
-  priority?: number;
-  visibilityConfig?: VisibilityConfig;
-};
-
-export type WafAction = 'Allow' | 'Block';
-export type WafRuleAction = 'Allow' | 'Block' | 'Count' | 'Captcha';
-
-export type WafRuleThrottle = {
-  throttle: WafThrottleConfig;
-};
-
-export type WafRuleCustom = {
-  name: string;
-  priority?: number;
-  action?: WafRuleAction;
-  statement: CfnWafRuleStatement;
-  visibilityConfig?: VisibilityConfig;
-};
-
-export type WafRuleDisableIntrospection = {
-  disableIntrospection: WafDisableIntrospectionConfig;
-};
-
-export type WafRule =
-  | WafRuleThrottle
-  | WafRuleDisableIntrospection
-  | WafRuleCustom
-  | 'disableIntrospection'
-  | 'throttle';
-
-export type ApiKeyConfig = {
-  apiKeyId?: string;
-  name: string;
-  description?: string;
-  expiresAfter?: string | number;
-  expiresAt?: string;
-  wafRules?: WafRule[];
-};
-
-export type CognitoAuth = {
-  type: 'AMAZON_COGNITO_USER_POOLS';
-  config: {
-    userPoolId: string | IntrinsicFunction;
-    awsRegion?: string | IntrinsicFunction;
-    defaultAction?: 'ALLOW' | 'DENY';
-    appIdClientRegex?: string | IntrinsicFunction;
-  };
-};
-
-export type IamAuth = {
-  type: 'AWS_IAM';
-};
-
-export type LambdaAuth = {
-  type: 'AWS_LAMBDA';
-  config: LambdaConfig & {
-    identityValidationExpression?: string;
-    authorizerResultTtlInSeconds?: number;
-  };
-};
-
-export type OidcAuth = {
-  type: 'OPENID_CONNECT';
-  config: {
-    issuer: string;
-    clientId: string;
-    iatTTL?: number;
-    authTTL?: number;
-  };
-};
-
-export type ApiKeyAuth = {
-  type: 'API_KEY';
-};
-
-export type Auth = CognitoAuth | LambdaAuth | OidcAuth | ApiKeyAuth | IamAuth;
-
-export type DomainConfig = {
-  enabled?: boolean;
-  useCloudFormation?: boolean;
-  retain?: boolean;
-  name: string;
-  certificateArn?: string;
-  hostedZoneId?: string;
-  hostedZoneName?: string;
-  route53?: boolean;
-};
-
-export type SyncConfig = {
-  conflictDetection: 'VERSION' | 'NONE';
-  conflictHandler: 'OPTIMISTIC_CONCURRENCY' | 'AUTOMERGE' | 'LAMBDA';
-} & LambdaConfig;
-
 export type BaseResolverConfig = {
   field: string;
   type: string;
   request?: string | false;
   response?: string | false;
+  code?: string;
   caching?:
     | {
         ttl?: number;
@@ -171,115 +64,9 @@ export type UnitResolverConfig = BaseResolverConfig & {
 
 export type PipelineResolverConfig = BaseResolverConfig & {
   kind?: 'PIPELINE';
-  code?: string;
   functions: string[];
 };
 
-export type Substitutions = Record<string, string | IntrinsicFunction>;
-
-export type PipelineFunctionConfig = {
-  name: string;
-  dataSource: string;
-  description?: string;
-  code?: string;
-  request?: string;
-  response?: string;
-  maxBatchSize?: number;
-  substitutions?: Substitutions;
-  sync?: SyncConfig;
-};
-
-export type DsDynamoDBConfig = {
-  type: 'AMAZON_DYNAMODB';
-  config: {
-    tableName: string | IntrinsicFunction;
-    useCallerCredentials?: boolean;
-    serviceRoleArn?: string | IntrinsicFunction;
-    region?: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-    versioned?: boolean;
-    deltaSyncConfig?: {
-      deltaSyncTableName: string;
-      baseTableTTL?: number;
-      deltaSyncTableTTL?: number;
-    };
-  };
-};
-
-export type DsEventBridgeConfig = {
-  type: 'AMAZON_EVENTBRIDGE';
-  config: {
-    serviceRoleArn?: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-    eventBusArn: string | IntrinsicFunction;
-  };
-};
-
-export type DsRelationalDbConfig = {
-  type: 'RELATIONAL_DATABASE';
-  config: {
-    region?: string;
-    relationalDatabaseSourceType?: 'RDS_HTTP_ENDPOINT';
-    serviceRoleArn?: string | IntrinsicFunction;
-    dbClusterIdentifier: string | IntrinsicFunction;
-    databaseName?: string | IntrinsicFunction;
-    schema?: string;
-    awsSecretStoreArn: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-  };
-};
-
-export type DsOpenSearchConfig = {
-  type: 'AMAZON_OPENSEARCH_SERVICE';
-  config: {
-    domain?: string;
-    endpoint?: string | IntrinsicFunction;
-    region?: string | IntrinsicFunction;
-    serviceRoleArn?: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-  };
-};
-
-export type LambdaConfig =
-  | {
-      functionName: string;
-      functionAlias?: string;
-    }
-  | {
-      functionArn: string | IntrinsicFunction;
-    }
-  | {
-      function: Record<string, unknown>;
-    };
-
-export type DsLambdaConfig = {
-  type: 'AWS_LAMBDA';
-  config: {
-    serviceRoleArn?: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-  } & LambdaConfig;
-};
-
-export type DsHttpConfig = {
-  type: 'HTTP';
-  config: {
-    endpoint: string | IntrinsicFunction;
-    serviceRoleArn?: string | IntrinsicFunction;
-    iamRoleStatements?: IamStatement[];
-    authorizationConfig?: {
-      authorizationType: 'AWS_IAM';
-      awsIamConfig: {
-        signingRegion: string | IntrinsicFunction;
-        signingServiceName?: string | IntrinsicFunction;
-      };
-    };
-  };
-};
-
-export type DsNone = {
-  type: 'NONE';
-};
-
 export type DataSourceConfig = {
   name: string;
   description?: string;
@@ -293,25 +80,15 @@ export type DataSourceConfig = {
   | DsNone
 );
 
-export type VisibilityConfig = {
-  name?: string;
-  cloudWatchMetricsEnabled?: boolean;
-  sampledRequestsEnabled?: boolean;
-};
-
-export type LoggingConfig = {
-  level: 'ERROR' | 'NONE' | 'ALL';
-  enabled?: boolean;
-  excludeVerboseContent?: boolean;
-  retentionInDays?: number;
-  roleArn?: string | IntrinsicFunction;
-};
-
-export type CachingConfig = {
-  enabled?: boolean;
-  behavior: 'FULL_REQUEST_CACHING' | 'PER_RESOLVER_CACHING';
-  type?: string;
-  ttl?: number;
-  atRestEncryption?: boolean;
-  transitEncryption?: boolean;
+export type PipelineFunctionConfig = {
+  name: string;
+  dataSource: string;
+  description?: string;
+  code?: string;
+  request?: string;
+  response?: string;
+  maxBatchSize?: number;
+  substitutions?: Substitutions;
+  sync?: SyncConfig;
 };
+export { Substitutions };
diff --git a/src/types/serverless.d.ts b/src/types/serverless.d.ts
index 1038c06..3d298c5 100644
--- a/src/types/serverless.d.ts
+++ b/src/types/serverless.d.ts
@@ -24,7 +24,7 @@ declare module '@serverless/utils/log' {
 }
 
 declare module 'serverless/lib/Serverless' {
-  import { AppSyncConfigInput } from 'plugin';
+  import { AppSyncConfig } from 'plugin';
   import Provider from 'serverless/lib/plugins/aws/provider.js';
   import type { AWS } from '@serverless/typescript';
 
@@ -59,7 +59,7 @@ declare module 'serverless/lib/Serverless' {
       options: Record<string, unknown>;
     };
     configurationInput: AWS & {
-      appSync: AppSyncConfigInput;
+      appSync: AppSyncConfig;
     };
     service: AWS & {
       setFunctionNames(rawOptions: Record<string, unknown>): void;
diff --git a/src/validation.ts b/src/validation.ts
index a187049..bc2887e 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -838,6 +838,15 @@ export const appSyncSchema = {
       ],
       errorMessage: 'contains invalid pipeline function definitions',
     },
+    esbuild: {
+      oneOf: [
+        {
+          type: 'object',
+        },
+        { const: false },
+      ],
+      errorMessage: 'must be an esbuild config object or false',
+    },
   },
   required: ['name', 'authentication'],
   additionalProperties: {
diff --git a/tsconfig.json b/tsconfig.json
index 2077e07..84c8630 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,8 @@
     "resolveJsonModule": true,
     "skipLibCheck": true,
     "outDir": "lib",
-    "noImplicitAny": false
+    "noImplicitAny": false,
+    "declaration": true
   },
   "include": ["src/**/*.ts"],
   "exclude": ["node_modules", "src/__tests__"]


Test patch:
diff --git a/src/__tests__/basicConfig.ts b/src/__tests__/basicConfig.ts
index 9c30b12..7d8692f 100644
--- a/src/__tests__/basicConfig.ts
+++ b/src/__tests__/basicConfig.ts
@@ -1,6 +1,6 @@
-import { AppSyncConfigInput } from '../getAppSyncConfig';
+import { AppSyncConfig } from '../types';
 
-export const basicConfig: AppSyncConfigInput = {
+export const basicConfig: AppSyncConfig = {
   name: 'My Api',
   authentication: {
     type: 'API_KEY',
diff --git a/src/__tests__/getAppSyncConfig.test.ts b/src/__tests__/getAppSyncConfig.test.ts
index d68400f..db93954 100644
--- a/src/__tests__/getAppSyncConfig.test.ts
+++ b/src/__tests__/getAppSyncConfig.test.ts
@@ -1,6 +1,7 @@
 import { pick } from 'lodash';
-import { getAppSyncConfig, ResolverConfigInput } from '../getAppSyncConfig';
+import { getAppSyncConfig } from '../getAppSyncConfig';
 import { basicConfig } from './basicConfig';
+import { ResolverConfig } from '../types';
 
 test('returns basic config', async () => {
   expect(getAppSyncConfig(basicConfig)).toMatchSnapshot();
@@ -228,7 +229,7 @@ describe('Resolvers', () => {
             field: 'getUsers',
           },
         },
-      ] as Record<string, ResolverConfigInput>[],
+      ] as Record<string, ResolverConfig>[],
     });
     expect(config.resolvers).toMatchSnapshot();
   });
diff --git a/src/__tests__/resolvers.test.ts b/src/__tests__/resolvers.test.ts
index 28ea5fe..55439d6 100644
--- a/src/__tests__/resolvers.test.ts
+++ b/src/__tests__/resolvers.test.ts
@@ -1,24 +1,122 @@
+import * as esbuild from 'esbuild';
 import fs from 'fs';
 import { Api } from '../resources/Api';
 import * as given from './given';
 
 const plugin = given.plugin();
 
+jest.mock('esbuild', () => ({
+  buildSync: jest.fn().mockImplementation((config) => {
+    return {
+      errors: [],
+      warnings: [],
+      metafile: undefined,
+      mangleCache: undefined,
+      outputFiles: [
+        {
+          path: 'path/to/file',
+          contents: Uint8Array.from([]),
+          text: `Bundled content of ${`${config.entryPoints?.[0]}`.replace(
+            /\\/g,
+            '/',
+          )}`,
+        },
+      ],
+    };
+  }),
+}));
+
 describe('Resolvers', () => {
   let mock: jest.SpyInstance;
-  let mockEists: jest.SpyInstance;
+  let mockExists: jest.SpyInstance;
+  let mockEsbuild: jest.SpyInstance;
   beforeEach(() => {
     mock = jest
       .spyOn(fs, 'readFileSync')
       .mockImplementation(
         (path) => `Content of ${`${path}`.replace(/\\/g, '/')}`,
       );
-    mockEists = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
+    mockExists = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
+    mockEsbuild = jest
+      .spyOn(esbuild, 'buildSync')
+      .mockImplementation((config) => {
+        return {
+          errors: [],
+          warnings: [],
+          metafile: undefined,
+          mangleCache: undefined,
+          outputFiles: [
+            {
+              path: 'path/to/file',
+              contents: Uint8Array.from([]),
+              text: `Bundled content of ${`${config.entryPoints?.[0]}`.replace(
+                /\\/g,
+                '/',
+              )}`,
+            },
+          ],
+        };
+      });
   });
 
   afterEach(() => {
     mock.mockRestore();
-    mockEists.mockRestore();
+    mockExists.mockRestore();
+    mockEsbuild.mockRestore();
+  });
+
+  describe('esbuild', () => {
+    it('should skip esbuild when disabled', () => {
+      const api = new Api(
+        given.appSyncConfig({
+          esbuild: false,
+          dataSources: {
+            myTable: {
+              name: 'myTable',
+              type: 'AMAZON_DYNAMODB',
+              config: { tableName: 'data' },
+            },
+          },
+        }),
+        plugin,
+      );
+      expect(
+        api.compilePipelineFunctionResource({
+          dataSource: 'myTable',
+          code: 'path/to/my-resolver.js',
+          name: 'my-function',
+        }),
+      ).toMatchInlineSnapshot(`
+        Object {
+          "GraphQlFunctionConfigurationmyfunction": Object {
+            "Properties": Object {
+              "ApiId": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlApi",
+                  "ApiId",
+                ],
+              },
+              "Code": "Content of path/to/my-resolver.js",
+              "DataSourceName": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlDsmyTable",
+                  "Name",
+                ],
+              },
+              "Description": undefined,
+              "FunctionVersion": "2018-05-29",
+              "MaxBatchSize": undefined,
+              "Name": "my-function",
+              "Runtime": Object {
+                "Name": "APPSYNC_JS",
+                "RuntimeVersion": "1.0.0",
+              },
+            },
+            "Type": "AWS::AppSync::FunctionConfiguration",
+          },
+        }
+      `);
+    });
   });
 
   describe('Unit Resolvers', () => {
@@ -76,6 +174,62 @@ describe('Resolvers', () => {
       `);
     });
 
+    it('should generate JS Resources with specific code', () => {
+      const api = new Api(
+        given.appSyncConfig({
+          dataSources: {
+            myTable: {
+              name: 'myTable',
+              type: 'AMAZON_DYNAMODB',
+              config: { tableName: 'data' },
+            },
+          },
+        }),
+        plugin,
+      );
+      expect(
+        api.compileResolver({
+          type: 'Query',
+          kind: 'UNIT',
+          field: 'user',
+          dataSource: 'myTable',
+          code: 'resolvers/getUserFunction.js',
+        }),
+      ).toMatchInlineSnapshot(`
+        Object {
+          "GraphQlResolverQueryuser": Object {
+            "DependsOn": Array [
+              "GraphQlSchema",
+            ],
+            "Properties": Object {
+              "ApiId": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlApi",
+                  "ApiId",
+                ],
+              },
+              "Code": "Bundled content of resolvers/getUserFunction.js",
+              "DataSourceName": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlDsmyTable",
+                  "Name",
+                ],
+              },
+              "FieldName": "user",
+              "Kind": "UNIT",
+              "MaxBatchSize": undefined,
+              "Runtime": Object {
+                "Name": "APPSYNC_JS",
+                "RuntimeVersion": "1.0.0",
+              },
+              "TypeName": "Query",
+            },
+            "Type": "AWS::AppSync::Resolver",
+          },
+        }
+      `);
+    });
+
     it('should generate Resources with direct Lambda', () => {
       const api = new Api(
         given.appSyncConfig({
@@ -275,7 +429,7 @@ describe('Resolvers', () => {
 
   describe('Pipeline Resovlers', () => {
     it('should generate JS Resources with default empty resolver', () => {
-      mockEists.mockReturnValue(false);
+      mockExists.mockReturnValue(false);
       const api = new Api(
         given.appSyncConfig({
           dataSources: {
@@ -458,7 +612,7 @@ describe('Resolvers', () => {
                   "ApiId",
                 ],
               },
-              "Code": "Content of resolvers/getUserFunction.js",
+              "Code": "Bundled content of resolvers/getUserFunction.js",
               "FieldName": "user",
               "Kind": "PIPELINE",
               "PipelineConfig": Object {
@@ -546,7 +700,7 @@ describe('Resolvers', () => {
                   "ApiId",
                 ],
               },
-              "Code": "Content of funciton1.js",
+              "Code": "Bundled content of funciton1.js",
               "DataSourceName": Object {
                 "Fn::GetAtt": Array [
                   "GraphQlDsmyTable",
@@ -923,5 +1077,69 @@ describe('Resolvers', () => {
         }
       `);
     });
+
+    it('should fallback to global caching TTL', () => {
+      const api = new Api(
+        given.appSyncConfig({
+          caching: {
+            behavior: 'PER_RESOLVER_CACHING',
+            ttl: 300,
+          },
+          dataSources: {
+            myTable: {
+              name: 'myTable',
+              type: 'AMAZON_DYNAMODB',
+              config: { tableName: 'data' },
+            },
+          },
+        }),
+        plugin,
+      );
+      expect(
+        api.compileResolver({
+          dataSource: 'myTable',
+          kind: 'UNIT',
+          type: 'Query',
+          field: 'user',
+          caching: {
+            keys: ['$context.identity.sub', '$context.arguments.id'],
+          },
+        }),
+      ).toMatchInlineSnapshot(`
+        Object {
+          "GraphQlResolverQueryuser": Object {
+            "DependsOn": Array [
+              "GraphQlSchema",
+            ],
+            "Properties": Object {
+              "ApiId": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlApi",
+                  "ApiId",
+                ],
+              },
+              "CachingConfig": Object {
+                "CachingKeys": Array [
+                  "$context.identity.sub",
+                  "$context.arguments.id",
+                ],
+                "Ttl": 300,
+              },
+              "DataSourceName": Object {
+                "Fn::GetAtt": Array [
+                  "GraphQlDsmyTable",
+                  "Name",
+                ],
+              },
+              "FieldName": "user",
+              "Kind": "UNIT",
+              "MaxBatchSize": undefined,
+              "TypeName": "Query",
+            },
+            "Type": "AWS::AppSync::Resolver",
+          },
+        }
+      `);
+    });
   });
 });
diff --git a/src/__tests__/validation/__snapshots__/base.test.ts.snap b/src/__tests__/validation/__snapshots__/base.test.ts.snap
index c0639dc..79de313 100644
--- a/src/__tests__/validation/__snapshots__/base.test.ts.snap
+++ b/src/__tests__/validation/__snapshots__/base.test.ts.snap
@@ -56,5 +56,6 @@ exports[`Valdiation should validate  1`] = `
 : must have required property 'authentication'
 /unknownPorp: invalid (unknown) property
 /xrayEnabled: must be boolean
-/visibility: must be \\"GLOBAL\\" or \\"PRIVATE\\""
+/visibility: must be \\"GLOBAL\\" or \\"PRIVATE\\"
+/esbuild: must be an esbuild config object or false"
 `;
diff --git a/src/__tests__/validation/auth.test.ts b/src/__tests__/validation/auth.test.ts
index 6cc4cdf..2849065 100644
--- a/src/__tests__/validation/auth.test.ts
+++ b/src/__tests__/validation/auth.test.ts
@@ -1,4 +1,4 @@
-import { AppSyncConfigInput } from '../../getAppSyncConfig';
+import { AppSyncConfig } from '../../types';
 import { validateConfig } from '../../validation';
 import { basicConfig } from '../basicConfig';
 
@@ -12,7 +12,7 @@ describe('Valdiation', () => {
           authentication: {
             type: 'API_KEY',
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'Cognito',
@@ -27,7 +27,7 @@ describe('Valdiation', () => {
               appIdClientRegex: '.*',
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'Cognito with Refs',
@@ -44,7 +44,7 @@ describe('Valdiation', () => {
               },
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'OIDC',
@@ -59,7 +59,7 @@ describe('Valdiation', () => {
               authTTL: 3600,
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'OIDC without a clientId',
@@ -73,7 +73,7 @@ describe('Valdiation', () => {
               authTTL: 3600,
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'IAM',
@@ -82,7 +82,7 @@ describe('Valdiation', () => {
           authentication: {
             type: 'AWS_IAM',
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'Lambda with functionName',
@@ -96,7 +96,7 @@ describe('Valdiation', () => {
               authorizerResultTtlInSeconds: 600,
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
       {
         name: 'Lambda with functionArn',
@@ -108,7 +108,7 @@ describe('Valdiation', () => {
               functionArn: 'arn:aws:lambda:...',
             },
           },
-        } as AppSyncConfigInput,
+        } as AppSyncConfig,
       },
     ];
 
diff --git a/src/__tests__/validation/base.test.ts b/src/__tests__/validation/base.test.ts
index bd9b7ee..4f899d2 100644
--- a/src/__tests__/validation/base.test.ts
+++ b/src/__tests__/validation/base.test.ts
@@ -1,4 +1,4 @@
-import { AppSyncConfigInput } from '../../getAppSyncConfig';
+import { AppSyncConfig } from '../../types';
 import { validateConfig } from '../../validation';
 import { basicConfig } from '../basicConfig';
 
@@ -12,6 +12,11 @@ describe('Valdiation', () => {
         tags: {
           foo: 'bar',
         },
+        esbuild: {
+          target: 'es2020',
+          sourcemap: false,
+          treeShaking: false,
+        },
       }),
     ).toBe(true);
 
@@ -20,6 +25,7 @@ describe('Valdiation', () => {
         visibility: 'FOO',
         xrayEnabled: 'BAR',
         unknownPorp: 'foo',
+        esbuild: 'bad',
       });
     }).toThrowErrorMatchingSnapshot();
   });
@@ -34,7 +40,7 @@ describe('Valdiation', () => {
             logging: {
               level: 'ALL',
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'Full',
@@ -46,7 +52,7 @@ describe('Valdiation', () => {
               excludeVerboseContent: true,
               loggingRoleArn: { Ref: 'MyLogGorupArn' },
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
       ];
 
@@ -94,7 +100,7 @@ describe('Valdiation', () => {
             waf: {
               rules: [],
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'Full',
@@ -164,7 +170,7 @@ describe('Valdiation', () => {
                 },
               ],
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'Using arn',
@@ -260,7 +266,7 @@ describe('Valdiation', () => {
               name: 'api.example.com',
               certificateArn: 'arn:aws:',
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'Full',
@@ -274,7 +280,7 @@ describe('Valdiation', () => {
               hostedZoneName: 'example.com.',
               route53: true,
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'useCloudFormation: false, missing certificateArn',
@@ -284,7 +290,7 @@ describe('Valdiation', () => {
               name: 'api.example.com',
               useCloudFormation: false,
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
       ];
 
@@ -364,7 +370,7 @@ describe('Valdiation', () => {
             caching: {
               behavior: 'PER_RESOLVER_CACHING',
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
         {
           name: 'Full',
@@ -378,7 +384,7 @@ describe('Valdiation', () => {
               atRestEncryption: true,
               transitEncryption: true,
             },
-          } as AppSyncConfigInput,
+          } as AppSyncConfig,
         },
       ];
 
diff --git a/src/__tests__/validation/pipelineFunctions.test.ts b/src/__tests__/validation/pipelineFunctions.test.ts
index 7341e10..c372a6f 100644
--- a/src/__tests__/validation/pipelineFunctions.test.ts
+++ b/src/__tests__/validation/pipelineFunctions.test.ts
@@ -1,4 +1,4 @@
-import { FunctionConfigInput } from '../../getAppSyncConfig';
+import { PipelineFunctionConfig } from '../../types';
 import { validateConfig } from '../../validation';
 import { basicConfig } from '../basicConfig';
 
@@ -19,7 +19,7 @@ describe('Basic', () => {
               request: 'request.vtl',
               response: 'response.vtl',
             },
-          } as Record<string, FunctionConfigInput>,
+          } as Record<string, PipelineFunctionConfig>,
         },
       },
       {
@@ -51,7 +51,7 @@ describe('Basic', () => {
               },
               function4: 'ds1',
             },
-          ] as Record<string, FunctionConfigInput>[],
+          ] as Record<string, PipelineFunctionConfig>[],
         },
       },
     ];