aws-appsync-js - v1.0.1

⚑ aws-appsync-js

The AWS AppSync client that fits in 3 KB.
Every auth mode. Zero dependencies. End-to-end TypeScript inference.

pnpm add aws-appsync-js

npm version downloads CI status coverage bundle size license MIT

types included zero runtime deps ESM + CJS 5/5 AppSync auth modes edge-runtime

πŸ“– Docs site  Β·  API reference  Β·  Quickstart  Β·  Auth modes  Β·  Sponsor


import { AppSyncClient } from 'aws-appsync-js';

const client = new AppSyncClient({
url: 'https://xxx.appsync-api.us-east-1.amazonaws.com/graphql',
auth: { type: 'apiKey', apiKey: 'da2-…' },
});

const { events } = await client.request<{ events: Event[] }>(`
query { events { id name } }
`);

Two lines of setup, one line per query, and the response is typed exactly the way you say it is. The rest of this README is "and also…".


The honest pitch: if you already use TanStack Query / SWR / Zustand and just want a typed, correct AppSync transport β€” this is the smallest, sharpest tool for the job.

πŸͺΆ ~3 KB gzipped β€” measured by size-limit in CI on every PR. πŸ” Every AppSync auth mode β€” API key, Cognito, OIDC, Lambda, IAM (SigV4).
🧬 TypedDocumentNode β€” full inference for response and variables. ⏱ AbortSignal Β· timeouts Β· retries β€” exponential backoff with jitter.
🧱 Discriminated auth config β€” bad combos won't compile. 🌐 Edge-ready β€” Node, browsers, Workers, Vercel Edge, Deno, Bun.
🧨 Typed error classes β€” instanceof or stable code, your call. πŸ“¦ ESM + CJS with proper .d.ts, validated by publint + attw.

What you don't get: built-in cache / normalization, subscriptions (yet). Both are excellent in apollo-client / urql / aws-amplify if you need them β€” pair aws-appsync-js with one of them, or with TanStack Query / SWR.



The AppSync ecosystem has two extremes:

  1. aws-amplify β€” full SDK, ~200 KB minified, expects you to live inside its world.
  2. Hand-rolled fetch + SigV4 + auth-mode plumbing β€” three subtle things to get right per service.

aws-appsync-js is the missing middle: a 3 KB GraphQL-over-fetch client that understands AppSync (every auth mode, retry semantics, error shapes), gives you real TypeScript (not any-flavoured types), and doesn't drag in anything else. Zero runtime dependencies. ESM-first with a proper CJS fallback. Works in Node, edge runtimes (Cloudflare Workers, Vercel Edge), and the browser.


pnpm add aws-appsync-js
# or
npm install aws-appsync-js
# or
yarn add aws-appsync-js
# or
bun add aws-appsync-js

Optional peer dependencies (only needed for the typed-document workflow):

pnpm add -D graphql @graphql-typed-document-node/core

Runs on Node β‰₯ 18.17, modern browsers (evergreen, Safari 16+), Cloudflare Workers, Vercel Edge, Deno, and Bun.


import { AppSyncClient } from 'aws-appsync-js';

const client = new AppSyncClient({
url: process.env.APPSYNC_URL!,
auth: { type: 'apiKey', apiKey: process.env.APPSYNC_API_KEY! },
});

// Query
const { events } = await client.request<{ events: { id: string; name: string }[] }>(`
query ListEvents { events { id name } }
`);

// Mutation with variables
const { createEvent } = await client.request<
{ createEvent: { id: string } },
{ input: { name: string } }
>(
`mutation CreateEvent($input: CreateEventInput!) {
createEvent(input: $input) { id }
}`,
{ input: { name: 'Re:Invent' } },
);

That's the whole API for 90 % of use cases. The rest of this README shows you the 10 %.


flowchart LR
    A[Your app code] -->|request<TData,TVars>| B((AppSyncClient))
    B --> C{auth.type}
    C -->|apiKey| D[x-api-key header]
    C -->|cognito / oidc| E[Authorization: JWT]
    C -->|lambda| F[Authorization: custom token]
    C -->|iam| G[SigV4-sign request]
    D & E & F & G --> H[fetch POST /graphql]
    H --> I[(AppSync endpoint)]
    I -->|2xx + data| J[TData]
    I -->|errors[]| K[AppSyncGraphQLError]
    I -->|non-2xx| L[AppSyncHttpError]
    H -.->|retry on 5xx / 429 / network| H

