The AWS AppSync client that fits in 3 KB.
Every auth mode. Zero dependencies. End-to-end TypeScript inference.
pnpm add aws-appsync-js
π 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-amplifyif you need them β pairaws-appsync-jswith one of them, or with TanStack Query / SWR.
The AppSync ecosystem has two extremes:
aws-amplify β full SDK, ~200 KB minified, expects you to live inside its world.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.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.
switch onInstead 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
fetch for edge runtimes / tracingconst 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:
/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:
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.