aws-appsync-js
v1.0.1
Published
A tiny, fully-typed, zero-dependency AWS AppSync (GraphQL) client for Node and the browser. Supports all 5 AppSync auth modes, AbortSignal, retries, and end-to-end TypeScript inference via TypedDocumentNode.
Maintainers
Readme
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…".
✨ Why you'll love it
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-amplifyif you need them — pairaws-appsync-jswith one of them, or with TanStack Query / SWR.
📚 Table of contents
- Why this exists
- Install
- Quickstart
- How it works
- Every AppSync auth mode
- TypeScript superpowers
- Cookbook
- API at a glance
- Comparison
- Compatibility
- Docs site
- Contributing & sponsoring
🧭 Why this exists
The AppSync ecosystem has two extremes:
aws-amplify— full SDK, ~200 KB minified, expects you to live inside its world.- 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.
📦 Install
pnpm add aws-appsync-js
# or
npm install aws-appsync-js
# or
yarn add aws-appsync-js
# or
bun add aws-appsync-jsOptional peer dependencies (only needed for the typed-document workflow):
pnpm add -D graphql @graphql-typed-document-node/coreRuns on Node ≥ 18.17, modern browsers (evergreen, Safari 16+), Cloudflare Workers, Vercel Edge, Deno, and Bun.
🚀 Quickstart
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 %.
🔎 How it works
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| HNo client-side cache. No normalization. No subscriptions (yet). Just a typed transport.
🔐 Every AppSync auth mode
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.
🧬 TypeScript superpowers
This is the part most clients get wrong. aws-appsync-js solves three classic AppSync-on-TypeScript pain points:
1. Type-safe responses without writing types twice
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 youDrop 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.
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;2. Discriminated auth config — bad combos won't compile
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.
3. Typed error classes you can switch on
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;
}🧑🍳 Cookbook
Cancel a request
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 */
}
});Per-call timeout (overrides the client default of 30 s)
await client.request(query, vars, { timeoutMs: 2_000 });Configure retries
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,
},
});Read partial data on GraphQL errors (don't throw)
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 populatedCustom fetch for edge runtimes / tracing
const client = new AppSyncClient({
url,
auth,
fetch: (input, init) => tracedFetch(input, { ...init, integrity: 'sri-…' }),
});Get the introspection schema (build-time codegen, GraphiQL, etc.)
const schema = await client.introspect();
fs.writeFileSync('./schema.json', JSON.stringify(schema));→ More recipes: docs site → Cookbook.
📘 API at a glance
| 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/.
⚖️ Comparison
| 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.
🧪 Compatibility
| 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+ | ✅ |
🌐 Docs site
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).
❤️ Contributing & sponsoring
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.