No client-side cache. No normalization. No subscriptions (yet). Just a typed transport.


AppSync has five. aws-appsync-js supports all five. Same client, different auth field:

// 1. API_KEY β€” public-ish APIs, the easiest to set up
new AppSyncClient({ url, auth: { type: 'apiKey', apiKey: 'da2-…' } });

// 2. AMAZON_COGNITO_USER_POOLS β€” your users sign in, you forward their JWT
new AppSyncClient({
url,
auth: { type: 'cognito', jwtToken: () => session.getIdToken().getJwtToken() },
});

// 3. OPENID_CONNECT β€” same shape, different IdP (Auth0, Okta, …)
new AppSyncClient({ url, auth: { type: 'oidc', jwtToken: getAccessToken } });

// 4. AWS_LAMBDA β€” your custom authorizer takes an opaque token
new AppSyncClient({
url,
auth: { type: 'lambda', authorizationToken: 'whatever-your-fn-expects' },
});

// 5. AWS_IAM β€” SigV4-signed requests using IAM credentials
new AppSyncClient({
url,
auth: {
type: 'iam',
region: 'us-east-1',
credentials: { accessKeyId, secretAccessKey, sessionToken },
},
});

The token / credential fields can also be functions (sync or async) β€” aws-appsync-js calls them per request, so silent token refresh, IMDS lookups, or any custom strategy just works:

auth: {
type: 'cognito',
jwtToken: async () => (await refreshIfExpired()).idToken,
},

β†’ Full guide with trade-offs, pitfalls, and IdP-specific recipes: docs site.


This is the part most clients get wrong. aws-appsync-js solves three classic AppSync-on-TypeScript pain points:

The naΓ―ve pattern duplicates your schema across files and the types drift:

// ❌ The shape exists in your schema. Now it also exists here. Forever.
type GetUserData = { user: { id: string; name: string; email: string | null } };
const { user } = await client.request<GetUserData>(`
query GetUser($id: ID!) { user(id: $id) { id name email } }
`, { id });

With @graphql-codegen + graphql-typed-document-node, you write the query once and the client infers both the response and the variables:

// βœ… One source of truth. Types come from your schema.
import { GetUserDocument } from './generated/graphql';

const data = await client.request(GetUserDocument, { id: '1' });
// ^? GetUserQuery ^? GetUserQueryVariables β€” TS checks the call site for you

Drop a .ts import alongside your .graphql file and you get end-to-end safety with zero hand-written types. If you change a field, your IDE squiggles every call site immediately.

codegen config (paste into codegen.ts)
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
schema: 'https://your.appsync.endpoint/graphql',
documents: 'src/**/*.{ts,graphql}',
generates: {
'src/generated/graphql.ts': {
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
config: { useTypeImports: true, dedupeFragments: true },
},
},
};
export default config;

The most common AppSync footgun is "oh I forgot to set the region for IAM auth". The discriminated union catches that at compile time:

new AppSyncClient({
url,
auth: {
type: 'iam',
// ❌ Error: Property 'region' is missing in type
// ❌ Error: Property 'credentials' is missing in type
},
});

Same for swapping fields between modes β€” TypeScript narrows the auth object on type and only the matching keys are valid.

Instead of stringly-typed catches, every error has a stable code you can branch on without an instanceof chain across module boundaries:

try {
await client.request(query);
} catch (err) {
if (err instanceof AppSyncGraphQLError) {
// err.errors is GraphQLFormattedError[] β€” full shape, fully typed
return err.errors.map(e => e.message);
}
if (err instanceof AppSyncHttpError && err.status === 401) {
return refreshAndRetry();
}
if (err instanceof AppSyncAbortError && err.reason === 'timeout') {
return 'took too long';
}
throw err;
}

const controller = new AbortController();
const promise = client.request(query, vars, { signal: controller.signal });
setTimeout(() => controller.abort(), 100);

