or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

appsync-events.mdappsync-graphql.mdbedrock-agent.mdhttp-middleware.mdhttp-routing.mdindex.md
tile.json

appsync-graphql.mddocs/

AppSync GraphQL API

Route GraphQL queries, mutations, and custom resolvers for AWS AppSync without VTL or JavaScript templates.

AppSyncGraphQLResolver

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);

Query Resolvers

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;

Mutation Resolvers

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 };
});

Generic Resolvers

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'
});

Batch Resolvers

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));
});

Exception Handlers

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;
});

GraphQL Scalar Utilities

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

Router Composition

Organize 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);

Types

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 {}