How to use TypeScript with AppSync Lambda Resolvers

Generate TS types directly from your schema!

How to use TypeScript with AppSync Lambda Resolvers

✏️ Edit - 2023-04-11: If you're interested in creating JavaScript resolvers with TypeScript rather than utilizing Lambda functions, check out this alternative article.

One of the great benefits of GraphQL is typing! Define your schema, and GraphQL enforces the input/output "shape" of your endpoints data.

If you are using Lambda as your AppSync resolvers with the node.js runtime, you might be using TypeScript, too. If you do, you might also be defining TS types that correspond to your schema. Doing this manually can be tedious, is prone to error, and is basically doing the same job twice! 🙁 Wouldn't it be great if you could import your GraphQL types into your code automatically?

In this article, I'll show you how to generate TypeScript types directly from your GraphQL schema, just by running a simple command line. Then, I'll teach you how to use those types in your Lambda resolvers.

Let's begin.

Pre-requisites

You should already have a basic AppSync project setup with a defined GraphQL schema (If you don't have one already, you can use the example down below).

For the purpose of this tutorial, I will take this simple schema as an example:

type Query {
    post(id: ID!): Post
}

type Mutation {
    createPost(post: PostInput!): Post!
}

type Post {
    id: ID!
    title: String!
    content: String!
    publishedAt: AWSDateTime
}

input PostInput {
    title: String!
    content: String!
}

Setting up the project

Install the dependencies

We will need to install three packages:

npm i @graphql-codegen/cli @graphql-codegen/typescript @types/aws-lambda  -D

The first two packages belong to the graphql-code-generator suite. The first one is the base CLI, while the second one is the plugin that generates TypeScript code from a GraphQL schema.

@types/aws-lambda is a collection of TypeScript types for AWS Lambda. It includes all sorts of Lambda event type definitions (API gateway, S3, SNS, etc.), including one for AppSync resolvers (AppSyncResolverHandler). We'll use that last one later when we build our resolvers.

Create the configuration file

It's time to configure graphql-codegen and tell it how to generate our TS types. For that, we'll create a codegen.yml file:

overwrite: true
schema:
  - schema.graphql #your schema file

generates:
  appsync.d.ts:
    plugins:
      - typescript

This tells codegen which schema file(s) it should use (in the example: schema.graphql), what plugin (typescript) and where the output should be placed (appsync.d.ts). Fell free to change these parameters to match your needs.

Support for AWS Scalars

If you are using special AWS AppSync Scalars, you will also need to tell graphql-codegen how to handle them.

💡 You need to declare, at the minimum, the scalars that you use, but it might be a good idea to just declare them all and forget about it.

Let's create a new appsync.graphql file with the following content:

scalar AWSDate
scalar AWSTime
scalar AWSDateTime
scalar AWSTimestamp
scalar AWSEmail
scalar AWSJSON
scalar AWSURL
scalar AWSPhone
scalar AWSIPAddress

⚠️ Don't place these types in the same file as your main schema. You only need them for code generation and they should not get into your deployment package to AWS AppSync.

We also need to tell codegen how to map these scalars to TypeScript. For that, we will modify the codegen.yml file. Add/edit the following sections:

schema:
  - schema.graphql
  - appsync.graphql # 👈 add this

# and this 👇
config:
  scalars:
    AWSJSON: string
    AWSDate: string
    AWSTime: string
    AWSDateTime: string
    AWSTimestamp: number
    AWSEmail: string
    AWSURL: string
    AWSPhone: string
    AWSIPAddress: string

Generate the code

We are all set with the configuration. Time to generate some code! Run the following command:

graphql-codegen

💡 You can also add "codegen": "graphql-codegen" to you package.json under the "scripts" section, and use npm run codegen.

If you look in your working directory, you should now see an appsync.d.ts file that contains your generated types.

export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  AWSDate: string;
  AWSTime: string;
  AWSDateTime: string;
  AWSTimestamp: number;
  AWSEmail: string;
  AWSJSON: string;
  AWSURL: string;
  AWSPhone: string;
  AWSIPAddress: string;
};

export type Query = {
  __typename?: 'Query';
  post?: Maybe<Post>;
};


export type QueryPostArgs = {
  id: Scalars['ID'];
};

export type Mutation = {
  __typename?: 'Mutation';
  createPost: Post;
};


export type MutationCreatePostArgs = {
  post: PostInput;
};

export type Post = {
  __typename?: 'Post';
  id: Scalars['ID'];
  title: Scalars['String'];
  content: Scalars['String'];
  publishedAt?: Maybe<Scalars['AWSDateTime']>;
};

export type PostInput = {
  title: Scalars['String'];
  content: Scalars['String'];
};

Notice that, apart from some helper types at the top, different types are being generated:

  • Scalars

Contains all the basic scalars (ID, String, etc.) and the AWS custom Scalars.

  • Query and Mutation

These two types describe the full Query and Mutation types.

  • Post

This is our Post type from our schema translated into TypeScript. It is also the return value of the post query and the createPost mutation.

  • QueryPostArgs and MutationCreatePostArgs

These types describe the input arguments of the post Query and the createPost mutation, respectively.

💡 Did you notice the name pattern here? Argument types are always named Query[NameOfTheEndpoint]Args and Mutation[NameOfTheEndpoint]Args in PascalCase. This is useful to know when you want to auto-complete types in your IDE.

Use the generated types

Now that we have generated our types, it's time to use them!

Let's implement the Query.post resolver as an example.

Lambda handlers always receive 3 arguments:

  • event: contains information about the input query (arguments, identity, etc)

  • context: contains information about the executed Lambda function

  • callback: a function you can call when your handler finishes (if you are not using async/promises)

The shape of an AppSync handler is almost always the same. It turns out that there is a DefinitelyTyped package that already defines it. We installed it at the beginning of this tutorial. Let's use it!

The AppSyncResolverHandler type takes two arguments. The first one is the type for the event.arguments object, and the second one is the return value of the resolver.

In our case that will be: QueryPostArgs and Post, respectively.

Here is how to use it:

import db from './db';
import { AppSyncResolverHandler } from 'aws-lambda';
import {Post, QueryPostArgs} from './appsync';

export const handler: AppSyncResolverHandler<QueryPostArgs, Post> = async (event) => {
    const post = await db.getPost(event.arguments.id);

    if (post) {
        return post;
    }

    throw new Error('Not Found');
};

Now, our Lambda handler benefits from type-checking in 2 ways:

  • event.arguments will be of type QueryPostArgs (with the benefits of auto-complete!)

  • the return value, or the second argument of the callback, is expected to be of the same shape as Post (with an id, title, etc); or TypeScript will show you an error.

Advanced usage

There are lots of options that let you customize your generated types. Check out the documentation for more details!

Conclusion

By auto-generating types, you will not only improve your development speed and experience but will also ensure that your resolvers do what your API is expecting. You also ensure that your code types and your schema types are always in perfect sync, avoiding mismatches that could lead to bugs.

Don't forget to re-run the graphql-codegen command each time you edit your schema! It might be a good idea to automate the process or validate your types in your CI/CD pipeline.

Did you find this article valuable?

Support Benoît Bouré by becoming a sponsor. Any amount is appreciated!