Niraj Chauhan

Niraj Chauhan

#father #husband #SoftwareCraftsman #foodie #gamer #OnePiece #naruto

Going Full Serverless with AWS AppSync and DynamoDB using TypeScript CDK

Posted by on

In this blog post, we’ll explore how to build a fully serverless architecture using AWS AppSync and DynamoDB, eliminating the need to create and manage REST APIs manually using frameworks like Express or Fastify. By leveraging AppSync’s GraphQL capabilities and DynamoDB as our data store, we’ll achieve a scalable and efficient solution for managing orders in an e-commerce system. We’ll also use the AWS Cloud Development Kit (CDK) in TypeScript to define our infrastructure.

servers

What is Appsync?

AppSync is a fully managed service that simplifies building scalable GraphQL APIs by automatically handling the backend data sources like DynamoDB, Lambda, and more. One of its strengths lies in the ease of defining real-time, flexible APIs without the need for managing server infrastructure.

What is DynamoDB?

DynamoDB is a fully managed NoSQL database that offers high scalability and performance. One crucial aspect of DynamoDB is its use of indexes for efficient data querying. When planning your data model, it’s essential to define your indexes upfront (e.g., primary keys and secondary indexes), as these determine how you’ll query the data. Without proper indexing, you’ll be forced to rely on scan operations, which are slower and more expensive. Additionally, once your table is created, adding indexes later is not possible, making it critical to plan your access patterns and indexing from the start.

The GraphQL Schema

The first step in any GraphQL-based solution is defining the schema. Here’s a sample schema for an order management system:

type Order {
  orderId: String!
  customerId: String!
  orderDate: Int!
  totalAmount: String!
  status: String!
  productCategory: String!
}

type Query {
  getOrder(orderId: String!): [Order]
  getOrdersByCustomerId(customerId: Int!): [Order]
  getOrdersByCustomerAndDateRange(customerId: String!, startTime: String!, endTime: String!): [Order]
}

type Mutation {
  createOrder(
    orderId: String!
    customerId: String!
    orderDate: Int!
    totalAmount: String!
    status: String!
    productCategory: String!
  ): Order
}

schema {
  query: Query
  mutation: Mutation
}

This schema outlines three main queries and a mutation to handle the core operations for managing orders:

  • Queries: Fetch orders by order ID, customer ID, and a date range.
  • Mutations: Create a new order.

Let’s Start Developing

Now that we’ve laid the foundation with the GraphQL schema and an understanding of the architecture, it’s time to start building. We’ll begin by setting up the DynamoDB table, which will serve as the core data storage for our order management system. After that, we’ll move on to creating the AppSync API and setting up the resolvers. Let’s dive into the development process step-by-step!

Step 1: Creating the DynamoDB Table

The first step in building our infrastructure is creating a DynamoDB table. DynamoDB is a NoSQL database service that allows you to scale seamlessly based on traffic. We will create a table named orders that uses orderId as the partition key and orderDate as the sort key.

const ordersTable = new dynamodb.Table(this, "OrdersTable", {
  tableName: "orders",
  partitionKey: { name: "orderId", type: dynamodb.AttributeType.STRING },
  sortKey: { name: "orderDate", type: dynamodb.AttributeType.STRING },
  removalPolicy: cdk.RemovalPolicy.RETAIN,
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  pointInTimeRecovery: true,
  encryption: dynamodb.TableEncryption.AWS_MANAGED,
  stream: dynamodb.StreamViewType.NEW_IMAGE,
});
  • BillingMode: We’re using PAY_PER_REQUEST to allow automatic scaling without provisioning capacity upfront.
  • Point-in-Time Recovery: This ensures data recovery for up to 35 days, adding resilience to the system.
  • Encryption: We’re using AWS-managed encryption to secure data at rest.

Step 2: Creating Global Secondary Indexes (GSI)

To efficiently query orders by customerId, we’ll create a Global Secondary Index (GSI). This index uses customerId as the partition key and orderDate as the sort key.

ordersTable.addGlobalSecondaryIndex({
  indexName: "customerIdIndex",
  partitionKey: { name: "customerId", type: dynamodb.AttributeType.STRING },
  sortKey: { name: "orderDate", type: dynamodb.AttributeType.STRING },
});

GSI’s allow for querying DynamoDB in different ways, which is crucial for queries like retrieving all orders for a specific customer within a date range.

Step 3: Creating the AppSync API

Next, we create the AppSync API. AppSync manages the GraphQL layer, exposing our operations and automatically scaling based on traffic.

