AWSAppSyncDynamoDB | 13 Min Read

Building a GraphQL API With TypeScript Resolvers Using AWS AppSync and CDK

Learn to create a powerful and scalable GraphQL API using AWS AppSync with TypeScript resolvers via the AWS CDK.

In a previous post we took a look at building a GraphQL API using the AWS CDK, in that post we looked at using VTL to define the resolvers used to process requests to and responses from the API. However, using VTL added an extra hurdle to deploying a GraphQL API via the AWS CDK because it meant learning and becoming comfortable with an entirely new language.

However, there is a solution to this, we can use the JavaScript resolvers that are supported by AWS AppSync, meaning our resolvers and CDK stack will all be written in the same language.

So, in this tutorial, we’re going to take a look at how we can build a GraphQL API for fetching and creating blog posts from an AWS DynamoDB database using AWS AppSync and the AWS CDK as well as using TypeScript to write our resolvers for the API.

Prerequisites

To get started with this tutorial, you’ll of course need an AWS CDK project so make sure to create a new one using `cdk init app --language typescript` if you don’t already have one. Then once your CDK project is set up, we’re ready to get started building!

Creating a DynamoDB Database

The first thing we’re going to do is create a new DynamoDB database, to do this, add the below code to your stack definition file (`./lib/*-stack.ts`).

