Guides

Generate from GraphQL

A GraphQL schema generates the same package as an OpenAPI spec, on the same runtime: queries on client.query, mutations on client.mutation, typed selections, connections that auto-paginate.

typeship accepts a GraphQL schema wherever it accepts an OpenAPI spec. The output is a TypeScript, Python, or Go package with the same client, results, errors, retries, and platforms. The CLI and MCP server are TypeScript, as for every spec.

Inputs

Three shapes work as a project's spec URL or as an ad hoc input:

  • SDL: a .graphql schema file.
  • Introspection JSON: the __schema payload.
  • A GraphQL endpoint URL: typeship runs the standard introspection query server-side and generates from the result. The endpoint becomes the client's default baseUrl. Endpoints with introspection disabled need the SDL instead.

What the schema cannot say

A GraphQL schema has no equivalent of OpenAPI's servers and securitySchemes. Where the API lives and how requests authenticate come from the project's config, or from the endpoint and auth fields the generator shows when it sees a GraphQL input:

  • Endpoint: every request is one POST to it, and it becomes the client's default baseUrl. A schema fetched from its own endpoint already has one. Named environments (sandbox, production) become client environments.
  • Auth: bearer by default, sent as Authorization: Bearer. basic suits key-pair APIs, with the public key as the username and the private key as the password. api_key sends a header. none generates no credential option. The generated README, AGENTS.md, CLI, and MCP server all describe the scheme that was chosen.
  • Name: the package and client names (braintree, BraintreeClient) come from title, or from the endpoint's host when it is not set.

Headers an API needs on every call, such as a version header, go in defaultHeaders on the client.

The mapping

  • Each root field becomes an operation. Queries land on client.query, mutations on client.mutation. Field arguments become a typed body object, required when any argument is non-null.
  • Object types carry an optional __typename literal. Unions, and interfaces with implementers, become a union of their concrete types, so results narrow on __typename.
  • Custom scalars map to unknown. Built-ins map to their TypeScript equivalents.
  • Subscriptions are skipped, with a warning.
  • Operation documents (files of queries you already wrote) are not an input. Generation works from the schema alone.

Selections

A schema does not say which fields to fetch. Generated methods select every scalar and enum field to depth 2 by default, with a ... on fragment for every concrete member of a union or interface. Every object-returning method takes an optional select argument after its body. A field that returns a scalar takes none.

In TypeScript, select is a typed object checked against the schema. A typo is a compile error, and the result type narrows to exactly what was picked:

const client = new BraintreeClient({ baseUrl: "https://payments.sandbox.braintree-api.com/graphql", basicAuth: { username: publicKey, password: privateKey } });

const whole = await client.query.transaction({ id: "txn_1" });               // default selection
const slim = await client.query.transaction({ id: "txn_1" }, { id: true, amount: true, customer: { email: true } });
// slim.data: { id: string; amount: number; customer: { email: string | null } | null } | null

Unions and interfaces take on, keyed by concrete type name, and come back discriminated by __typename:

const node = await client.query.node({ id }, { id: true, __typename: true, on: { Transaction: { amount: true }, Customer: { email: true } } });
if (node.ok && node.data?.__typename === "Transaction") node.data.amount;

Each method's default is exported, so adding a field is a spread: { ...queryTransactionSelection, status: true }. A raw selection-set string ("{ id amount }") is the escape hatch and returns the full type. Selection<T> and Selected<T, S> are exported for helpers of your own.

In Python the same override is the select= keyword with a raw selection set (client.query.transaction(id="txn_1", select="{ id amount }")). In Go it is a per-call option (client.Query.Transaction(ctx, &braintree.QueryTransactionParams{ID: "txn_1"}, braintree.WithSelection("{ id amount }"))). The CLI exposes it as --select, and MCP tools accept a select argument.

Connections

Relay-style connections auto-paginate. A field taking first and after and returning edges plus pageInfo (with hasNextPage and endCursor) is detected as a list operation. Iteration yields edges, and pages advance by resending the query with after set to the previous page's endCursor:

for await (const edge of client.query.transactions({ first: 50 })) {
  edge.node; // Transaction
}

Errors

GraphQL reports failures in-band, as a 200 carrying an errors array. The runtime turns that into GraphQLRequestError, carrying the full errors array, on the error side of ApiResult. Transport failures and undocumented statuses surface as TransportError and UnexpectedApiError exactly as they do for OpenAPI. Successful payloads are unwrapped: result.data is the field's value, not the { data: { ... } } envelope.

Limits

  • Runtime validation does not cover GraphQL operations.
  • Spec patches address OpenAPI documents. GraphQL schemas generate unpatched, with a warning when patches are configured.
  • The free plan's 25-operation allowance counts root fields, in schema order (queries first, then mutations). The generated README says when output was capped.

On this page