Appsync Unified Repo File

Use @graphql-codegen/cli to generate TypeScript types for your Lambda resolvers:

// packages/data-sources/src/types/graphql.ts generated from schema.graphql
export type QueryGetPostArgs =  id: string ;
export type Post =  id: string; title: string; content: string ;

// Now your Lambda is fully typed import type QueryGetPostArgs, Post from './types/graphql';

export const handler = async (event: AppSyncResolverEvent<QueryGetPostArgs>): Promise<Post> => const id = event.arguments; // Your logic here... ;

No more manual type updates when the schema changes. Run yarn generate and the unified repo syncs everything.

  • Keep business logic out of VTL as much as possible; prefer pipeline functions that call Lambda or use JavaScript resolvers where supported.
  • Data source adapters:
  • Convention: monorepo-style with clear, minimal top-level directories. appsync unified repo

  • /schema
  • /resolvers
  • /shared
  • /clients
  • /tests
  • /scripts
  • /docs
  • README.md and CODEOWNERS, CONTRIBUTING.md, SECURITY.md at repo root.
  • This document describes a recommended structure, conventions, and workflows for an "AppSync unified repo": a single repository that centralizes GraphQL schemas, resolvers, client-generated types, infrastructure-as-code, and developer tooling for projects using AWS AppSync (or an AppSync-compatible GraphQL service). It’s intended as a practical reference for teams aiming to improve consistency, speed of iteration, and cross-project reuse.

    AppSync uses data sources. Storing database credentials in a unified repo is fine (encrypted), but the rotation logic should be a separate construct. The unified repo can contain the rotation Lambda code, but keep the rotation schedule outside the API stack to avoid unintentional resetting.

    import  appSyncClient  from '../client/AppSyncClient';
    import  Observable  from 'zen-observable-ts';
    

    export interface IAppSyncRepository<T, TCreateInput, TUpdateInput> get(id: string): Promise<T>; list(limit?: number, nextToken?: string): Promise< items: T[]; nextToken?: string >; create(input: TCreateInput): Promise<T>; update(id: string, input: TUpdateInput): Promise<T>; delete(id: string): Promise<string>; subscribeToCreated(): Observable<T>; subscribeToUpdated(): Observable<T>; subscribeToDeleted(): Observable<string>;

    export class AppSyncUnifiedRepository<T, TCreateInput, TUpdateInput> implements IAppSyncRepository<T, TCreateInput, TUpdateInput> { constructor( private readonly queries: get: string; list: string; create: string; update: string; delete: string; , private readonly subscriptions: onCreate: string; onUpdate: string; onDelete: string; , private readonly modelName: string ) {}

    async get(id: string): Promise<T> try const result = await appSyncClient.query< [key: string]: T >( query: this.queries.get, variables: id , fetchPolicy: 'network-only', ); return result[get$this.modelName]; catch (error) throw new Error(Failed to get $this.modelName: $error.message); No more manual type updates when the schema changes

    async list(limit = 100, nextToken?: string): Promise< items: T[]; nextToken?: string > try const result = await appSyncClient.query< [key: string]: items: T[]; nextToken?: string ; >( query: this.queries.list, variables: limit, nextToken , ); return result[list$this.modelNames]; catch (error) throw new Error(Failed to list $this.modelNames: $error.message);

    async create(input: TCreateInput): Promise<T> try const result = await appSyncClient.mutate< [key: string]: T >( mutation: this.queries.create, variables: input , ); return result[create$this.modelName]; catch (error) throw new Error(Failed to create $this.modelName: $error.message);

    async update(id: string, input: TUpdateInput): Promise<T> try const result = await appSyncClient.mutate< [key: string]: T >( mutation: this.queries.update, variables: input: id, ...input , ); return result[update$this.modelName]; catch (error) throw new Error(Failed to update $this.modelName: $error.message);

    async delete(id: string): Promise<string> try const result = await appSyncClient.mutate< [key: string]: string >( mutation: this.queries.delete, variables: input: id , ); return result[delete$this.modelName]; catch (error) throw new Error(Failed to delete $this.modelName: $error.message);

    subscribeToCreated(): Observable<T> return appSyncClient.subscribe<T>( query: this.subscriptions.onCreate, ); Keep business logic out of VTL as much

    subscribeToUpdated(): Observable<T> return appSyncClient.subscribe<T>( query: this.subscriptions.onUpdate, );

    subscribeToDeleted(): Observable<string> return appSyncClient.subscribe<string>( query: this.subscriptions.onDelete, ); }


    AppSync is schema-first. When your schema, resolvers, and infrastructure live together, you can leverage tools like GraphQL Code Generator to automatically type your resolvers. You catch errors like $ctx.args.input.id being an integer vs. a string at build time, not runtime.

    Your CDK stack reads the local resolvers and injects them directly into the AppSync API:

    new appsync.Resolver(this, 'GetPostResolver', 
      api: this.api,
      typeName: 'Query',
      fieldName: 'getPost',
      code: appsync.Code.fromAsset(path.join(__dirname, '../graphql/resolvers/getPost.js')),
      runtime: appsync.FunctionRuntime.JS_1_0_0,
      dataSource: this.postTableDataSource,
    );