Skip to content
TypeWire

@tahanabavi/typefetch-graphql

v0.1.0

GraphQL transport for TypeFetch contracts — one client, one middleware chain, and selection sets generated from your Zod response schema.

What the generator refuses

It fails at client.init() — not on a request in production — naming the endpoint and the path that caused it, and pointing at document. Guessing would produce a query the server rejects at runtime, which is strictly worse.

SchemaWhy
z.union([...]) of objectsneeds inline fragments (... on Type), and the schema does not say which types
z.record(...)GraphQL has no way to ask for "every field"
recursive schemasa query must be finite; the depth is a decision only you can make
variables with no rootthere is no single field to attach the arguments to

Give those endpoints an explicit document; everything else about them still works, including validation against response.

Errors

GraphQL error codes are mapped onto typefetch's shared ErrorKind, so one global handler covers every transport:

ts
client.onError((error) => {
if (error.kind === 'unauthenticated') redirectToLogin()
})

fires for a GraphQL UNAUTHENTICATED exactly as it does for an HTTP 401.

extensions.codekind
UNAUTHENTICATEDunauthenticated
FORBIDDENpermission_denied
BAD_USER_INPUT, GRAPHQL_VALIDATION_FAILED, GRAPHQL_PARSE_FAILEDinvalid_argument
NOT_FOUNDnot_found
TOO_MANY_REQUESTSresource_exhausted
INTERNAL_SERVER_ERRORinternal
SERVICE_UNAVAILABLEunavailable

Both response media types are handled: the legacy application/json (errors delivered inside a 200) and application/graphql-response+json (errors with a real 4xx/5xx). extensions.http.status is honoured when present.

Typed error bodies

errors is keyed by extensions.code for a GraphQL endpoint, and isContractError narrows off it:

ts
errors: {
UNAUTHENTICATED: z.object({ code: z.literal("UNAUTHENTICATED"), realm: z.string() }),
}

try {
await api.user.get({ id });
} catch (e) {
if (isContractError(contracts.user.get, e, "UNAUTHENTICATED")) {
e.data.realm; // fully typed
}
}

Partial data

A response carrying both data and errors is the case every GraphQL client gets wrong in one direction or the other.

  • errorPolicy: "none" (default) — throw. A partially-failed response is a failed one, and silently returning half a result is how a null reaches a UI three screens from its cause.

  • errorPolicy: "all" — resolve the data and hand the errors to onPartialErrors, so the return type stays Promise<T> and nothing is dropped.

ts
graphqlTransport({
url: '…',
errorPolicy: 'all',
onPartialErrors: (errors, { endpointId }) => log.warn(endpointId, errors),
})