Route GraphQL queries, mutations, and custom resolvers for AWS AppSync without VTL or JavaScript templates.
class AppSyncGraphQLResolver {
constructor(options?: { logger?: GenericLogger });
resolve(event, context, options?: ResolveOptions): Promise<unknown>;
onQuery<TParams>(fieldName: string, handler: ResolverHandler<TParams>): void;
onQuery(fieldName: string): MethodDecorator;
onMutation<TParams>(fieldName: string, handler: ResolverHandler<TParams>): void;
onMutation(fieldName: string): MethodDecorator;
resolver<TParams>(handler: ResolverHandler<TParams>, options: { fieldName: string; typeName?: string }): void;
resolver(options: { fieldName: string; typeName?: string }): MethodDecorator;
batchResolver<TParams, TSource, T extends boolean = true>(handler: BatchResolverHandler<TParams, TSource, T>, options: GraphQlBatchRouteOptions<T>): void;
onBatchQuery<TParams, TSource>(fieldName: string, handler: BatchResolverHandler<TParams, TSource, true>, options?: Omit<GraphQlBatchRouteOptions<true>, 'fieldName' | 'typeName'>): void;
onBatchMutation<TParams, TSource>(fieldName: string, handler: BatchResolverHandler<TParams, TSource, true>, options?: Omit<GraphQlBatchRouteOptions<true>, 'fieldName' | 'typeName'>): void;
exceptionHandler<T extends Error>(error: ErrorClass<T> | ErrorClass<T>[], handler: ExceptionHandler<T>): void;
exceptionHandler<T extends Error>(error: ErrorClass<T> | ErrorClass<T>[]): MethodDecorator;
includeRouter(router: AppSyncGraphQLResolver | AppSyncGraphQLResolver[]): void;
}Setup
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
const app = new AppSyncGraphQLResolver();
export const handler = async (event, context) => app.resolve(event, context);Functional API
app.onQuery<{ id: string }>('getTodo', async ({ id }) => await fetchTodo(id));
app.onQuery<{ userId: string; limit?: number }>('getUserPosts', async (args) => {
const { userId, limit = 10 } = args;
return await fetchUserPosts(userId, limit);
});
app.onQuery('listAllUsers', async () => await fetchAllUsers());Access Event & Context
app.onQuery<{ id: string }>('getTodo', async (args, { event, context }) => {
console.log('Identity:', event.identity);
console.log('Remaining time:', context.getRemainingTimeInMillis());
return await fetchTodo(args.id);
});Decorator API
class TodoResolvers {
resolver = new AppSyncGraphQLResolver();
@resolver.onQuery('getTodo')
async getTodo(args: { id: string }) { return await fetchTodo(args.id); }
handler = async (event, context) => this.resolver.resolve(event, context, { scope: this });
}
export const handler = new TodoResolvers().handler;import { makeId } from '@aws-lambda-powertools/event-handler/appsync-graphql';
app.onMutation<{ title: string; description?: string }>('createTodo', async (args) => {
const todo = { id: makeId(), title: args.title, description: args.description || '', completed: false };
await saveTodo(todo);
return todo;
});
app.onMutation<{ id: string; completed: boolean }>('updateTodo', async (args) =>
await updateTodoStatus(args.id, args.completed)
);
app.onMutation<{ id: string }>('deleteTodo', async ({ id }) => {
await deleteTodo(id);
return { success: true };
});Route any GraphQL type and field, including custom types and subscriptions.
// Custom type field resolver
app.resolver(async (args: { todoId: string }) => await fetchTodoComments(args.todoId), {
fieldName: 'comments',
typeName: 'Todo'
});
// Subscription resolver
app.resolver(async (args) => ({ subscriptionId: makeId() }), {
fieldName: 'onTodoCreated',
typeName: 'Subscription'
});Efficiently process multiple sources at once to solve N+1 query problem.
Aggregate Mode (default: processes all sources at once)
app.onBatchQuery<never, { authorId: string }>('author', async (sources) => {
const authorIds = sources.map(post => post.authorId);
const authors = await batchFetchAuthors(authorIds);
return sources.map(post => authors.find(author => author.id === post.authorId));
});Individual Mode (processes one source at a time)
app.batchResolver(async (args, source) => await fetchAuthor(source.authorId), {
fieldName: 'author',
typeName: 'Post',
aggregate: false
});Performance Comparison
// Without batching: N+1 queries (100 posts = 100 DB queries)
app.resolver(async (args, { event }) => await db.getAuthor(event.source.authorId), {
fieldName: 'author',
typeName: 'Post'
});
// With batching: 1 query (100 posts = 1 DB query)
app.onBatchQuery<never, { authorId: string }>('author', async (sources) => {
const authorIds = sources.map(post => post.authorId);
const authors = await db.getAuthors(authorIds);
return sources.map(post => authors.find(a => a.id === post.authorId));
});class NotFoundError extends Error { name = 'NotFoundError'; }
class ValidationError extends Error { name = 'ValidationError'; }
app.exceptionHandler(NotFoundError, async (error) => ({
__typename: 'NotFoundError',
message: error.message,
}));
app.exceptionHandler([ValidationError, TypeError], async (error) => ({
__typename: 'ValidationError',
message: error.message,
type: error.name,
}));
app.onQuery<{ id: string }>('getTodo', async ({ id }) => {
const todo = await fetchTodo(id);
if (!todo) throw new NotFoundError(`Todo ${id} not found`);
return todo;
});Generate AWS AppSync scalar type values.
makeId(): string; // UUID v4 for ID type
awsDate(timezoneOffset?: number): string; // YYYY-MM-DD (AWSDate)
awsTime(timezoneOffset?: number): string; // hh:mm:ss.sss (AWSTime)
awsDateTime(timezoneOffset?: number): string; // YYYY-MM-DDThh:mm:ss.sssZ (AWSDateTime)
awsTimestamp(): number; // Unix timestamp in seconds (AWSTimestamp)Usage
import { makeId, awsDate, awsDateTime, awsTimestamp } from '@aws-lambda-powertools/event-handler/appsync-graphql';
app.onMutation<{ title: string }>('createTodo', async ({ title }) => ({
id: makeId(), // "550e8400-e29b-41d4-a716-446655440000"
title,
createdAt: awsDateTime(), // "2025-12-08T11:30:45.123Z"
createdDate: awsDate(), // "2025-12-08"
timestamp: awsTimestamp(), // 1733656245
completed: false,
}));
// Timezone offset (hours from UTC)
const tokyoTime = awsDateTime(9); // Tokyo UTC+9
const newYorkDate = awsDate(-5); // New York UTC-5Organize resolvers into separate routers.
// User router
const userRouter = new AppSyncGraphQLResolver();
userRouter.onQuery('getUser', async ({ id }) => fetchUser(id));
userRouter.onMutation('updateUser', async (args) => updateUser(args));
// Todo router
const todoRouter = new AppSyncGraphQLResolver();
todoRouter.onQuery('getTodo', async ({ id }) => fetchTodo(id));
todoRouter.onMutation('createTodo', async (args) => createTodo(args));
// Main resolver
const app = new AppSyncGraphQLResolver();
app.includeRouter([userRouter, todoRouter]);
export const handler = async (event, context) => app.resolve(event, context);interface AppSyncResolverEvent<TArguments = Record<string, unknown>, TSource = Record<string, unknown>> {
arguments: TArguments; // Field arguments
identity?: { // Caller identity
sub?: string;
issuer?: string;
username?: string;
claims?: Record<string, unknown>;
sourceIp?: string[];
defaultAuthStrategy?: string;
groups?: string[];
};
source: TSource; // Parent object
request: {
headers: Record<string, string>;
domainName?: string;
};
prev?: { result: unknown }; // Pipeline resolver result
info: {
fieldName: string;
parentTypeName: string;
variables: Record<string, unknown>;
selectionSetList: string[];
selectionSetGraphQL: string;
};
stash?: Record<string, unknown>; // Pipeline resolver stash
}
type ResolverHandler<TParams = unknown> = (
args: TParams,
options: { event: AppSyncResolverEvent; context: Context }
) => Promise<unknown> | unknown;
type BatchResolverHandler<TParams = unknown, TSource = unknown, T extends boolean = true> =
T extends true
? (sources: TSource[], options: { event: AppSyncResolverEvent[]; context: Context }) => Promise<unknown[]> | unknown[]
: (args: TParams, source: TSource, options: { event: AppSyncResolverEvent; context: Context }) => Promise<unknown> | unknown;
type ExceptionHandler<T extends Error> = (error: T) => Promise<unknown> | unknown;
type ErrorClass<T extends Error> = new (...args: unknown[]) => T;
interface GraphQlRouteOptions {
fieldName: string;
typeName?: string; // default: 'Query'
}
interface GraphQlBatchRouteOptions<T extends boolean = true, R extends boolean = false> {
fieldName: string;
typeName?: string;
aggregate?: T; // default: true (process all at once)
throwOnError?: R; // default: false (only when aggregate=false)
}
interface ResolveOptions {
scope?: unknown; // For decorator binding
}
class ResolverNotFoundException extends Error {}
class InvalidBatchResponseException extends Error {}