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.
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.