AWSDynamoDBAppSync | 14 Min Read
How to Create a GraphQL API: Step-by-Step Guide with AppSync and DynamoDB Using the AWS CDK
Discover how to effortlessly create, deploy, and test a powerful GraphQL API with AWS AppSync, DynamoDB, and the AWS CDK.
When it comes to building an API, the default choice is often to use a REST API but in recent years GraphQL APIs have been gaining popularity. So, in this tutorial, we’re going to explore GraphQL APIs further and build a GraphQL version of the REST API we built in this post.
For this tutorial, we’re going to be using AWS AppSync to create our GraphQL API as well as DynamoDB for our database as we did in that previous post. But, before we jump into building our example API, let’s explore GraphQL APIs a bit further.
Benefits of GraphQL APIs
GraphQL APIs offer a couple of enticing benefits over their REST counterparts. One of the largest benefits of GraphQL is the ability for the client to request just the data they need and receive it all in a single response. This solves a couple of problems that REST APIs have; firstly, the requirement to send multiple requests to different endpoints to fetch all the data we need, and secondly, over/under-fetching data from a single endpoint.
Because in a REST API, the server controls the structure and data included in a response, the client might receive a lot more data than they need (over-fetching) or the opposite and not get all the data they require (under-fetching). Both of these situations lead to potential issues, for under-fetching, it leads to multiple requests being required to multiple endpoints as described above, and for over-fetching, it leads to wasted bandwidth and increased latency while waiting for larger than-required responses being sent and processed.
Secondly, unlike REST APIs, GraphQL takes a strong opinion on avoiding versioning and instead serves versionless API. This is because, unlike REST APIs, GraphQL only returns the data requested by the client so we can safely add new features, endpoints, and fields without worrying about adding a breaking change into the API and thus requiring a new version.
On the surface, this might seem like a minor thing but maintaining an API with multiple versions can add a lot of work to a team and add complexity to a codebase and project. From the client's perspective, consuming a versioned API can also add extra maintenance and complexity to ensure they stay in line with the latest API version and its changes.
Now we’ve seen some of the benefits of GraphQL APIs. Let’s move into the actual tutorial portion of this post and build our example GraphQL API to create, delete, and retrieve blog posts from a DynamoDB database.
Prerequisites
For this tutorial, you’ll need to have an AWS account as well as the AWS CDK configured on your local machine. You’ll also need either a new or existing AWS CDK project to define the API that we’re going to create in. For the purposes of this tutorial, I’m going to create a new CDK project using TypeScript via the command `cdk init app --language typescript`
.
DynamoDB
Once we have all of the needed prerequisites covered, we’re ready to get started with building our GraphQL API. The first thing we need to do is define a new DynamoDB table that we’ll use to store the example blog posts we’ll create and retrieve via the API.
To create our new DynamoDB table, add the below code to your stack definition file inside the `lib`
directory.
1// 1. Create a new DynamoDB table2const table = new Table(this, "posts-db", {3 tableName: "graphql-posts-db",4 partitionKey: { name: "id", type: AttributeType.STRING },5 removalPolicy: RemovalPolicy.DESTROY,6});
tsGraphQL API
With our DynamoDB table taken care of, let’s move on to defining our new GraphQL API using AWS AppSync. To do this, add the below code under the code we just added for our DyanamoDB table.
1// 2. Create an AppSync GraphQL API2const api = new GraphqlApi(this, "graphql-api", {3 name: "graphql-api",4 schema: SchemaFile.fromAsset("./graphql/schema.graphql"),5 authorizationConfig: {6 defaultAuthorization: {7 authorizationType: AuthorizationType.API_KEY,8 apiKeyConfig: {9 expires: Expiration.after(Duration.days(365)),10 },11 },12 },13});
tsWith this code, we define a new GraphQL API with the name of `graphql-api`
, we then point it to the file that will contain the GraphQL schema for our API before then configuring the authorization for our API. In the case of our API, we’ll be using API keys for authorization of which any keys generated are set to expire one year after creation.
With our new GraphQL API defined in our CDK stack, let’s now create the GraphQL schema file that we pointed our API to above. To do this, create a new file at `./graphql/schema.graphql`
and add the below code to it.
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!): Post11 getAllPosts: [Post]12}13
14type Mutation {15 createPost(input: CreatePostInput!): Post16 deletePost(id: ID!): ID17}18
19input CreatePostInput {20 title: String!21 description: String!22 author: String!23 publicationDate: String!24}
graphqlThis schema defines two queries and two mutations for us to use on our API; this is to match the operations that were available on the REST API we built in the previous tutorial linked at the start of this post. With our API using this schema, we’ll be able to get a single post (`getPost`
), get all of the existing posts (`getAllPosts`
), create a new post (`createPost`
), and delete an existing post (`deletePost`
).
Linking our API and Database
With our database and API defined, let’s now connect the two together so our API can read and write data from our database. To do this we will need to create a new data source by adding the below code to your stack definition file in the `lib`
directory under the code we added for our GraphQL API.
1// 3. Link the GraphQL API with the DynamoDB table2const dataSource = api.addDynamoDbDataSource("post-db-source", table);
tsDefining our Resolvers
With our API and database now created and linked, we’re ready to move on to defining the queries and mutations that we can send in requests to our API. To do this, we need to define a new resolver on the data source that we created in the last step for each query and mutation we defined in our schema file.
What these resolvers do is tell the API what code to run in response to receiving a certain query or mutation in a request. You can think of them as the link between the definition of a query/mutation and the logic we want to run for that request.
Below is the code for the four resolvers we need for our queries and mutations. When defining a resolver, we need to give it a few configuration options, these are.
`typeName`
: If it’s a “Query” or “Mutation”`fieldName`
: The name of the query or mutation to trigger that resolver.`requestMappingTemplate`
: The request mapping template file that is linked to this resolver.`responseMappingTemplate`
: The response mapping template file that is linked to this resolver.
1// 4. Create our resolvers for queries2// 4a. Create a resolver for the `getPost` query3dataSource.createResolver("get-post-query", {4 typeName: "Query",5 fieldName: "getPost",6 requestMappingTemplate: MappingTemplate.fromFile(7 "./graphql/resolvers/Query.getPost.req.vtl"8 ),9 responseMappingTemplate: MappingTemplate.fromFile(10 "./graphql/resolvers/Query.getPost.res.vtl"11 ),12});13
14// 4b. Create a resolver for the `getAllPosts` query15dataSource.createResolver("get-all-posts-query", {16 typeName: "Query",17 fieldName: "getAllPosts",18 requestMappingTemplate: MappingTemplate.fromFile(19 "./graphql/resolvers/Query.getAllPosts.req.vtl"20 ),21 responseMappingTemplate: MappingTemplate.fromFile(22 "./graphql/resolvers/Query.getAllPosts.res.vtl"23 ),24});25
26// 4c. Create a resolver for the `createPost` mutation27dataSource.createResolver("create-post-mutation", {28 typeName: "Mutation",29 fieldName: "createPost",30 requestMappingTemplate: MappingTemplate.fromFile(31 "./graphql/resolvers/Mutation.createPost.req.vtl"32 ),33 responseMappingTemplate: MappingTemplate.fromFile(34 "./graphql/resolvers/Mutation.createPost.res.vtl"35 ),36});37
38// 4d. Create a resolver for the `deletePost` mutation39dataSource.createResolver("delete-post-mutation", {40 typeName: "Mutation",41 fieldName: "deletePost",42 requestMappingTemplate: MappingTemplate.fromFile(43 "./graphql/resolvers/Mutation.deletePost.req.vtl"44 ),45 responseMappingTemplate: MappingTemplate.fromFile(46 "./graphql/resolvers/Mutation.deletePost.res.vtl"47 ),48});
tsCreating our Mapping Templates
At this point, you may be wondering what the request and response mapping templates mentioned in the code above are. These are files that contain the actual logic we want to run when we receive a request (`requestMappingTemplate`
) as well as how we want to respond to the request (`responseMappingTemplate`
).
Typically, the `requestMappingTemplate`
will contain the logic for reading/writing to the database and the `responseMappingTemplate`
will contain the logic for sending the data back to the user.
Besides the above, the only other thing we need to know about mapping templates for this tutorial is that they’re written in a language called VTL. But, if you’re interested in learning more about mapping templates, you can do that here.
Now, we know a bit more about mapping templates and their role in our API, let’s create the 8 mapping templates we need for our resolvers (2 per resolver, 1 request, and 1 response).
To create our mapping templates, we’ll create a new `resolvers`
directory inside our `graphql`
directory from earlier. Then we need to create the below 8 files with the shown contents in that directory.
`getPost`
With the `getPost`
query, we perform a `GetItem`
operation which will retrieve the item in the database that has an id that matches the one passed into the request. We then take the result of that operation and pass it back to the requester using `$util.toJson`
.
1{2 "version": "2017-02-28",3 "operation": "GetItem",4 "key": {5 "id": $util.dynamodb.toDynamoDBJson($ctx.args.id),6 }7}
vtl1$util.toJson($context.result);
vtl`getAllPosts`
For `getAllPosts`
, we do a similar thing to `getPost`
but instead of doing a `GetItem`
operation, we do a `Scan`
operation to read all of the items in the database. We then take the result of that `Scan`
operation and check if there are any post items in the returned data, if so we return them to the requester using `$util.toJson`
. Otherwise, we return an empty array.
1{2 "version": "2017-02-28",3 "operation": "Scan"4}
vtl1#if($ctx.result && $ctx.result.items)2 $util.toJson($ctx.result.items)3#else4 []5#end
vtl`createPost`
For `createPost`
, we perform a `PutItem`
operation to create a new item in our database. For the attributes of the new item, we take the arguments passed into the request and use them as the values we also generate a new ID for the item using the `autoId()`
function.
After we create the new item in the database, we return the attributes of that item back to the requester, including the generated ID.
1{2 "version": "2017-02-28",3 "operation": "PutItem",4 "key": {5 "id": $util.dynamodb.toDynamoDBJson($util.autoId())6 },7 "attributeValues": {8 "title": $util.dynamodb.toDynamoDBJson($ctx.arguments.input.title),9 "description": $util.dynamodb.toDynamoDBJson($ctx.arguments.input.description),10 "author": $util.dynamodb.toDynamoDBJson($ctx.arguments.input.author),11 "publicationDate": $util.dynamodb.toDynamoDBJson($ctx.arguments.input.publicationDate)12 }13}
vtl1$util.toJson($context.result);
vtl`deletePost`
Finally, for `deletePost`
, we use the `DeleteItem`
operation to delete the item that matches the ID passed into the request from the database. After the operation has been performed, we return the id of the item deleted back to the requester.
1{2 "version": "2017-02-28",3 "operation": "DeleteItem",4 "key": {5 "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)6 }7}
vtl1$util.toJson($context.args.id);
vtlDeploying our API
With all of our mapping templates and resolvers now defined, we have everything needed to deploy a working GraphQL API using AWS AppSync. But, before we can do that we need to add a couple of outputs to our stack definition file in our `lib`
directory. One for the URL the API is deployed to and one for the API key value we can use in our requests. Add these outputs to your stack by adding the below code snippet under the resolvers code from earlier.
1// Misc. Outputs2new CfnOutput(this, "ApiKeyOutput", {3 value: api.apiKey || "",4});5
6new CfnOutput(this, "ApiUrl", {7 value: api.graphqlUrl || "",8});
tsNow, with those outputs added, we’re ready to deploy our new API so to do that run the command `cdk deploy`
and accept any prompts you’re given. Once that command finishes you’re new GraphQL API should be deployed to AWS and we can move on to testing it using Postman.
Testing our GraphQL API using Postman
After your API has finished deploying to AWS you should be given your API URL and API key in your terminal, it’ll appear in a format like this.
1Outputs:2<STACK_NAME>.ApiKeyOutput = <API_KEY>3<STACK_NAME>.ApiUrl = <API_URL>
Once you have these values, you’re ready to test your API. For this tutorial, I’ll be using Postman to test our new API so make sure to install it if you don’t already have it installed. Once you have Postman set up on your machine, open it up and you should be greeted with a UI that allows you to enter the URL you’d like to test as well the HTTP method you’d like to use as well as a series of options for adding things such as “Headers” and “Body” to the request.
For the URL, you’ll want to enter the API URL you were given in your terminal, then select POST as the target HTTP method as that’s the only HTTP method that GraphQL APIs support. After those two things are configured, we’re ready to start testing!
Unauthorized 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.
However, before, we can send our test request to our API, we need to add a body to our request to make sure it’s a valid GraphQL query. To do this, click on the “Body” tab and then select “raw” and enter the below code snippet in the code box provided.
1{2 "query": "query {getAllPosts {id title description author publicationDate}}",3 "variables": {}4}
jsonNow, press “Send” and you should get a response back similar to the one below with a `401 Unauthorized`
status.
1{2 "errors": [3 {4 "errorType": "UnauthorizedException",5 "message": "You are not authorized to make this call."6 }7 ]8}
jsonIf so, congrats, your API correctly responds to unauthorized responses. Let’s now move on to some successful outcome testing.
No Posts Exist
Now we know our API returns the correct response for unauthorized requests, we can add our API key by going to the “Headers” tab next to “Body” and then adding an entry to the table. The “Key” will be `x-api-key`
and the value will be your API key from the terminal output a moment ago.
With our API key now configured, we’re ready to test the response if no posts exist in our database. We already have the correct query configured from our unauthorized test so just hit “Send” again and you should get a `200 OK`
response with a body like the one below.
1{2 "data": {3 "getAllPosts": []4 }5}
jsonCreate a Post
To test if we can create a new post, switch out the body in your request with the one below.
1{2 "query": "mutation createPost($input: CreatePostInput!) {createPost(input: $input) { id title description author publicationDate }}",3 "variables": {4 "input": {5 "title": "New Post",6 "description": "This is a new post",7 "author": "John Doe",8 "publicationDate": "2023-06-23"9 }10 }11}
jsonThen send your request again and you should get a `200 OK`
response with a response body like below (your ID will differ).
1{2 "data": {3 "createPost": {4 "id": "8ad633ca-1f88-419e-ba0d-c66f83258631",5 "title": "New Post",6 "description": "This is a new post",7 "author": "John Doe",8 "publicationDate": "2023-06-23"9 }10 }11}
jsonGet All Posts
Now we have a post in our database, let’s rerun the `getAllPosts`
query we ran a moment ago but this time instead of an empty array being returned to us, we should get an array of the posts that exist in our database which will look like this.
1{2 "data": {3 "getAllPosts": [4 {5 "id": "8ad633ca-1f88-419e-ba0d-c66f83258631",6 "title": "New Post",7 "description": "This is a new post",8 "author": "John Doe",9 "publicationDate": "2023-06-23"10 }11 ]12 }13}
jsonGet a Single Post
After you have retrieved all of your posts, copy the ID of a post that exists in your database and we’ll use that to retrieve a single post from our database using `getPost`
. So replace your body with the one below and make sure to update the “id” variable to be your post ID.
1{2 "query": "query getPost($id: String!) {getPost(id: $id) {id title description author publicationDate}}",3 "variables": {4 "id": "YOUR_POST_ID"5 }6}
jsonThen send your request and you should get a `200 OK`
response with a body containing the data of the post ID you requested.
1{2 "data": {3 "getPost": {4 "id": "8ad633ca-1f88-419e-ba0d-c66f83258631",5 "title": "New Post",6 "description": "This is a new post",7 "author": "John Doe",8 "publicationDate": "2023-06-23"9 }10 }11}
jsonDelete a Post
Finally, let’s test our `deletePost`
endpoint. To do this, replace your request body with the one below and once again, update your “id” variable to point to the post ID you’d like to delete.
1{2 "query": "mutation deletePost($id: ID!) {deletePost(id: $id) }",3 "variables": {4 "id": "YOUR_POST_ID"5 }6}
jsonThen press “Send” and you should get a `200 OK`
response with a body containing the ID of the post that was deleted.
1{2 "data": {3 "deletePost": "8ad633ca-1f88-419e-ba0d-c66f83258631"4 }5}
jsonAt this point, if you wanted to be 100% sure the post was deleted you could rerun the `getAllPosts`
query from earlier and make sure it returns an array that doesn’t contain the post with the ID you deleted.
If you’ve successfully followed along and got all the same response statuses and bodies as shown 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 as well as tidy up the resources we provisioned.
Closing Thoughts
In this post, we’ve looked at how to create a GraphQL API using AWS AppSync that allows us to create, delete, and retrieve blog posts from a DynamoDB table using the AWS CDK. We’ve also covered how we can test our deployed API using Postman as as well why we might want to use a GraphQL API over a more traditional REST API.
If you’d like to see the complete example code for this tutorial, then you can see it over on the GitHub repository here.
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
*NOTE: Once you’re finished with this CDK project, make sure to remove it from your AWS account to ensure you don’t get billed for it by running
`cdk destroy`
.*