await promise.catch((err) => {
if (err.code === 'ABORTED') {
/* user-cancelled or timed out */
}
});
await client.request(query, vars, { timeoutMs: 2_000 });
const client = new AppSyncClient({
url,
auth,
retry: {
attempts: 5,
baseDelayMs: 250,
maxDelayMs: 8_000,
// Default retries network errors + 5xx + 429. Add your own:
shouldRetry: (err, attempt) =>
err instanceof AppSyncHttpError && err.status === 503 && attempt < 5,
},
});

GraphQL servers can return both data and errors for partial successes. By default aws-appsync-js throws; switch to non-throwing mode when you need the partial payload:

const { data, errors } = await client.requestRaw(query);
if (errors) reportToSentry(errors);
render(data); // may be partially populated
const client = new AppSyncClient({
url,
auth,
fetch: (input, init) => tracedFetch(input, { ...init, integrity: 'sri-…' }),
});
const schema = await client.introspect();
fs.writeFileSync('./schema.json', JSON.stringify(schema));

β†’ More recipes: docs site β†’ Cookbook.


Method What it does
new AppSyncClient(opts) Create a client. See AppSyncClientOptions.
client.request(doc, vars?, opts?) Send a query/mutation. Returns data (throws on errors).
client.requestRaw(doc, vars?, opts?) Same, but returns { data, errors, extensions } without throwing.
client.query(...) / mutate(...) Aliases for request() β€” purely stylistic.
client.introspect(opts?) Run the standard introspection query, returns the typed schema.

Full generated reference: https://yankouskia.github.io/aws-appsync-js/api/.


Feature aws-appsync-js aws-amplify apollo-client
Bundle size (gzipped) ~3 KB ~200 KB ~40 KB
Runtime deps 0 dozens several
All 5 AppSync auth modes βœ… βœ… manual
SigV4 included βœ… (Node) βœ… ❌
TypedDocumentNode βœ… partial βœ…
AbortSignal / timeouts βœ… ❌ via link
Subscriptions ❌ (planned) βœ… βœ…
Caching / normalization ❌ (by design) βœ… βœ…

Use aws-appsync-js when you want a thin, typed HTTP client for AppSync and you already handle caching/state elsewhere (TanStack Query, SWR, your own store). Use the full SDKs when you want batteries-included client-side cache or subscriptions.


Runtime Status
Node β‰₯ 18.17 (LTS) βœ…
Node 20 / 22 LTS βœ…
Cloudflare Workers βœ… (API_KEY / Cognito / OIDC / Lambda; IAM not supported β€” workers have no node:crypto)
Vercel Edge βœ… (same caveat as Workers)
Deno β‰₯ 1.40 βœ… via npm: specifier
Bun β‰₯ 1.0 βœ…
Chrome / Safari / Firefox (last 2) βœ…
iOS Safari 16+ βœ…

A full Docusaurus-powered docs site is published to GitHub Pages on every push to master:

https://yankouskia.github.io/aws-appsync-js

It contains:

  • Quickstart β€” get to your first typed query in 60 seconds.
  • One page per auth mode β€” including pitfalls, error shapes, and refresh recipes.
  • TypeScript & codegen β€” the recommended workflow.
  • Cookbook β€” retries, timeouts, partial responses, observability, headers.
  • Edge runtimes β€” Workers / Edge / Deno / Bun specifics.
  • Migration from v0 β€” for users of the original 2018-era package.
  • API reference β€” full TypeDoc output at /api/.

The site sources live in ./website and are built with Docusaurus 3 (TypeScript-first config, Mermaid support, dark mode, a custom theme).


PRs welcome. See CONTRIBUTING.md. Security disclosures: SECURITY.md.

If aws-appsync-js is saving your team time:

  • ⭐ Star the repo β€” it's the cheapest way to say thanks and it helps other engineers find the project.
  • πŸ’– Sponsor on GitHub β€” every dollar funds maintenance, new auth-mode work, and the long-awaited subscription support.
  • πŸ› Open an issue β€” bugs, ideas, "this README is unclear" β€” all welcome.

MIT Β© Aliaksandr Yankouski

Built with care for the AppSync community.
If you ship a side project on top of this, I'd love to see it β€” tag @yankouskia or open a discussion.