GraphQL
Body-hash caching, operation-name auto-tags, and framework adapters for GraphQL.
If you're coming from Apollo Client: Swoff caches GraphQL responses by body hash (query + variables → SHA-256 digest), not by normalized entities. No schema introspection, no fragment matchers, no
InMemoryCacheconfig. Each query is a standalone cache entry. Mutations auto-invalidate by operation name tags. See the GraphQL & Push architecture.
Preconditions
- Swoff initialized with data fetching enabled
- A GraphQL endpoint (default:
/graphql)
Status
Disabled by default. GraphQL support must be explicitly enabled.
Enable
npx @swoff/cli add graphqlOr set features.graphql.enabled: true and features.graphql.endpoints in swoff.config.json then regenerate.
Generated files
| File | What it does | Import in your code? |
|---|---|---|
swoff/graphql/index.ts | fetchWithGql(), queryGql(), mutateGql() | Yes |
swoff-api.bundle.js | Same API on swoff.queryGql(), swoff.mutateGql() for no-bundler | No (auto-initializes) |
Usage
import { queryGql, mutateGql } from "./swoff/graphql";
// Query — cached by body hash (query + variables)
// Auto-tagged by operation name: "GetNotes"
const { data, fromCache } = await queryGql(`
query GetNotes {
notes { id title }
}
`);
// Query with variables
const { data: todo } = await queryGql(
`
query GetTodo($id: ID!) {
todo(id: $id) { id title }
}
`,
{ id: "42" },
);
// Authenticated query
const { data } = await queryGql(
`
query Me { me { name } }
`,
{},
{ auth: true },
);
// Mutation — auto-invalidates related tags by operation name
const { data: created } = await mutateGql(
`
mutation CreateNote($title: String!) {
createNote(title: $title) { id }
}
`,
{ title: "New note" },
);
// Auto-invalidates: ["notes", "note"] (derived from "CreateNote")
// Offline: mutations are auto-queued
const { data } = await mutateGql(
`
mutation CreateNote($title: String!) {
createNote(title: $title) { id }
}
`,
{ title: "Offline task" },
{ queueOffline: true },
);
// Custom tags override operation-name auto-tags
const { data } = await queryGql(
`query GetNotes { notes { id } }`,
{},
{
tags: ["custom-tag"],
},
);Cross-query cache invalidation
Auto-tags from operation names handle paired read/write operations (e.g. GetNotes ↔ CreateNote). But the same entity type often appears in multiple queries — a User in GetDashboard, GetTeam, and me. Op-name auto-tags only cover the top-level operation, not nested entity references.
This is a deliberate tradeoff vs normalized caches like Apollo Client:
| Swoff (body-hash + auto-tags) | Apollo Client (normalized store) | |
|---|---|---|
| Cross-query auto-invalidation | Operation-name based. UpdateUser invalidates user; GetDashboard (tagged dashboard) is NOT auto-invalidated | Automatic — Apollo extracts __typename + id, updates all queries referencing that entity |
| Schema coupling | None — no __typename, no id field required | Requires __typename in every response, consistent id field |
| Runtime size | 0 kB (generated code) | ~32 kB gzip |
| Cache persistence | Cache API (disk, survives tab close) | In-memory |
| Offline mutation replay | ✅ Auto-invalidates cache tags on replay | ❌ No SW-level offline support |
| Entity update granularity | Invalidates entire cache entry | Patches individual entity fields |
| Anonymous query support | ❌ No op name → no auto-tags, can't be tag-invalidated | ✅ Normalizes by entity regardless |
For most apps, op-name auto-tags cover the common CRUD pairs. When you need cross-query invalidation, use explicit tags and invalidate:
// Tag both queries with the same tag
const dashboard = await queryGql(`
query GetDashboard { user { id name } team { id } }
`, {}, { tags: ["user", "team"] });
const team = await queryGql(`
query GetTeam { members { id name } }
`, {}, { tags: ["user", "team"] });
// Invalidate both with the same tag
await mutateGql(`
mutation UpdateProfile($name: String!) { updateProfile(name: $name) { id } }
`, { name }, { invalidate: ["user", "team"] });Anonymous queries (no operation name, e.g.
{ todos { id } }) produce no auto-tags and cannot be tag-invalidated. Always name your operations for caching to work.
No-bundler usage
Include swoff-api.bundle.js after client-injector.bundle.js:
<script src="/swoff/swoff-api.bundle.js"></script>
<script>
// Query — cached by body hash
const { data, fromCache } = await swoff.queryGql(`
query GetNotes { notes { id title } }
`);
// Mutation — auto-invalidates related tags
const { data: created } = await swoff.mutateGql(`
mutation CreateNote($title: String!) {
createNote(title: $title) { id }
}
`, { title: "New note" });
// Offline: mutations are auto-queued when mutation queue is enabled
const { data } = await swoff.mutateGql(`
mutation CreateNote($title: String!) {
createNote(title: $title) { id }
}
`, { title: "Offline task" }, { queueOffline: true });
</script>Framework adapters
GraphQL uses the same useSwoffFetch adapter pattern described in the Data Fetching guide. When a mutation auto-invalidates by operation name, the SW dispatches cache-invalidated:
Events:
| Event | Detail shape | Purpose |
|---|---|---|
cache-invalidated | { tags } | Mutation auto-invalidated by operation name — refetch affected queries |
swoff:cache-updated | { url } | Background re-fetch completed (optional, for fine-grained reactivity) |
No GraphQL-specific adapter is needed — the same useSwoffFetch adapter handles both REST and GraphQL. Body-hash caching and operation-name auto-tags happen automatically in the SW. See the Blueprint for the event reference and adapter patterns.
For a dedicated GraphQL API with operation-name auto-tags, use the generated queryGql/mutateGql functions instead.
From any HTTP client (header-based)
GraphQL responses can be cached by the SW even without the generated queryGql wrapper. The SW detects GraphQL requests by the presence of a query field in the POST body and computes the body hash automatically:
# Any HTTP client — POST to GraphQL endpoint
POST /graphql
Content-Type: application/json
X-SW-Strategy: cache-first
{"query": "query GetNotes { notes { id title } }"}No swoff import needed. The SW hashes the request body and caches the response. Tag invalidation works via X-SW-Cache-Tags header on the response, or by using the generated invalidateByTag API from swoff/cache/invalidate.
Multiple endpoints
import { queryGql } from "./swoff/graphql";
// Pass endpointIndex to choose which endpoint
const { data } = await queryGql(`query { ... }`, {}, {}, 1);
// Uses GQL_ENDPOINTS[1]Customize
No generated files to edit. The wrapper lives at swoff/graphql/index.ts and is regenerated on each swoff generate — manual edits would be overwritten.
Config
{
"features": {
"graphql": {
"enabled": true,
"endpoints": ["/graphql"]
}
}
}endpoints— array of GraphQL endpoint paths. Index 0 is the default. UseendpointIndexinqueryGql/mutateGqlto select others.
Related
- Library Comparison — Swoff vs Apollo Client (Tier 2)
- Library Comparison — Swoff vs TanStack Query / SWR / Apollo Client