const api = new appsync.GraphqlApi(this, "Api", {
  name: "orders-api",
  schema: appsync.SchemaFile.fromAsset(join(__dirname, "schema.graphql")),
  authorizationConfig: {
    defaultAuthorization: {
      authorizationType: appsync.AuthorizationType.API_KEY,
      apiKeyConfig: {
        expires: cdk.Expiration.after(cdk.Duration.days(365)),
      },
    },
  },
  xrayEnabled: true,
  logConfig: {
    fieldLogLevel: appsync.FieldLogLevel.ALL,
    excludeVerboseContent: false,
    retention: logs.RetentionDays.ONE_WEEK,
  },
});
  • Authorization Configuration:
    • Default Authorization Type: We are using API_KEY as the default authorization method.
    • API Key Expiry: By default, AppSync API keys expire after 7 days. Here, we extend the expiration to 365 days using cdk.Expiration.after(cdk.Duration.days(365)). This value can be adjusted to meet different expiration requirements.
    • Additional Authorization Options: You can also use other authorization methods such as Cognito for user-level access control, IAM, and OpenID Connect for more advanced security needs.
  • X-Ray: AppSync’s X-Ray integration allows us to trace requests for monitoring and debugging.
  • Logging: Logging configurations ensure we can troubleshoot by capturing field-level logs.

Step 4: Creating Resolvers

Resolvers in AppSync map incoming GraphQL requests to DynamoDB operations. Below are the resolvers for querying and mutating data.

Resolver: Get Order by orderId

This resolver queries the table using the orderId as the partition key.

ordersDataSource.createResolver("getOrder", {
  typeName: "Query",
  fieldName: "getOrder",
  requestMappingTemplate: appsync.MappingTemplate.dynamoDbQuery(appsync.KeyCondition.eq("orderId", "orderId")),
  responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});

Resolver: Get Orders by customerId

This resolver queries the table using the customerId. This is possible because we have created a GSI on the customerId field.

ordersDataSource.createResolver("getOrdersByCustomerId", {
  typeName: "Query",
  fieldName: "getOrdersByCustomerId",
  requestMappingTemplate: appsync.MappingTemplate.dynamoDbQuery(
    appsync.KeyCondition.eq("customerId", "customerId"),
    "customerIdIndex"
  ),
  responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});

Resolver: Get Orders by Customer and Date Range

This resolver queries the DynamoDB table to retrieve orders for a specific customer within a given date range. This efficient query is made possible by the Global Secondary Index (GSI) we created on the customerId field, along with the orderDate as the sort key, enabling precise filtering based on both customer ID and date.

ordersDataSource.createResolver("getOrdersByCustomerAndDateRange", {
  typeName: "Query",
  fieldName: "getOrdersByCustomerAndDateRange",
  requestMappingTemplate: appsync.MappingTemplate.fromString(`
    {
      "version": "2017-02-28",
      "operation": "Query",
      "index": "customerIdIndex",
      "query": {
        "expression": "#customerId = :customerId AND #orderDate BETWEEN :startTime AND :endTime",
        "expressionNames": {
          "#customerId": "customerId",
          "#orderDate": "orderDate"
        },
        "expressionValues": {
          ":customerId": $util.dynamodb.toDynamoDBJson($ctx.args.customerId),
          ":startTime": $util.dynamodb.toDynamoDBJson($ctx.args.startTime),
          ":endTime": $util.dynamodb.toDynamoDBJson($ctx.args.endTime)
        }
      }
    }
  `),
  responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(),
});

Resolver: Create a New Order

The mutation to create a new order uses dynamoDbPutItem to store the order in DynamoDB.

ordersDataSource.createResolver("createOrder", {
  typeName: "Mutation",
  fieldName: "createOrder",
  requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem(
    appsync.PrimaryKey.partition("orderId").is("orderId").sort("orderDate").is("orderDate"),
    appsync.Values.projecting()
  ),
  responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
});

Deploying and Using the API

That’s it! Once this setup is deployed using the cdk deploy command, your infrastructure will be up and running. You’ll need to get the GraphQL API URL and API Key from AWS console. With this information, you can start interacting with your API immediately. Simply use fetch, axios or any GraphQL client to make queries and mutations to your AppSync API.

Conclusion: The Power of Full Serverless

By combining AWS AppSync and DynamoDB, we’ve built a fully serverless solution that scales automatically, handles our traffic, and simplifies development.

No manual API creation: AppSync abstracts the API layer, reducing the need to manage Express.js or other server frameworks manually. You define your GraphQL schema, and AppSync takes care of the rest.

Seamless integrations: AppSync natively integrates with DynamoDB, Aurora, Lambda, and other AWS services, enabling powerful and flexible serverless solutions with minimal overhead.