./lib/*-stack.ts
1// 1. Create a new DynamoDB table
2const table = new Table(this, 'posts-db', {
3 tableName: 'graphql-posts-db',
4 partitionKey: { name: 'id', type: AttributeType.STRING },
5 removalPolicy: RemovalPolicy.DESTROY,
6});
ts

In this code, we define a new DynamoDB table with the name `graphql-posts-db`, we also add a partition key of `id` and set the database to be destroyed when we destroy the CDK stack.

Defining an AppSync GraphQL API

With our DynamoDB database now configured and ready to go, let’s turn our attention to the AppSync GraphQL API. To create this, we’re going to add the below code under the code we just added for DynamoDB.

./lib/*-stack.ts
1// ...DynamoDB definition
2
3// 2. Create an AppSync GraphQL API
4const api = new GraphqlApi(this, 'graphql-api', {
5 name: 'graphql-api',
6 definition: Definition.fromFile(
7 path.join(__dirname, '../graphql/schema.graphql')
8 ),
9 authorizationConfig: {
10 defaultAuthorization: {
11 authorizationType: AuthorizationType.API_KEY,
12 apiKeyConfig: {
13 expires: Expiration.after(Duration.days(365)),
14 },
15 },
16 },
17});
ts

With this code, we generate a new GraphQL API with the name `graphql-api` and point it to our GraphQL schema (we’ll create this in the next step), then finally we configure our API to use an API key for authorisation.

Adding a GraphQL Schema

With our GraphQL API now defined, let’s take a look at creating the GraphQL Schema file we linked to the API. To do this, create a new folder in the root of the project called `graphql` and then inside that directory create a new file called `schema.graphql`. Inside that file, add the below code.

./graphql/schema.graphql
1type Post {
2 id: ID!
3 title: String!
4 description: String!
5 author: String!
6 publicationDate: String!
7}
8
9type Query {
10 getPost(id: ID!): Post
11 getAllPosts: [Post]
12}
13
14type Mutation {
15 createPost(input: CreatePostInput!): Post
16 deletePost(id: ID!): ID
17}
18
19input CreatePostInput {
20 id: ID
21 title: String!
22 description: String!
23 author: String!
24 publicationDate: String!
25}
graphql

In our schema, we define the two queries we’ll be using (`getPost` and `getAllPosts`) as well as the two mutations we’ll be using (`createPost` and `deletePost`). We also define the input `CreatePostInput` which is what users of our API will need to provide to create a new post using the `createPost` mutation.

Linking the Database and GraphQL API

Finally, with our GraphQL API and schema defined, we can link it to our database by adding the below code to our stack definition file under our GraphQL API definition from earlier.

./lib/*-stack.ts
1// ...GraphQL API definition
2
3// 3. Link the GraphQL API with the DynamoDB table
4const dataSource = api.addDynamoDbDataSource('post-db-source', table);
ts

Generating TypeScript Types From a GraphQL Schema

With our GraphQL API, all set up and configured, it’s almost time for us to write our TypeScript resolvers. But, before we can do this, we need to generate the TypeScript types that we’ll use in the resolvers to type the requests and responses being processed.

Because GraphQL is fully typed via its schema we’re able to generate TypeScript types from it automatically. We can do this by running `npx @aws-amplify/cli codegen add` in our `graphql` directory. This helpful command from AWS Amplify will generate us our GraphQL types, queries and mutations. After running the command you’ll be prompted with a bunch of questions, here are the answers you’ll need for them.

1? Choose the type of app that you're building - javascript
2? What javascript framework are you using - none
3? Choose the code generation language target - typescript
4? Enter the file name pattern of graphql queries, mutations and subscriptions - types/*.ts
5? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions - Yes
6? Enter maximum statement depth [increase from default if your schema is deeply nested] - 2
7? Enter the file name for the generated code - types/graphql.ts
8? Do you want to generate code for your newly created GraphQL API - Yes

Once this command has finished, you should now have all of queries, mutations, and most importantly types generated from your GraphQL API available inside a new `types` directory inside the `graphql` directory. You’ll also notice that a new `.graphqlconfig.yml` was generated, this file is just the configuration used to generate the types in case we need to re-generate the types which we can do at any point by running `npx @aws-amplify/cli codegen`.

Creating the TypeScript AppSync Resolvers

With the TypeScript types for our GraphQL schema generated, we’re ready to start writing our TypeScript resolvers for our GraphQL API. But, first, we need to install a few more NPM packages to help with this which we can do with the command `npm i -D esbuild glob @aws-appsync/utils`.

Create

With those packages installed, let’s create our TypeScript resolvers. First of all, create a new directory inside our `graphql` directory called `ts-resolvers` and then inside that directory create a new file called `create-post.ts`. This will be the resolver for our `createPost` mutation and handle any requests sent to the mutation as well as the responses being sent back. Inside our resolver file, add the below code.

./graphql/ts-resolvers/create-post.ts
1import { put } from '@aws-appsync/utils/dynamodb';
2import { Context, util } from '@aws-appsync/utils';
3import { CreatePostMutationVariables, Post } from '../types/graphql';
4
5export function request(ctx: Context<CreatePostMutationVariables>) {
6 return put({
7 key: { id: util.autoId() },
8 item: ctx.args.input,
9 });
10}
11
12export function response(ctx: Context) {
13 return ctx.result as Post;
14}
ts

In this file, we export two functions (`request` and `response`) which both handle the action they’re named after. In the `request` function, we call the `put` function from the `@aws-appsync/utils/dynamodb` package which allows us to create new items in our database using the data passed to it from the user’s request which we access via the `ctx` prop. Then in the `response` function, we return the data created in the database from the request.

We then just need to repeat this process for the remaining queries and mutations in our API so we have one resolver file for each of the queries and mutations. Below is the code required for the three remaining resolvers for our API.

Delete

./graphql/ts-resolvers/delete-post.ts
1import { remove } from '@aws-appsync/utils/dynamodb';
2import { Context } from '@aws-appsync/utils';
3import { DeletePostMutationVariables, Post } from '../types/graphql';
4
5export function request(ctx: Context<DeletePostMutationVariables>) {
6 return remove({
7 key: { id: ctx.args.id },
8 });
9}
10
11export function response(ctx: Context) {
12 const result = ctx.result as Post;
13
14 return result.id;
15}
ts

Get One

./graphql/ts-resolvers/get-one-post.ts
1import { get } from '@aws-appsync/utils/dynamodb';
2import { Context } from '@aws-appsync/utils';
3import { Post, GetPostQueryVariables } from '../types/graphql';
4
5export function request(ctx: Context<GetPostQueryVariables>) {
6 return get({
7 key: { id: ctx.args.id },
8 });
9}
10
11export function response(ctx: Context) {
12 return ctx.result as Post;
13}
ts

Get All

./graphql/ts-resolvers/get-all-posts.ts
1import { scan } from '@aws-appsync/utils/dynamodb';
2import { Context } from '@aws-appsync/utils';
3import { Post } from '../types/graphql';
4
5export function request() {
6 return scan({});
7}
8
9export function response(ctx: Context) {
10 const { items } = ctx.result as { items: Post[] };
11
12 return items;
13}
ts

Building Our TypeScript Resolvers to JavaScript

With the TypeScript resolvers for our GraphQL API written we’ve almost finished with building our API but there is one problem left that we need to overcome. That problem is that AppSync can’t process TypeScript, it can only process JavaScript so if we provide it a TypeScript file as the resolver code it’ll error and not work.

But, there is an easy solution to this, we can build out our TypeScript to JavaScript before we deploy our CDK stack, this is why we installed `esbuild` and `glob` earlier on. To set this up, create a new file in the root of your CDK project called `build.mjs` and add the below code to it.

./build.mjs
1import { build } from "esbuild";
2import { glob } from "glob";
3
4const files = await glob("graphql/ts-resolvers/**/*.ts");
5
6await build({
7 sourcemap: "inline",
8 sourcesContent: false,
9 format: "esm",
10 target: "esnext",
11 platform: "node",
12 external: ["@aws-appsync/utils"],
13 outdir: "graphql/js-resolvers",
14 entryPoints: files,
15 bundle: true,
16});
js

