Directory-Based Resolver Mapping for AWS AppSync

Cross-posted from my tech blog.

Managing AWS AppSync Pipeline Resolvers at Scale

AWS AppSync pipeline resolvers enable multi-step processing for GraphQL field resolution. However, as GraphQL schemas grow, several challenges emerge:

Disor…


This content originally appeared on DEV Community and was authored by Daniel Worsnup

Cross-posted from my tech blog.

Managing AWS AppSync Pipeline Resolvers at Scale

AWS AppSync pipeline resolvers enable multi-step processing for GraphQL field resolution. However, as GraphQL schemas grow, several challenges emerge:

  1. Disorganized Code Placement: Resolvers stored within the AWS console or as scattered files become difficult to locate in schemas with hundreds of fields.
  2. Inconsistent Naming: Without standardization, resolver files develop inconsistent patterns that impede discovery and maintenance.
  3. Implicit Dependencies: Pipeline resolvers often depend on specific data sources without explicit links in the code.
  4. Team Scaling Issues: Multiple developers contribute divergent implementation patterns without a consistent structure.
  5. Deployment Challenges: Coordinating resolver changes across diverse data sources introduces errors.
  6. Complex Troubleshooting: Identifying specific functions causing errors becomes time-consuming without clear organization.

Directory-Based Schema Mapping

This approach organizes resolvers by mirroring the GraphQL schema structure in your filesystem:

pipelines/
  ├── {TypeName}/
  │     └── {FieldName}/
  │           ├── meta.json
  │           ├── 0.resolver.js
  │           └── 1.resolver.js (optional additional functions)

For example, this GraphQL query:

type Query {
  listRestaurants(input: ListRestaurantsInput!): RestaurantPage!
}

Maps to:

pipelines/Query/listRestaurants/

Implementation Components

1. Resolver Functions

Each resolver function is a numbered JavaScript file implementing AppSync's request/response pattern:

// pipelines/Query/listRestaurants/0.resolver.js
export function request(ctx) {
  const { args } = ctx;
  return {
    operation: 'Query',
    query: {
      expression: '#location = :location',
      expressionNames: { '#location': 'location' },
      expressionValues: { ':location': args.input.location }
    },
  };
}

export function response(ctx) {
  return ctx.result;
}

2. Data Source Configuration

A meta.json file specifies the data source configuration. Each element of the dataSourceConfigs array defines the data source for the resolver with the same index. The example below specifies that the resolver defined by the file 0.resolver.js uses a DynamoDB data source and requires permissions for the RestaurantsTable.

// pipelines/Query/listRestaurants/meta.json
{
  "dataSourceConfigs": [
    {
      "type": "ddb",
      "tableArns": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/RestaurantsTable"
      ]
    }
  ]
}

3. Automated Resolver Registration

The infrastructure code scans the directory structure to register resolvers:

export function defineResolvers({ scope, api }) {
  const typeNamePaths = globSync(path.join(__dirname, `./pipelines/*`), { withFileTypes: true });

  for (const typeNamePath of typeNamePaths) {
    if (!typeNamePath.isDirectory()) continue;

    const fieldNamePaths = globSync(`${typeNamePath.fullpath()}/*`, { withFileTypes: true });
    for (const fieldNamePath of fieldNamePaths) {
      if (!fieldNamePath.isDirectory()) continue;

      definePipelineResolver({
        scope,
        api,
        typeName: typeNamePath.name,
        fieldName: fieldNamePath.name,
        path: fieldNamePath,
      });
    }
  }
}

For each resolver, the infrastructure code scans the resolver directory for {n}.resolver.js source files and the meta.json file:

function definePipelineResolver({
  scope,
  api,
  typeName,
  fieldName,
  path,
}) {
  const pipelineName = `${typeName}${upperFirst(fieldName)}Pipeline`;
  const functionDefs = [];
  let meta = null;

  // Scan resolver directory for files and configuration
  for (const child of path.readdirSync()) {
    if (child.isDirectory()) continue;

    const match = child.name.match(/^([0-9]+)\.resolver\.js$/i);
    if (match) {
      const index = +match[1];
      functionDefs.push({ index, path: child });
    } else if (child.name === 'meta.json') {
      meta = JSON.parse(fs.readFileSync(child.fullpath(), 'utf8'));
    }
  }

  // Sort functions by numerical prefix
  const sortedFunctionDefs = _.sortBy(functionDefs, ({ index }) => index);

  // Create pipeline configuration
  const pipelineConfig = sortedFunctionDefs.map(({ path, index }) => {
    const dataSourceConfig = meta.dataSourceConfigs[index];
    const dataSource = defineDataSource({ 
      scope, 
      name: `${pipelineName}DS${index}`, 
      api, 
      config: dataSourceConfig 
    });

    return defineFunction({
      name: `${pipelineName}Function${index}`,
      path,
      dataSource,
      scope,
      api,
    });
  });

  // Register the AppSync resolver
  new appsync.Resolver(scope, pipelineName, {
    api,
    typeName,
    fieldName,
    pipelineConfig,
    code: Code.fromInline(`
      function request(ctx) { return ctx; }
      function response(ctx) { return ctx.result; }
      export { request, response };
    `),
    runtime: FunctionRuntime.JS_1_0_0,
  });
}

