Event Handler for AWS AppSync GraphQL APIs simplifies routing and processing of events in AWS Lambda functions. It allows you to define resolvers for GraphQL types and fields, making it easier to handle GraphQL requests without the need for complex VTL or JavaScript templates.
stateDiagram-v2
direction LR
EventSource: AWS Lambda Event Sources
EventHandlerResolvers: AWS AppSync invocation
LambdaInit: Lambda invocation
EventHandler: Event Handler
EventHandlerResolver: Route event based on GraphQL type/field keys
YourLogic: Run your registered resolver function
EventHandlerResolverBuilder: Adapts response to Event Source contract
LambdaResponse: Lambda response
state EventSource {
EventHandlerResolvers
}
EventHandlerResolvers --> LambdaInit
LambdaInit --> EventHandler
EventHandler --> EventHandlerResolver
state EventHandler {
[*] --> EventHandlerResolver: app.resolve(event, context)
EventHandlerResolver --> YourLogic
YourLogic --> EventHandlerResolverBuilder
}
EventHandler --> LambdaResponse
Key Features¶
Direct Lambda Resolver. A custom AppSync Resolver that bypasses Apache Velocity Template (VTL) and JavaScript templates, and automatically maps your function's response to a GraphQL field.
Batching resolvers. A technique that allows you to batch multiple GraphQL requests into a single Lambda function invocation, reducing the number of calls and improving performance.
Getting started¶ Tip: Designing GraphQL Schemas for the first time?Visit AWS AppSync schema documentation to understand how to define types, nesting, and pagination.
Required resources¶You must have an existing AppSync GraphQL API and IAM permissions to invoke your Lambda function. That said, there is no additional permissions to use Event Handler as routing requires no dependency (standard library).
This is the sample infrastructure we will be using for the initial examples with an AppSync Direct Lambda Resolver.
gettingStartedSchema.graphqltemplate.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
schema {
query: Query
mutation: Mutation
}
type Query {
# these are fields you can attach resolvers to (type_name: Query, field_name: getTodo)
getTodo(id: ID!): Todo
listTodos: [Todo]
}
type Mutation {
createTodo(title: String!): Todo
}
type Todo {
id: ID!
userId: String
title: String
completed: Boolean
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Hello world Direct Lambda Resolver
Globals:
Function:
Timeout: 5
MemorySize: 256
Runtime: nodejs22.x
Environment:
Variables:
# Powertools for AWS Lambda (TypeScript) env vars: https://docs.powertools.aws.dev/lambda/typescript/latest/environment-variables/
POWERTOOLS_LOG_LEVEL: INFO
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
POWERTOOLS_SERVICE_NAME: example
Resources:
TodosFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
CodeUri: hello_world
# IAM Permissions and Roles
AppSyncServiceRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "appsync.amazonaws.com"
Action:
- "sts:AssumeRole"
InvokeLambdaResolverPolicy:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: "DirectAppSyncLambda"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "lambda:invokeFunction"
Resource:
- !GetAtt TodosFunction.Arn
Roles:
- !Ref AppSyncServiceRole
# GraphQL API
TodosApi:
Type: "AWS::AppSync::GraphQLApi"
Properties:
Name: TodosApi
AuthenticationType: "API_KEY"
XrayEnabled: true
TodosApiKey:
Type: AWS::AppSync::ApiKey
Properties:
ApiId: !GetAtt TodosApi.ApiId
TodosApiSchema:
Type: "AWS::AppSync::GraphQLSchema"
Properties:
ApiId: !GetAtt TodosApi.ApiId
DefinitionS3Location: ../src/getting_started_schema.graphql
Metadata:
cfn-lint:
config:
ignore_checks:
- W3002 # allow relative path in DefinitionS3Location
# Lambda Direct Data Source and Resolver
TodosFunctionDataSource:
Type: "AWS::AppSync::DataSource"
Properties:
ApiId: !GetAtt TodosApi.ApiId
Name: "HelloWorldLambdaDirectResolver"
Type: "AWS_LAMBDA"
ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
LambdaConfig:
LambdaFunctionArn: !GetAtt TodosFunction.Arn
ListTodosResolver:
Type: "AWS::AppSync::Resolver"
Properties:
ApiId: !GetAtt TodosApi.ApiId
TypeName: "Query"
FieldName: "listTodos"
DataSourceName: !GetAtt TodosFunctionDataSource.Name
GetTodoResolver:
Type: "AWS::AppSync::Resolver"
Properties:
ApiId: !GetAtt TodosApi.ApiId
TypeName: "Query"
FieldName: "getTodo"
DataSourceName: !GetAtt TodosFunctionDataSource.Name
CreateTodoResolver:
Type: "AWS::AppSync::Resolver"
Properties:
ApiId: !GetAtt TodosApi.ApiId
TypeName: "Mutation"
FieldName: "createTodo"
DataSourceName: !GetAtt TodosFunctionDataSource.Name
Outputs:
TodosFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt TodosFunction.Arn
TodosApi:
Value: !GetAtt TodosApi.GraphQLUrl
Registering a resolver¶
You can register functions to match GraphQL types and fields with one of three methods:
onQuery()
- Register a function to handle a GraphQL Query type.onMutation()
- Register a function to handle a GraphQL Mutation type.resolver()
- Register a function to handle a GraphQL type and field.What is a type and field?
A type would be a top-level GraphQL Type like Query
, Mutation
, Todo
. A GraphQL Field would be listTodos
under Query
, createTodo
under Mutation
, etc.
The function receives the parsed arguments from the GraphQL request as its first parameter. We also take care of parsing the response or catching errors and returning them in the expected format.
Query resolver¶When registering a resolver for a Query
type, you can use the onQuery()
method. This method allows you to define a function that will be invoked when a GraphQL Query is made.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
app.onQuery<{ id: string }>('getTodo', async ({ id }) => {
logger.debug('Resolving todo', { id });
// Simulate fetching a todo from a database or external service
return {
id,
title: 'Todo Title',
completed: false,
};
});
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Mutation resolver¶
Similarly, you can register a resolver for a Mutation
type using the onMutation()
method. This method allows you to define a function that will be invoked when a GraphQL Mutation is made.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import {
AppSyncGraphQLResolver,
makeId,
} from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
app.onMutation<{ title: string }>('createTodo', async ({ title }) => {
logger.debug('Creating todo', { title });
const todoId = makeId();
// Simulate creating a todo in a database or external service
return {
id: todoId,
title,
completed: false,
};
});
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Generic resolver¶
When you want to have more control over the type and field, you can use the resolver()
method. This method allows you to register a function for a specific GraphQL type and field including custom types.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
app.resolver(
async () => {
logger.debug('Resolving todos');
// Simulate fetching a todo from a database or external service
return [
{
id: 'todo-id',
title: 'Todo Title',
completed: false,
},
{
id: 'todo-id-2',
title: 'Todo Title 2',
completed: true,
},
];
},
{
fieldName: 'listTodos',
typeName: 'Query',
}
);
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Using decorators¶
If you prefer to use the decorator syntax, you can instead use the same methods on a class method to register your handlers. Learn more about how Powertools for TypeScript supports decorators.
Using decorators to register a resolver1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import {
AppSyncGraphQLResolver,
makeId,
} from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
class Lambda implements LambdaInterface {
@app.onMutation('createTodo')
public async createTodo({ title }: { title: string }) {
logger.debug('Creating todo', { title });
const todoId = makeId();
// Simulate creating a todo in a database or external service
return {
id: todoId,
title,
completed: false,
};
}
@app.onQuery('getTodo')
public async getTodo({ id }: { id: string }) {
logger.debug('Resolving todo', { id });
// Simulate fetching a todo from a database or external service
return {
id,
title: 'Todo Title',
completed: false,
};
}
@app.resolver({
fieldName: 'listTodos',
typeName: 'Query',
})
public async listTodos() {
logger.debug('Resolving todos');
// Simulate fetching a todo from a database or external service
return [
{
id: 'todo-id',
title: 'Todo Title',
completed: false,
},
{
id: 'todo-id-2',
title: 'Todo Title 2',
completed: true,
},
];
}
async handler(event: unknown, context: Context) {
return app.resolve(event, context, { scope: this }); // (1)!
}
}
const lambda = new Lambda();
export const handler = lambda.handler.bind(lambda);
this
to ensure the correct class scope is propageted to the route handler functions.When working with AWS AppSync Scalar types, you might want to generate the same values for data validation purposes.
For convenience, the most commonly used values are available as helper functions within the module.
Creating key scalar values1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
import {
AppSyncGraphQLResolver,
awsDate,
awsDateTime,
awsTime,
awsTimestamp,
makeId,
} from '@aws-lambda-powertools/event-handler/appsync-graphql';
import type { Context } from 'aws-lambda';
const app = new AppSyncGraphQLResolver();
app.resolver(
async ({ title, content }) => {
// your business logic here
return {
title,
content,
id: makeId(),
createdAt: awsDateTime(),
updatedAt: awsDateTime(),
timestamp: awsTimestamp(),
time: awsTime(),
date: awsDate(),
};
},
{
fieldName: 'createTodo',
typeName: 'Mutation',
}
);
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Here's a table with their related scalar as a quick reference:
Scalar type Scalar function Sample value IDmakeId
e916c84d-48b6-484c-bef3-cee3e4d86ebf
AWSDate awsDate
2022-07-08Z
AWSTime awsTime
15:11:00.189Z
AWSDateTime awsDateTime
2022-07-08T15:11:00.189Z
AWSTimestamp awsTimestamp
1657293060
Advanced¶ Nested mappings¶
You can register the same route handler multiple times to resolve fields with the same return value.
Nested Mappings ExampleNested Mappings Schema
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
type Location = {
id: string;
name: string;
description?: string;
};
const locationsResolver = async (): Promise<Location[]> => {
logger.debug('Resolving locations');
// Simulate fetching locations from a database or external service
return [
{
id: 'loc1',
name: 'Location One',
description: 'First location description',
},
{
id: 'loc2',
name: 'Location Two',
description: 'Second location description',
},
];
};
app.resolver(locationsResolver, {
fieldName: 'locations',
typeName: 'Merchant',
});
app.resolver(locationsResolver, {
fieldName: 'listLocations', // (1)!
});
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
typeName
defaults to Query
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
schema {
query: Query
}
type Query {
listLocations: [Location]
}
type Location {
id: ID!
name: String!
description: String
address: String
}
type Merchant {
id: String!
name: String!
description: String
locations: [Location]
}
Accessing Lambda context and event¶
You can access the original Lambda event or context for additional information. These are passed to the handler function as optional arguments.
Access event and context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
});
const app = new AppSyncGraphQLResolver({ logger });
app.onQuery<{ id: string }>('getTodo', async ({ id }, { event, context }) => {
const { headers } = event.request; // (1)!
const { awsRequestId } = context;
logger.info('headers', { headers, awsRequestId });
return {
id,
title: 'Todo Title',
completed: false,
};
});
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
event
parameter contains the original AppSync event and has type AppSyncResolverEvent
from the @types/aws-lambda
.By default, the utility uses the global console
logger and emits only warnings and errors.
You can change this behavior by passing a custom logger instance to the AppSyncGraphQLResolver
or Router
and setting the log level for it, or by enabling Lambda Advanced Logging Controls and setting the log level to DEBUG
.
When debug logging is enabled, the resolver will emit logs that show the underlying handler resolution process. This is useful for understanding how your handlers are being resolved and invoked and can help you troubleshoot issues with your event processing.
For example, when using the Powertools for AWS Lambda logger, you can set the LOG_LEVEL
to DEBUG
in your environment variables or at the logger level and pass the logger instance to the constructor to enable debug logging.
Debug loggingLogs output
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import { Logger } from '@aws-lambda-powertools/logger';
import {
correlationPaths,
search,
} from '@aws-lambda-powertools/logger/correlationId';
import type { Context } from 'aws-lambda';
const logger = new Logger({
serviceName: 'TodoManager',
logLevel: 'DEBUG',
correlationIdSearchFn: search,
});
const app = new AppSyncGraphQLResolver({ logger });
app.onQuery<{ id: string }>('getTodo', async ({ id }) => {
logger.debug('Resolving todo', { id });
// Simulate fetching a todo from a database or external service
return {
id,
title: 'Todo Title',
completed: false,
};
});
export const handler = async (event: unknown, context: Context) => {
logger.setCorrelationId(event, correlationPaths.APPSYNC_RESOLVER);
return app.resolve(event, context);
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
[
{
"level": "DEBUG",
"message": "Adding resolver for field Query.getTodo",
"timestamp": "2025-07-02T13:39:36.017Z",
"service": "service_undefined",
"sampling_rate": 0
},
{
"level": "DEBUG",
"message": "Looking for resolver for type=Query, field=getTodo",
"timestamp": "2025-07-02T13:39:36.033Z",
"service": "service_undefined",
"sampling_rate": 0,
"xray_trace_id": "1-68653697-0f1223120d19409c38812f01",
"correlation_id": "Root=1-68653697-3623822a02e171272e2ecfe4"
},
{
"level": "DEBUG",
"message": "Resolving todo",
"timestamp": "2025-07-02T13:39:36.033Z",
"service": "service_undefined",
"sampling_rate": 0,
"xray_trace_id": "1-68653697-0f1223120d19409c38812f01",
"correlation_id": "Root=1-68653697-3623822a02e171272e2ecfe4",
"id": "42"
}
]
Testing your code¶
You can test your resolvers by passing an event with the shape expected by the AppSync GraphQL API resolver.
Here's an example of how you can test your resolvers that uses a factory function to create the event shape:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
import type { Context } from 'aws-lambda';
import { describe, expect, it } from 'vitest';
import { handler } from './advancedNestedMappings.js';
const createEventFactory = (
fieldName: string,
args: Record<string, unknown>,
parentTypeName: string
) => ({
arguments: { ...args },
identity: null,
source: null,
request: {
headers: {
key: 'value',
},
domainName: null,
},
info: {
fieldName,
parentTypeName,
selectionSetList: [],
variables: {},
},
prev: null,
stash: {},
});
const onGraphqlEventFactory = (
fieldName: string,
typeName: 'Query' | 'Mutation',
args: Record<string, unknown> = {}
) => createEventFactory(fieldName, args, typeName);
describe('Unit test for AppSync GraphQL Resolver', () => {
it('returns the location', async () => {
// Prepare
const event = onGraphqlEventFactory('listLocations', 'Query');
// Act
const result = (await handler(event, {} as Context)) as Promise<unknown[]>;
// Assess
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: 'loc1',
name: 'Location One',
description: 'First location description',
});
expect(result[1]).toEqual({
id: 'loc2',
name: 'Location Two',
description: 'Second location description',
});
});
});
2025-08-14
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4