This code will build out all of the TypeScript files in our `ts-resolvers` directory to JavaScript files in a new directory called `js-resolvers`, we can check this works smoothly by running `node build.mjs` in the root of the project. And, now with these JavaScript files generated, we can point our resolver definitions to them and everything will run smoothly!

But, before we do that, let’s make a small quality-of-life improvement and add a new `predeploy` step to our `package.json` so we don’t need to remember to run our build file each time manually before deploying. To add this script, add the below line of code to your `scripts` object in your `package.json` file.

./package.json
1{
2 "scripts": {
3 "predeploy": "node build.mjs"
4 }
5}
json

Connecting the Resolvers to Our GraphQL API

With our resolvers now written and building out to JavaScript, let’s connect them to our GraphQL API by creating a resolver resource for each one on the data source we created earlier when we connected DynamoDB to our GraphQL API. Below is the code you’ll need to add to your stack definition file for all of the resolvers.

./lib/*-stack.ts
1// ...data source definition
2
3// 4. Defining our AppSync Resolvers
4dataSource.createResolver("getOnePostResolver", {
5 typeName: "Query",
6 fieldName: "getPost",
7 runtime: FunctionRuntime.JS_1_0_0,
8 code: Code.fromAsset(
9 path.join(__dirname, "../graphql/js-resolvers/get-one-post.js")
10 ),
11});
12
13dataSource.createResolver("getAllPostsResolver", {
14 typeName: "Query",
15 fieldName: "getAllPosts",
16 runtime: FunctionRuntime.JS_1_0_0,
17 code: Code.fromAsset(
18 path.join(__dirname, "../graphql/js-resolvers/get-all-posts.js")
19 ),
20});
21
22dataSource.createResolver("createPostResolver", {
23 typeName: "Mutation",
24 fieldName: "createPost",
25 runtime: FunctionRuntime.JS_1_0_0,
26 code: Code.fromAsset(
27 path.join(__dirname, "../graphql/js-resolvers/create-post.js")
28 ),
29});
30
31dataSource.createResolver("deletePostResolver", {
32 typeName: "Mutation",
33 fieldName: "deletePost",
34 runtime: FunctionRuntime.JS_1_0_0,
35 code: Code.fromAsset(
36 path.join(__dirname, "../graphql/js-resolvers/delete-post.js")
37 ),
38});
ts

The most important thing to note in these definitions is the `typeName` property changing based on whether the resolver is for a query or mutation as well as the `fieldName` property which is the action being performed in the API. Finally, we pass the generated JavaScript resolver files to the `code` property.

Deployment

And, now we’ve finished defining all of the services and resources we’ll need to create our GraphQL API with TypeScript resolvers. But, before we deploy our resources, let’s add a couple of outputs for our API URL and API key so we’re able to easily test our new API.

To add these outputs, add the below code under the resolvers code we just added in the last step in our stack definition file.

./lib/*-stack.ts
1// ...resolvers code
2
3// Misc. Outputs
4new CfnOutput(this, "api-key-output", {
5 value: api.apiKey || "",
6});
7
8new CfnOutput(this, "api-url", {
9 value: api.graphqlUrl || "",
10});
ts

Now, we can deploy our stack by running `npm run deploy` and accepting any prompts given to us. Once the stack has finished deploying you should have your API URL and API key in your terminal in a format like the below.

1Outputs:
2<STACK_NAME>.apikeyoutput = <API_KEY>
3<STACK_NAME>.apiurl = <API_URL>

Testing Our API in Postman

With our API now deployed, let’s make sure it works correctly by testing each of the queries and mutations we’ve defined on our API which we’ll do by performing requests from Postman.

To use Postman to perform these tests, we’re going to create a new request using the GraphQL option and then provide the URL returned to us from the deploy command a moment ago. We’re now set up to run the various tests we want to perform.

Unauthorised Response

The first test we’re going to perform is to make sure the API responds with the correct response when we don’t pass in an API key header to our request. So to do this, send an empty request with no query or headers to your API URL and you should see a response like the one below with a status code of `401`.

1{
2 "errors": [
3 {
4 "errorType": "UnauthorizedException",
5 "message": "You are not authorized to make this call."
6 }
7 ]
8}
json

Now we know our API authentication works correctly, let’s add our API key header to the request by creating a new entry on the “Headers” page in Postman with a key of `x-api-key` and a value of the API key given to you from the deploy command. With that configured, let’s start testing our queries and mutations to ensure they all return `200` status codes and with the correct data.

Creating a Post

The first test we’re going to perform is creating a new post, to do this, add the below code to the “Query” tab in Postman.

1mutation CreatePost {
2 createPost(
3 input: {
4 title: "Example Post"
5 author: "Myself"
6 description: "Some Description"
7 publicationDate: "today"
8 }
9 ) {
10 id
11 }
12}
graphql

After pushing the “Query” button you should then have a response that looks like the one below (with a unique ID value).

1{
2 "data": {
3 "createPost": {
4 "id": "POST_ID"
5 }
6 }
7}
json

Getting All Posts

Now we have a post in our database, let’s test our `getAllPosts` query by switching out the code in our “Query” tab on Postman with the below code and running the query again.

1query GetAllPosts {
2 getAllPosts {
3 id
4 title
5 description
6 author
7 publicationDate
8 }
9}
graphql

You should then see a response like the one below containing an array of all of the posts in your database.

1{
2 "data": {
3 "getAllPosts": [
4 {
5 "id": "POST_ID",
6 "title": "Example Post",
7 "description": "Some Description",
8 "author": "Myself",
9 "publicationDate": "today"
10 }
11 ]
12 }
13}
json

Get a Single Post

Let’s now test our ability to fetch a single post by running our `getPost` query by updating our “Query” tab on Postman to use the below code. Don’t forget to update the `POST_ID` value to be equal to the ID you got from the `createPost` mutation earlier.

1query GetPost {
2 getPost(id: "POST_ID") {
3 id
4 title
5 description
6 author
7 publicationDate
8 }
9}
graphql

Once that query is run, you should see a response that looks like the below but with your ID value in.

1{
2 "data": {
3 "getPost": {
4 "id": "POST_ID",
5 "title": "Example Post",
6 "description": "Some Description",
7 "author": "Myself",
8 "publicationDate": "today"
9 }
10 }
11}
json

Deleting a Post

Finally, the last mutation we need to test is the `deletePost` mutation so let’s test that by updating our “Query” page one last time to use the code below. Again, don’t forget to update the `POST_ID` value to be equal to the ID you got from the `createPost` mutation earlier.

1mutation DeletePost {
2 deletePost(id: "POST_ID")
3}
graphql

Once that query is run, you should see a response like below.

1{
2 "data": {
3 "deletePost": "POST_ID"
4 }
5}
json

If you wanted to you could also validate the post has been deleted by rerunning the `getAllPosts` query from earlier to check the item has indeed been removed from the database.

And, now, if you’ve received the same response statuses and bodies as shown above then congrats your GraphQL API works perfectly! And with that, we’ve now reached the end of the tutorial so let’s recap what we’ve covered.

Closing Thoughts

In this post, we’ve looked at how we can create a GraphQL API using AWS AppSync and TypeScript resolvers via the AWS CDK. If you’d like to see the full example code for this project, you can see it over on my GitHub. And, if you’d like to read more about the benefits of GraphQL or how to create a GraphQL API using VTL resolvers in AWS AppSync, make sure to check out my previous post.

I hope you found this tutorial helpful. If you did please consider sharing it with others who might find it helpful as well.

Thank you for reading

Coner



Content

Latest Blog Posts

Below is my latest blog post and a link to all of my posts.

View All Posts

Content

Latest Video

Here is my YouTube Channel and latest video for your enjoyment.

View All Videos
AWS Bedrock Text and Image Generation Tutorial (AWS SDK)

AWS Bedrock Text and Image Generation Tutorial (AWS SDK)

Contact

Join My Newsletter

Subscribe to my weekly newsletter by filling in the form.

Get my latest content every week and 0 spam!