Skip to content

AppSync GraphQL

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

  • Route events based on GraphQL type and field keys
  • Automatically parse API arguments to function parameters
  • Handle GraphQL responses and errors in the expected format

Terminology

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.

 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.

Registering a resolver for a Query type
 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.

Registering a resolver for a Mutation type
 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.

Registering a resolver for a type and field
 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 resolver
 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
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);
  1. It's recommended to pass a refernce of this to ensure the correct class scope is propageted to the route handler functions.

Scalar 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 values
 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,
  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
ID makeId 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

Note

The following examples use a more advanced schema. These schemas differ from the initial sample infrastructure we used earlier.

You can register the same route handler multiple times to resolve fields with the same return value.

 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);
  1. If omitted, the 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.

 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);
  1. The event parameter contains the original AppSync event and has type AppSyncResolverEvent from the @types/aws-lambda.

Logging

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.

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