How to use TypeScript with AppSync Lambda Resolvers
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.
Prerequisites
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 filegenerates:
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 usenpm 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
andMutation
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
andMutationCreatePostArgs
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
andMutation[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 functioncallback
: 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 typeQueryPostArgs
(with the benefits of auto-complete!)- the return value, or the second argument of the
callback
, is expected to be of the same shape asPost
(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.
Originally published at https://benoitboure.com.