Creating an API with AWS AppSync and DynamoDB: A Step-by-Step Guide

---- views

In today's fast-paced technology landscape, building a powerful API that can handle the demands of modern applications has become a necessity.

AWS AppSync and DynamoDB are two powerful services from Amazon Web Services that make building APIs a breeze.

With AppSync, you can develop GraphQL APIs with ease, and DynamoDB provides fast and predictable performance with seamless scalability.

In this step-by-step guide, we'll walk you through the process of creating a robust API using AWS AppSync and DynamoDB. We'll cover everything from setting up your API with AppSync to defining your schema, connecting to a DynamoDB table, adding resolvers, and testing your API. By the end of this guide, you'll have the knowledge and skills necessary to build powerful GraphQL APIs that can handle the demands of modern applications.

So, let's dive in and get started!

Step 1: Set up the DynamoDB table

We're going to create a simple ToDo application.

Each ToDo will be recorded in this DynamoDB table:

const todoTable = new dynamodb.Table(this, "ToDoTable", {
    billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
    removalPolicy: cdk.RemovalPolicy.DESTROY,
});

Step 2: Defined the GraphQL schema

Once we have your DynamoDB table set up, we'll need to define the API schema.

The schema defines the types of data that the API will return and the operations that it supports.

We can then define your schema using the GraphQL schema language.

Let's define the schema in the schema.graphql file:

schema {
  query: Query
  mutation: Mutation
}

type Query {
  todos(limit: Int, nextToken: String): ToDoPage!
}

type Mutation {
  addTodo(newToDo: ToDoInput!): ToDo!
  completeTodo(id: String!): ToDo!
  deleteTodo(id: String!): Boolean!
}

type ToDo {
  id: ID!
  name: String!
  description: String!
  completed: Boolean!
}

type ToDoPage {
  items: [ToDo!]
  nextToken: String
}

input ToDoInput {
  name: String!
  description: String!
}

Step 3: Create the AWS AppSync API

With the schema defined, we can now create the AppSync API:

const graphqlApi = new appsync.GraphqlApi(this, "ToDoAPI", {
    name: "ToDo API",
    schema: appsync.SchemaFile.fromAsset(
        path.join(__dirname, "schema.graphql")
    ),
    authorizationConfig: {
        defaultAuthorization: {
            authorizationType: appsync.AuthorizationType.API_KEY,
            apiKeyConfig: {
                description: "public key for getting data",
                expires: cdk.Expiration.after(cdk.Duration.days(30)),
                name: "API Token",
            },
        },
    }
});

Step 4: Add resolvers

Resolvers are functions that map GraphQL operations to the underlying data sources.

In other words, they tell your API how to retrieve and manipulate data in your DynamoDB table.

In AWS AppSync, you can define your resolver using the VTL (Velocity Template Language) syntax.

Connect the API to the DynamoDB table with a data source

First we need to let the API know it needs to connect the DnamoDB table with a data source:

const todoTableDataSource = graphqlApi.addDynamoDbDataSource(
    "ToDoTableDataSource",
    todoTable
);

Query todos

While querying todos could return the whole list of todos, it could quickly become inefficient if the list is large.

Instead our API allows us to return a paged list of todos:

type Query {
  todos(limit: Int, nextToken: String): ToDoPage!
}

type ToDoPage {
  items: [ToDo!]
  nextToken: String
}

We just have to provide a limit argument as the page size, and the nextToken will be returned, so we can query for the next page.

Here is how it is implemented:

todoTableDataSource.createResolver("QueryToDosResolver", {
    typeName: "Query",
    fieldName: "todos",
    requestMappingTemplate: appsync.MappingTemplate.fromString(`
        #set( $limit = $util.defaultIfNull($context.args.limit, 100) )
        #set( $ListRequest = {
            "version": "2018-05-29",
            "limit": $limit
        } )
        #if( $context.args.nextToken )
            #set( $ListRequest.nextToken = $context.args.nextToken )
        #end
        $util.qr($ListRequest.put("operation", "Scan"))
        $util.toJson($ListRequest)
    `),
    responseMappingTemplate: appsync.MappingTemplate.fromString(`
        #if($ctx.error)
            $util.error($ctx.error.message, $ctx.error.type)
        #else
            $util.toJson($ctx.result)
        #end
    `),
});

Notice that we assume a default limit of 100 if none is provided.

Mutation addTodo

The addTodo mutation leverages the dynamoDbPutItem method to insert an item in our DynamoDB table.

It's worth mentionning that we let AppSync generate the ToDo ID by calling the auto() function on our partition key.

todoTableDataSource.createResolver("MutationAddToDoResolver", {
    typeName: "Mutation",
    fieldName: "addTodo",
    requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(
        appsync.PrimaryKey.partition("id").auto(),
        appsync.Values.projecting("newToDo").attribute("completed").is("false")
    ),
    responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});

Mutation completeTodo

todoTableDataSource.createResolver("MutationCompleteToDoResolver", {
    typeName: "Mutation",
    fieldName: "completeTodo",
    requestMappingTemplate: appsync.MappingTemplate.fromString(`
    {
        "version": "2018-05-29",
        "operation" : "UpdateItem",
        "key" : {
            "id": $util.dynamodb.toDynamoDBJson($context.arguments.id),
        },
        "update" : {
            "expression": "set #completed = :completed",
            "expressionNames": {
            "#completed": "completed",
            },
            "expressionValues": {
            ":completed": $util.dynamodb.toDynamoDBJson(true),
            }
        },
        "condition": {
        "expression": "attribute_exists(id)"
        }
    }`),
    responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});

Mutation deleteTodo

todoTableDataSource.createResolver("MutationDeleteToDoResolver", {
    typeName: "Mutation",
    fieldName: "deleteTodo",
    requestMappingTemplate: appsync.MappingTemplate.dynamoDbDeleteItem(
        "id",
        "id"
    ),
    responseMappingTemplate: appsync.MappingTemplate.fromString(`
        #if($ctx.error)
            $util.error($ctx.error.message, $ctx.error.type)
        #else
            true
        #end
    `),
});

Testing the API

To test the AppSync API, log in the AWS console and go to the AppSync service.

In the AppSync console, you can select your API and select Queries from the menu.

You'll be presented with a GraphQL editor.

Let's try to use our API.

Testing the addTodo mutation

First create a Todo Item with the addTodo mutation:

GraphQL mutation to add a ToDo item

In my case, this mutation created a ToDo item with a particular ID. We'll use this ID to further explore the API. Notice how the ToDo is not completed by default.

Testing the completeTodo mutation

Next, let's complete the ToDo with the completeTodo mutation:

GraphQL mutation to complete a ToDo item

The returned ToDo now has the attribute completed set to true.

Testing the todos query

We've added and completed a ToDo, now let's try to fetch the list of ToDos with the todos query:

GraphQL query to fetch the list of ToDos

This query returns the list ToDos.

Testing the deleteTodo mutation

Finally, let's clean up our test by deleting the ToDo we created with the deleteToDo mutation:

GraphQL mutation to delete a ToDo item

Conclusion

Creating a fast and scalable API is critical for any modern application, and AWS AppSync and DynamoDB make this process simple and efficient.

By following the step-by-step guide outlined in this article, you can quickly set up a powerful API that can handle the demands of your application.

With AppSync handling the heavy lifting of securely connecting to data sources and DynamoDB providing fast and predictable performance with seamless scalability, you can easily build robust GraphQL APIs that can handle the demands of modern applications.

So, whether you're building a mobile app, a web app, or a desktop application, AWS AppSync and DynamoDB are powerful tools that can help you create an API that will meet your needs.

Source code available on github.