Build a GraphQL API with AWS AppSync

Khimananda Oli 6 min read Database
Build a GraphQL API with AWS AppSync

By Khimananda Oli | Last reviewed: August 2026

You need to build a GraphQL API with AWS AppSync when your application requires real-time data synchronization, offline capabilities, or flexible querying without managing server infrastructure. Unlike traditional REST endpoints on EC2, AppSync decouples the API layer from compute, handling caching, subscriptions, and authorization at the edge. This guide walks through provisioning a production-grade AppSync API using Terraform, connecting it to DynamoDB, and securing it with IAM, following the same infrastructure-as-code principles outlined in my practical Terraform guide.

Client AppsAWS AppSyncSchema + ResolversAuth + Cache + SubsDynamoDB
High-level architecture to build a GraphQL API with AWS AppSync as the managed gateway between clients and DynamoDB

How do you define an AppSync GraphQL schema?

The schema is the contract. When you build a GraphQL API with AWS AppSync, you define types in Schema Definition Language (SDL). A common mistake is copying REST resource structures directly into GraphQL; instead, model your types around client use cases. AppSync supports standard SDL with directives like @aws_cognito_user_pools for field-level authorization.

type Post {
  id: ID!
  title: String!
  content: String
  authorId: ID!
  createdAt: AWSDateTime!
}

type Query {
  getPost(id: ID!): Post
  listPosts(limit: Int, nextToken: String): PostConnection
}

type Mutation {
  createPost(input: CreatePostInput!): Post
}

type Subscription {
  onCreatePost: Post @aws_subscribe(mutations: ["createPost"])
}

type PostConnection {
  items: [Post]
  nextToken: String
}
  • AWS Scalars: Use AWSDateTime, AWSJSON, and AWSEmail for built-in validation and serialization.
  • Pagination: Always implement cursor-based pagination (nextToken) rather than offset-based for DynamoDB compatibility.
  • Subscriptions: Link mutations to subscriptions using @aws_subscribe; AppSync handles the WebSocket lifecycle automatically.

How do you provision AppSync infrastructure with Terraform?

Never click through the console for production APIs. Infrastructure as Code ensures reproducibility and audit trails. If you are new to this workflow, review AWS IAM best practices before creating roles. The following Terraform configuration provisions the API, data source, and resolver atomically.

resource "aws_appsync_graphql_api" "api" {
  name                = "blog-api"
  authentication_type = "AMAZON_COGNITO_USER_POOLS"

  user_pool_config {
    aws_region     = var.aws_region
    user_pool_id   = aws_cognito_user_pool.pool.id
    default_action = "ALLOW"
  }

  log_config {
    cloudwatch_logs_role_arn = aws_iam_role.appsync_log.arn
    field_log_level          = "ERROR"
  }
}

resource "aws_appsync_datasource" "posts_table" {
  api_id           = aws_appsync_graphql_api.api.id
  name             = "PostsTable"
  type             = "AMAZON_DYNAMODB"
  service_role_arn = aws_iam_role.appsync_dynamo.arn

  dynamodb_config {
    table_name = aws_dynamodb_table.posts.name
  }
}

Key considerations for 2026 deployments:

  1. Logging: Enable CloudWatch Logs at ERROR level minimum. ALL level generates excessive costs in high-traffic environments.
  2. IAM Roles: Create separate roles for logging and data source access. Never reuse the account root or admin roles.
  3. State Management: Store Terraform state in S3 with DynamoDB locking, never locally.
ClientAppSyncResolverDynamoDBGraphQL RequestEvaluate VTL/JSGetItem/PutItemMap ResponseJSON Response
Resolver execution sequence: request mapping, data source call, and response mapping within AWS AppSync

How do you write VTL resolvers for DynamoDB?

Velocity Template Language (VTL) remains the default for AppSync resolvers in 2026, though JavaScript resolvers are now GA. VTL is verbose but predictable. The request template transforms GraphQL arguments into DynamoDB operations; the response template maps results back to your schema.

Request Mapping Template

{
  "version": "2018-05-29",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($util.autoId())
  },
  "attributeValues": {
    "title": $util.dynamodb.toDynamoDBJson($ctx.args.input.title),
    "content": $util.dynamodb.toDynamoDBJson($ctx.args.input.content),
    "authorId": $util.dynamodb.toDynamoDBJson($ctx.identity.sub),
    "createdAt": $util.dynamodb.toDynamoDBJson($util.time.nowISO8601())
  },
  "condition": {
    "expression": "attribute_not_exists(id)"
  }
}

Response Mapping Template

#if($ctx.error)
  $util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)

In practice, always include condition expressions to prevent accidental overwrites. The $util.autoId() function generates ULIDs, which sort chronologically unlike UUIDs. For complex transformations, consider moving logic to Lambda, but remember that each Lambda invocation adds latency and cost.

