Serverless GraphQL API
GraphQL on serverless gives you flexible data fetching with per-request billing and automatic scaling. Whether you use AWS AppSync for a fully managed GraphQL service or deploy a custom Apollo/Yoga server on Lambda, we set up your GraphQL API with proper schema design, efficient resolvers, DataLoader for N+1 prevention, caching, real-time subscriptions, and deployment automation.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
AWS AppSync Setup
AppSync is the fastest path to a production GraphQL API on AWS. We configure it with Terraform, connect data sources, and set up caching and authentication.
resource "aws_appsync_graphql_api" "main" {
name = "${var.project}-api-${var.env}"
authentication_type = "AMAZON_COGNITO_USER_POOLS"
schema = file("schema.graphql")
user_pool_config {
user_pool_id = aws_cognito_user_pool.main.id
default_action = "ALLOW"
aws_region = var.region
}
additional_authentication_provider {
authentication_type = "API_KEY"
}
log_config {
cloudwatch_logs_role_arn = aws_iam_role.appsync_logs.arn
field_log_level = "ERROR"
}
xray_enabled = true
}
# DynamoDB data source
resource "aws_appsync_datasource" "orders" {
api_id = aws_appsync_graphql_api.main.id
name = "OrdersTable"
type = "AMAZON_DYNAMODB"
service_role_arn = aws_iam_role.appsync_dynamodb.arn
dynamodb_config {
table_name = aws_dynamodb_table.orders.name
region = var.region
}
}
# VTL resolver for getOrder query
resource "aws_appsync_resolver" "get_order" {
api_id = aws_appsync_graphql_api.main.id
type = "Query"
field = "getOrder"
data_source = aws_appsync_datasource.orders.name
request_template = <<-VTL
{
"version": "2018-05-29",
"operation": "GetItem",
"key": {
"PK": {"S": "ORDER#$ctx.args.id"},
"SK": {"S": "META"}
}
}
VTL
response_template = "$util.toJson($ctx.result)"
caching_config {
ttl = 60
caching_keys = ["$context.arguments.id"]
}
}AppSync resolvers connect directly to DynamoDB, Lambda, HTTP endpoints, or RDS via VTL templates or JavaScript resolvers. We use pipeline resolvers for complex operations that need multiple data sources. Caching is configured per-resolver with TTL and cache key expressions.
Custom GraphQL on Lambda
For more control over the GraphQL runtime, we deploy Apollo Server or GraphQL Yoga on Lambda with proper context, DataLoader, and error handling.
// src/server.ts — GraphQL Yoga on Lambda
import { createSchema, createYoga } from 'graphql-yoga';
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { createContext } from './context';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
const yoga = createYoga<{ event: APIGatewayProxyEventV2 }>({
schema: createSchema({ typeDefs, resolvers }),
context: ({ event }) => createContext(event),
graphiql: process.env.ENVIRONMENT !== 'production',
});
// Lambda handler
export const handler = async (
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
const response = await yoga.fetch(
`https://lambda.local${event.rawPath}`,
{
method: event.requestContext.http.method,
headers: event.headers as HeadersInit,
body: event.body,
},
{ event }
);
return {
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
body: await response.text(),
isBase64Encoded: false,
};
};
// src/context.ts — DataLoader setup
import DataLoader from 'dataloader';
export function createContext(event: APIGatewayProxyEventV2) {
const userId = event.requestContext.authorizer?.lambda?.userId;
const tenantId = event.requestContext.authorizer?.lambda?.tenantId;
return {
userId,
tenantId,
loaders: {
users: new DataLoader(async (ids: readonly string[]) => {
const users = await batchGetUsers(ids as string[]);
return ids.map(id => users.find(u => u.id === id) || null);
}),
projects: new DataLoader(async (ids: readonly string[]) => {
const projects = await batchGetProjects(ids as string[]);
return ids.map(id => projects.find(p => p.id === id) || null);
}),
},
};
}DataLoader batches and caches database lookups within a single request, solving the N+1 query problem. A new DataLoader instance is created per-request to prevent cross-request data leakage. We initialize the DataLoader context outside heavy handlers to keep cold start overhead minimal.
Schema Design & Validation
We design your GraphQL schema with pagination, error handling, and input validation patterns that scale as your API grows.
# schema.graphql
type Query {
# Cursor-based pagination
projects(first: Int = 25, after: String): ProjectConnection!
project(id: ID!): Project
me: User!
}
type Mutation {
createProject(input: CreateProjectInput!): CreateProjectPayload!
updateProject(id: ID!, input: UpdateProjectInput!): UpdateProjectPayload!
deleteProject(id: ID!): DeleteProjectPayload!
}
type Subscription {
projectUpdated(projectId: ID!): Project!
taskStatusChanged(projectId: ID!): Task!
}
# Relay-style connection for pagination
type ProjectConnection {
edges: [ProjectEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProjectEdge {
cursor: String!
node: Project!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Payload pattern for mutations
type CreateProjectPayload {
project: Project
errors: [UserError!]!
}
type UserError {
field: String!
message: String!
}
input CreateProjectInput {
name: String!
description: String
teamId: ID!
}We follow the Relay connection specification for pagination, use payload types for mutations that include user-facing errors (separate from GraphQL errors), and define input types for all mutations. The schema is the contract — we add ESLint rules to prevent breaking changes in CI.
Subscriptions & Real-Time
AppSync provides managed WebSocket subscriptions out of the box. For custom GraphQL servers, we integrate with API Gateway WebSocket API or use server-sent events.
# AppSync subscription resolver — triggered by mutation
resource "aws_appsync_resolver" "on_project_updated" {
api_id = aws_appsync_graphql_api.main.id
type = "Subscription"
field = "projectUpdated"
data_source = aws_appsync_datasource.none.name
request_template = <<-VTL
{
"version": "2018-05-29",
"payload": $util.toJson($context.arguments)
}
VTL
response_template = "$util.toJson($context.result)"
}
# Mutation resolver triggers subscription
resource "aws_appsync_resolver" "update_project" {
api_id = aws_appsync_graphql_api.main.id
type = "Mutation"
field = "updateProject"
kind = "PIPELINE"
pipeline_config {
functions = [
aws_appsync_function.validate_input.function_id,
aws_appsync_function.update_dynamodb.function_id,
aws_appsync_function.publish_event.function_id,
]
}
request_template = "{}"
response_template = "$util.toJson($ctx.result)"
}AppSync handles subscription connection management, authentication, and message fanout. Clients connect via WebSocket and receive real-time updates filtered by their subscription arguments. For custom servers, we implement SSE (Server-Sent Events) on Lambda Function URLs for simpler real-time without WebSocket complexity. The subscription filter ensures clients only receive events for resources they have access to — tenant isolation is enforced at the subscription layer.
Why Anubiz Engineering
Ready to get started?
Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.