4. Data Source Definition

Function to create data sources with appropriate IAM permissions:

function defineDdbDataSourceRole({
  scope,
  name,
  tableArns,
}: {
  scope: Construct;
  name: string;
  tableArns: string[];
}): Role | undefined {
  return new iam.Role(scope, name, {
    assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'),
    inlinePolicies: {
      AccessDynamoDbTables: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            actions: ['dynamodb:GetItem', 'dynamodb:Query', 'dynamodb:Scan' /* add actions as needed by resolvers */],
            resources: tableArns.reduce((collector: string[], tableArn: string) => {
              collector.push(tableArn);
              collector.push(`${tableArn}/index/*`);
              return collector;
            }, []),
          }),
        ],
      }),
    },
  });
}

function defineDataSource({
  scope,
  name,
  api,
  config,
}: {
  scope: Construct;
  name: string;
  api: IGraphqlApi;
  config: { type: string; tableArns: string[] };
}): BaseDataSource {
  if (config.type === 'ddb') {
    const serviceRole = defineDdbDataSourceRole({
      scope,
      name: `${name}ServiceRole`,
      tableArns: config.tableArns,
    });

    return new appsync.DynamoDbDataSource(scope, name, {
      api,
      table: dynamodb.Table.fromTableArn(scope, `${name}Table`, config.tableArns[0]),
      serviceRole,
    });
  }

  // Implement other types of data sources as needed

  throw new Error(`Unsupported data source type: ${config.type}`);
}

5. CDK Integration

Integrate the resolver mapping into your CDK stack:

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as path from 'path';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import { defineResolvers } from './resolvers';

export class ApiStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const graphqlApi = new appsync.GraphqlApi(this, 'GraphqlApi', {
      name: 'Api',
      definition: appsync.Definition.fromSchema(
        appsync.SchemaFile.fromAsset(path.join(__dirname, './shared/gql/schema.gql'))
      ),
    });

    // Register all resolvers from the directory structure
    defineResolvers({
      scope: this,
      api: graphqlApi,
    });
  }
}

Dependencies

This implementation relies on several key libraries and frameworks:

  1. AWS CDK (Cloud Development Kit) - Infrastructure as code framework that enables defining AWS resources using TypeScript.

  2. Constructs - Programming model for composing cloud resources.

  3. Lodash - JavaScript utility library used for its array and object manipulation functions.

    • lodash - Specifically, _.sortBy() for sorting function definitions by index
  4. Node.js Built-ins - Native Node.js modules used for filesystem operations.

    • fs - For reading files and directories
    • path - For path manipulation and joining
  5. Glob - Pattern matching library for directory scanning.

    • glob - The globSync function used for finding files matching patterns

These dependencies should be included in your package.json file, except for the Node.js built-ins which are available by default in Node.js environments.

Key Benefits

  • Schema-Aligned Organization: Filesystem mirrors GraphQL schema for intuitive resolver discovery.
  • Pipeline Step Ordering: Numbered files (e.g., 0.resolver.js) explicitly reflect execution order.
  • Logical Separation: Each file handles a distinct responsibility in the pipeline.
  • Simplified Maintenance: Easy location and modification of resolver logic through directory navigation.
  • Declarative Data Sources: Explicit data source configurations in meta.json files.
  • Automated Registration: New resolvers automatically register when added to the directory structure.

Conclusion

This directory-based approach creates a self-documenting system where filesystem structure reflects GraphQL schema organization. The pattern maintains clarity even as APIs scale to hundreds of fields and multiple data sources, while enabling automated infrastructure provisioning through AWS CDK.

By establishing this convention, team members can quickly locate, understand, and modify resolvers without hunting through disparate files or console UIs. The result is a maintainable, scalable architecture for complex AWS AppSync GraphQL APIs.

Like this post?

Follow me on Twitter where I tweet about frontend things: @thesnups


This content originally appeared on DEV Community and was authored by Daniel Worsnup


Print Share Comment Cite Upload Translate Updates
APA

Daniel Worsnup | Sciencx (2025-03-13T23:02:26+00:00) Directory-Based Resolver Mapping for AWS AppSync. Retrieved from https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/

MLA
" » Directory-Based Resolver Mapping for AWS AppSync." Daniel Worsnup | Sciencx - Thursday March 13, 2025, https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/
HARVARD
Daniel Worsnup | Sciencx Thursday March 13, 2025 » Directory-Based Resolver Mapping for AWS AppSync., viewed ,<https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/>
VANCOUVER
Daniel Worsnup | Sciencx - » Directory-Based Resolver Mapping for AWS AppSync. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/
CHICAGO
" » Directory-Based Resolver Mapping for AWS AppSync." Daniel Worsnup | Sciencx - Accessed . https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/
IEEE
" » Directory-Based Resolver Mapping for AWS AppSync." Daniel Worsnup | Sciencx [Online]. Available: https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/. [Accessed: ]
rf:citation
» Directory-Based Resolver Mapping for AWS AppSync | Daniel Worsnup | Sciencx | https://www.scien.cx/2025/03/13/directory-based-resolver-mapping-for-aws-appsync/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.