How does AppSync compare to API Gateway with Lambda?

Choosing between AppSync and API Gateway depends on your data access patterns. I have deployed both extensively; neither is universally superior. Refer to when serverless actually makes sense for broader context on this decision.

CriteriaAWS AppSyncAPI Gateway + Lambda
Data FetchingClient-driven, single endpointServer-defined REST endpoints
Real-timeBuilt-in WebSocket subscriptionsRequires API Gateway WebSocket API
CachingPer-field TTL caching at edgeFull-response caching only
Offline SyncDataStore SDK handles conflictsCustom implementation required
Resolver LogicVTL/JS templates or LambdaLambda only
Cost ModelPer-query + data transferPer-request + Lambda duration
Best ForCRUD apps, mobile, real-time dashboardsComplex business logic, integrations

If your frontend team needs flexibility and you have straightforward CRUD operations, AppSync wins. If your backend requires heavy processing, third-party integrations, or non-HTTP protocols, stick with API Gateway.

Start: New API ProjectNeed Real-time / Offline?YesNoAWS AppSyncGraphQL + SubscriptionsAPI GatewayREST / Custom LogicUse DataStore SDKAdd Lambda Layers
Decision flowchart: choose AppSync for real-time and offline-first apps, API Gateway for complex integrations

How do you secure and monitor AppSync in production?

Security cannot be an afterthought. Configure Cognito User Pools for user-facing APIs or IAM for service-to-service calls. Enable WAF integration if exposing public endpoints. For monitoring, rely on CloudWatch Metrics for latency and error rates, but also enable X-Ray tracing to visualize resolver chains. In Nepal-based projects serving global users, deploy AppSync in the region closest to your primary user base; latency matters more than marginal cost differences.

  • Field-Level Auth: Use @aws_auth directives to restrict sensitive fields to specific Cognito groups.
  • Rate Limiting: AppSync has no native throttling; place CloudFront in front with rate-based rules.
  • Schema Validation: Run amplify codegen or similar in CI to catch breaking changes before deployment.
  • Backup: Enable DynamoDB PITR; AppSync itself is stateless and requires no backup.

Next Steps for Your AppSync Deployment

When you build a GraphQL API with AWS AppSync correctly, you gain a managed, scalable data layer that frees your team from boilerplate. Start with the Terraform configuration above, iterate on your schema with real client feedback, and instrument observability before your first production release. If you need help architecting a compliant, audit-ready serverless backend or optimizing an existing deployment, reach out to discuss your project.

Frequently Asked Questions

Use the AWS Amplify CLI or CDK to scaffold your schema and resolvers. This automates provisioning, authentication setup, and deployment pipelines for your GraphQL API within minutes.

AppSync charges per query and data transfer, eliminating server maintenance costs. For high-traffic production workloads, calculate break-even points against EC2 or Lambda expenses carefully before committing.

Yes. Define VTL or JavaScript resolvers mapping directly to your existing table structures without migration. Ensure partition keys align with your access patterns for optimal performance.

Yes. AppSync provides native WebSocket-based subscriptions using MQTT over WSS. Simply define subscription types in your schema matching mutation payloads for automatic event propagation.

Configure IAM, Cognito User Pools, OIDC, or Lambda authorizers. Always apply field-level authorization directives like @aws_cognito_user_pools to restrict sensitive data access granularly.

Prefer APPSYNC_JS runtime over legacy VTL for complex logic. It supports modern JavaScript, better debugging, unit testing locally, and npm package imports for shared utilities.

Implement batch resolvers using BatchGetItem for DynamoDB or SQL IN clauses for Aurora. Group requests by parent ID to reduce round trips significantly during list operations.

Yes. Use HTTP resolvers for external REST APIs or Lambda functions for custom backends. Configure proper timeout handling and retry policies for reliable third-party integrations.

Use the Amplify Mock API command or SAM local invoke for Lambda resolvers. Write unit tests against APPSYNC_JS handlers using standard Jest frameworks offline.

Enable built-in TTL caching at the API level for read-heavy queries. Use resolver-level caching with cache keys based on arguments to prevent redundant backend calls.

Enable CloudWatch Logs for resolver tracing and X-Ray for end-to-end latency analysis. Set alarms on error rates and p99 latency thresholds for proactive alerting.

Schema translation requires manual effort since AppSync uses SDL with AWS-specific directives. Rewrite resolvers from TypeScript to APPSYNC_JS or VTL format completely.

Use feature flags and deprecated directives for backward compatibility. Deploy schema updates via CI/CD pipelines with automated validation tests before applying to live environments.

No. Use presigned S3 URLs generated via separate Lambda or AppSync resolver instead. Return upload credentials in mutation response for direct client-to-S3 transfers.

The limit is 1MB for queries and mutations including headers. Paginate large datasets and compress responses when approaching this threshold to avoid truncation errors.