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.
Convention: monorepo-style with clear, minimal top-level directories. appsync unified repo
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 changesasync 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,
);