# Design URL: /docs/design How Pothos carries type information — the SchemaTypes generic and Ref objects — and why that split keeps large schemas and plugins modular. Pothos gives you a fully typed schema with no code-generation step. To pull that off it has to carry the TypeScript shape of every type and field all the way through to the resolver that uses it. It does this two ways, and knowing which is which explains most of how Pothos behaves. ## The SchemaTypes generic [#the-schematypes-generic] The first mechanism is the `SchemaTypes` parameter you pass to `SchemaBuilder`. It's a single object of type-only configuration, shared across the whole schema. It carries your [`Context`](./fundamentals/context) shape and any Object, Interface, or Scalar type you want to reference later by name as a string. ```typescript const builder = new SchemaBuilder<{ Context: { currentUser: ICharacter }; Objects: { Race: IRace }; }>({}); // 'Race' is now usable by name anywhere a type is expected: t.field({ type: 'Race', resolve: () => findRace() }); ``` Keeping every type in one place is convenient: there's a single object to look at and a single name to spell. On a large schema it turns unwieldy, since every type piles into one generic and each addition widens a type the compiler re-checks everywhere it's referenced. ## Ref objects [#ref-objects] The second mechanism is the `Ref` object. Every builder method that creates a type or a field returns a `Ref` that carries the type information it represents: ```typescript const Race = builder.objectRef('Race'); // Race is a Ref that already knows its backing model — pass it directly: t.field({ type: Race, resolve: () => findRace() }); ``` Because a `Ref` holds its own type information, you never register it on `SchemaTypes` and never spell its name as a string. That's what makes the harder cases work: * **Unions and enums**, whose members are values rather than named entries in a generic. * **Large schemas**, where each type stays self-contained instead of swelling one central generic. * **Plugins that pull type information from another source**, like the Prisma plugin from your Prisma client or the simple-objects plugin from the object you hand it. They can mint a `Ref` for a type the builder's generic has never heard of. ## Why the split [#why-the-split] Separating the type information (the `Ref`) from the implementation (`implement`) is what keeps Pothos modular. A `Ref` exists before its fields do, so two types can reference each other without forward-declaration gymnastics. A plugin can hand you a `Ref` for a type it derived from somewhere else without ever touching the central generic. And each type carries its own shape, so nothing has to stay in sync with a schema-wide registry. This is why `objectRef` is the pattern the guide uses first: it keeps a type and everything the compiler knows about it in one place. The `SchemaTypes` generic is still there when you specifically want a name to hang behavior on: string-referenced types, the `Context` shape, and the plugin slots that carry an ORM's generated type information (`PrismaTypes`, `DrizzleRelations`). The two interoperate freely; most schemas use `objectRef` for the bulk of their types and the generic for the handful that need a name. # Overview URL: /docs Build fully type-safe GraphQL schemas in TypeScript, without writing your types twice. Pothos is a library for building GraphQL schemas in TypeScript. You define your types, fields, and resolvers with a builder, and the whole schema is fully type-safe without writing your types twice. The builder keeps track of your data's types wherever they are used, so there is no SDL to keep in sync and no code-generation step. `builder.toSchema()` hands you a standard graphql-js `GraphQLSchema` at startup, so any server can run it, whether that's [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server), Apollo, or another implementation. Pothos does its work in the type system and while the schema builds; your resolvers stay ordinary functions. Core depends only on `graphql`, and everything past it (Relay, scope-auth, errors, validation, dataloaders, Prisma, Drizzle, federation) is a plugin that extends the same builder. ```typescript playground example="getting-started-first-schema-step-2" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); // Character is backed by the TypeScript shape in the generic const Character = builder.objectRef<{ id: string; name: string }>('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.queryType({ fields: (t) => ({ frodo: t.field({ type: Character, resolve: () => ({ id: '1', name: 'Frodo Baggins' }), }), }), }); export const schema = builder.toSchema(); ``` Open the example in the playground to run it, edit the resolver, and watch the SDL update. The [Introduction](./getting-started/introduction) has the full narrative: how Pothos compares to schema-first tools and the mental model the guide builds on. ## How it fits together [#how-it-fits-together] The example above is the whole pipeline in miniature: * The backing model (`{ id: string; name: string }`) is a plain TypeScript type describing the data. `builder.objectRef()` creates a GraphQL type based on it. * The `frodo` resolver needs to return that shape, and TypeScript will report an error if it doesn't. * `builder.toSchema()` produces a normal graphql-js `GraphQLSchema` that works with any GraphQL server. Plugins add methods to the same builder (`t.connection`, `authScopes`, `t.prismaField`) with the same type safety as the built-in ones. ## Explore the docs [#explore-the-docs] # LLM integration URL: /docs/llms Machine-readable routes that serve the docs as plain text for LLMs and AI tools. This site serves machine-readable routes so LLMs and AI tools can read the Pothos documentation without scraping HTML. Point a model at these URLs to give it the full, up-to-date reference. ## Full documentation [#full-documentation] [`/llms-full.txt`](/llms-full.txt) concatenates every page into a single plain-text file, optimized for LLM consumption. Each entry carries the page title and URL, its description, and the full page content — enough for a model to answer questions across the whole corpus. ## Individual pages [#individual-pages] Append `.mdx` to any docs path to fetch that one page as clean, parseable MDX, for example [`/docs/fundamentals/objects.mdx`](/docs/fundamentals/objects.mdx). Use this to hand a model a single topic instead of the entire site. # Playground URL: /docs/playground Embed runnable Pothos schemas in the docs with the playground fence and its example, query, and tab attributes. Most code blocks in these docs are live. Add the `playground` marker to a TypeScript fence and it gains an "Open in Playground" button that boots the code in an in-browser sandbox: an editor, the generated SDL, and a GraphiQL tab, with nothing to install. Attributes on the fence decide what opens and what runs. ## Inline code [#inline-code] The `playground` marker on its own makes a fence runnable. Clicking loads the fence's exact code: ```ts playground import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); const Character = builder.objectRef<{ id: string; name: string }>('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.queryType({ fields: (t) => ({ frodo: t.field({ type: Character, resolve: () => ({ id: 'frodo', name: 'Frodo Baggins' }), }), }), }); export const schema = builder.toSchema(); ``` ## Registered examples [#registered-examples] Point `example` at a bundle ID to open a full, multi-file example instead of the single fence. The docs still show a focused slice; clicking loads every file in the bundle along with its default query: ```ts playground example="getting-started-first-schema-step-2" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); // Shown here in the docs; "Open in Playground" loads the full // getting-started-first-schema-step-2 bundle — every file, plus its default query. const Character = builder.objectRef<{ id: string; name: string }>('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` `example` values are kebab-case IDs that match a directory under `website/playground-examples/`. Every reference must resolve to a real bundle, or `pnpm check-playground-refs` fails the build. Keep the fence a faithful subset of the bundle's `schema.ts` so opening it is not jarring. ## Pre-filled queries [#pre-filled-queries] Add `query` to open GraphiQL with an operation already typed in and focused: ```ts playground query="{ frodo { id name } }" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); const Character = builder.objectRef<{ id: string; name: string }>('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.queryType({ fields: (t) => ({ frodo: t.field({ type: Character, resolve: () => ({ id: 'frodo', name: 'Frodo Baggins' }), }), }), }); export const schema = builder.toSchema(); ``` ## Example plus query [#example-plus-query] Combine the two to open a full bundle with a specific operation ready to run, handy for pointing readers at one capability of a larger schema: ```ts playground example="getting-started-first-schema-step-2" query="{ frodo { id name } }" // Opens the getting-started-first-schema-step-2 bundle with the query pre-filled. ``` The pre-filled query must be valid against the bundle it opens. Here `{ frodo { id name } }` matches the `frodo` field that step 2 defines. ## Multi-step examples [#multi-step-examples] A bundle can ship a progression of steps, each a self-contained schema in its own `step-N/` directory with its own default query. Reference a step with the `-step-N` suffix; the number resolves to that subdirectory: ```ts playground example="getting-started-first-schema-step-2" // Opens step 2 of the getting-started-first-schema bundle. ``` Steps suit a page that builds a schema up one change at a time — see [Installation](./getting-started/installation), which opens step 1 and then step 2 as it walks through a first schema. ## Switchable definition styles [#switchable-definition-styles] To show one schema written several ways (object refs, classes, or builder types), give each fence a `tab` label. Consecutive fences that share the `tab` attribute merge into a single block with a style switcher, and "Open in Playground" opens whichever tab is selected: ````mdx ```ts playground example="fundamentals-objects" tab="Object refs" // primary style — the default (first) tab ``` ```ts playground example="fundamentals-objects-variant-classes" tab="Classes" // alternative style ``` ```ts playground example="fundamentals-objects-variant-builder-types" tab="Builder types" // alternative style ``` ```` Each fence's `example` is the exact bundle it opens: the base ID for the default style, and `-variant-` for the others. Author the alternatives as `variant-/` subdirectories of the base bundle (see the [playground examples README](https://github.com/hayes/pothos/blob/main/website/playground-examples/README.md)). A bundle uses steps **or** variants, never both. ## The playground interface [#the-playground-interface] Every playground opens with three tabs: * **Code**: the TypeScript source, editable in place. * **Schema**: the generated GraphQL SDL, which updates as you edit. * **GraphiQL**: an interactive query editor wired to the schema. Edit the code and the SDL and query results follow, so you can see exactly how a Pothos change reshapes the schema. # Troubleshooting URL: /docs/troubleshooting Fixes for the most common Pothos issues you'll hit. ## Type errors are unreadable [#type-errors-are-unreadable] Pothos's type inference produces dense errors when something doesn't fit. Two tweaks make them readable: **Turn on `strict` mode.** Without it, the inference Pothos relies on collapses and the resulting errors look stranger. ```json { "compilerOptions": { "strict": true } } ``` **Extract the builder generic into a named interface.** The error message references your interface by name instead of inlining its full structure: ```typescript interface PothosTypes { Context: { user?: { id: string }; }; } const builder = new SchemaBuilder({}); ``` ## VS Code or `tsc` is slow [#vs-code-or-tsc-is-slow] Pothos's types are deliberately rich. Two things make them disproportionately slow: * **Complex `Context` types.** Avoid putting whole ORM models on the context type. Reference handles (`db`, `pubSub`), not types like `Prisma.UserGetPayload<{...}>` whose definitions are large. See [microsoft/TypeScript#45405](https://github.com/microsoft/TypeScript/issues/45405) for background. * **Inferring through many `objectRef`s in one expression.** If a single field's resolve function references a dozen other refs, TypeScript has to chase each one. Splitting field declarations across separate `objectField` calls usually fixes it. ## "Plugin methods are not defined" [#plugin-methods-are-not-defined] Pothos plugins extend the builder's prototype at import time. If two copies of `@pothos/core` exist in your `node_modules` (usually from pnpm hoisting or a mismatched workspace version), the plugin patches one copy while your app uses the other. Check for duplicates: ```bash npm ls @pothos/core # or pnpm why @pothos/core ``` The fix is to deduplicate so exactly one `@pothos/core` lives in the root `node_modules`. ## "Received multiple implementations for plugin" [#received-multiple-implementations-for-plugin] By default Pothos won't accept the same plugin registered twice. This usually surfaces when HMR re-runs the builder module without clearing the registry. To allow re-registration during development: ```typescript import SchemaBuilder from '@pothos/core'; SchemaBuilder.allowPluginReRegistration = true; ``` Set it before any plugin imports, and leave it off in production builds. ## "Cannot read property X of undefined" at startup [#cannot-read-property-x-of-undefined-at-startup] Most of these are circular imports. Pothos handles circular imports correctly *if* two rules hold: 1. The file that constructs the builder (`builder.ts`) imports nothing that uses the builder. 2. The file that calls `builder.toSchema()` (`schema.ts`) isn't imported by any of the files that use the builder. A common shape that works: ``` builder.ts ← exports `builder` ↑ domain/*.ts ← imports `builder`, registers types and queries ↑ schema.ts ← imports each domain module for side effects, calls toSchema() ``` See [Project layout](./patterns/project-layout) for the full pattern. ## Refs are undefined inside a resolver [#refs-are-undefined-inside-a-resolver] A symptom of the same circular-import class. The most reliable fix is the layout above — `builder` lives in its own file, types are declared in domain modules, and `schema.ts` is the only thing that calls `toSchema()`. Hit an issue not covered here? Open one at [github.com/hayes/pothos](https://github.com/hayes/pothos/issues) with a minimal reproduction. # Using plugins URL: /docs/using-plugins Install a Pothos plugin, register it on the builder, and understand the rules that apply to every plugin. Plugins extend Pothos with features that feel like they're part of the core API: auth scopes, validation, error unions, ORM integrations, Relay connections. Each plugin documents its own setup, but they all follow the same shape. ## Installing one [#installing-one] npm pnpm yarn bun ```bash npm install @pothos/plugin-scope-auth ``` ```bash pnpm add @pothos/plugin-scope-auth ``` ```bash yarn add @pothos/plugin-scope-auth ``` ```bash bun add @pothos/plugin-scope-auth ``` Import the plugin, list it in `plugins:`, and configure it if it asks for it: ```typescript import SchemaBuilder from '@pothos/core'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; const builder = new SchemaBuilder<{ Context: { user?: { id: string } }; AuthScopes: { loggedIn: boolean }; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { authScopes: (ctx) => ({ loggedIn: !!ctx.user, }), }, }); ``` Two slots are always involved: * **The generic** carries any types the plugin needs to know: `AuthScopes` for scope-auth, `DrizzleRelations` for drizzle, `PrismaTypes` for prisma. * **The constructor options** carry any runtime config: `scopeAuth: { authScopes: ... }`, `drizzle: { client, ... }`. The plugin's own docs spell out which keys. Once registered, the plugin's methods appear on the builder as if they were core methods. `t.withAuth({...})` only exists when scope-auth is loaded; `t.prismaField` only exists when the prisma plugin is loaded. ## Plugin order matters [#plugin-order-matters] Plugins are applied in reverse list order. The first plugin listed becomes the outermost wrapper at runtime — its hooks fire first on the way in, last on the way out. Put authorization-style plugins (anything that should reject early) near the front: ```typescript plugins: [ ScopeAuthPlugin, // applied last → wraps everything, runs first ErrorsPlugin, ValidationPlugin, RelayPlugin, ], ``` This ordering matters when one plugin's behavior depends on another's effect already being in place. The docs for each plugin call out the cases where order is load-bearing. ## Plugin catalog [#plugin-catalog] The full plugin index lives at [Plugins](./plugins). Some pointers: * **Auth and validation.** [`plugin-scope-auth`](./plugins/scope-auth), [`plugin-validation`](./plugins/validation). * **Error handling.** [`plugin-errors`](./plugins/errors) for typed result unions. * **ORMs.** [`plugin-prisma`](./plugins/prisma), [`plugin-drizzle`](./plugins/drizzle). * **Relay.** [`plugin-relay`](./plugins/relay) for connections, cursors, and node interfaces. * **Federation.** [`plugin-federation`](./plugins/federation), [`plugin-sub-graph`](./plugins/sub-graph). * **Performance.** [`plugin-dataloader`](./plugins/dataloader), [`plugin-complexity`](./plugins/complexity). * **Schema utilities.** [`plugin-directives`](./plugins/directives), [`plugin-mocks`](./plugins/mocks), [`plugin-simple-objects`](./plugins/simple-objects). # ArgBuilder URL: /docs/api/arg-builder Reference for the arg builder `t.arg` — the arg method and its scalar helpers. The arg builder is the `t.arg` object on a [`FieldBuilder`](./field-builder). Its methods define a field's arguments. For a guided introduction, see [Arguments](../fundamentals/args). ## `arg(options)` [#argoptions] * `options`: `ArgOptions` ### `ArgOptions` [#argoptions-1] ```typescript type ArgOptions = { type: ReturnType; required?: boolean; defaultValue?: DefaultValue; description?: string; deprecationReason?: string; extensions?: Record; }; ``` * `type`: [Type Parameter](./arg-builder#type-parameter) * `required`: whether the arg must be provided. Defaults to `false` unless you flip it in the builder — see [Default nullability](../patterns/default-nullability). * `defaultValue`: default value used when the arg is omitted. Its type follows the `type` option. * `description`: text description of the arg, surfaced in tools like GraphiQL. * `deprecationReason`: marks the arg deprecated with this reason. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ### Type parameter [#type-parameter] An arg's type can be any `InputTypeRef` returned by a [`SchemaBuilder`](./schema-builder) method that defines an `InputObject`, `Enum`, or `Scalar`, a TypeScript enum used to define a GraphQL enum, or a string matching a key of the `Scalars` map in `SchemaTypes`. For list args, wrap any of the above in an array — for example `['ID']`. ## Scalar helpers [#scalar-helpers] Shortcuts for defining scalar args. Each works like [`arg`](./arg-builder#argoptions) but omits the `type` option: * `arg.string(options)` * `arg.id(options)` * `arg.boolean(options)` * `arg.int(options)` * `arg.float(options)` * `arg.stringList(options)` * `arg.idList(options)` * `arg.booleanList(options)` * `arg.intList(options)` * `arg.floatList(options)` * `arg.listRef(type, options)` # FieldBuilder URL: /docs/api/field-builder Reference for the field builder `t` — field, the scalar helpers, and the expose helpers. The field builder is the `t` argument passed to every `fields` function. Its methods return field refs. For a guided introduction to defining fields and writing resolvers, see [Fields](../fundamentals/fields) and [Resolvers](../fundamentals/resolvers). ## `field(options)` [#fieldoptions] * `options`: `FieldOptions` ### `FieldOptions` [#fieldoptions-1] ```typescript type FieldOptions = { type: ReturnType; args?: Args; nullable?: boolean; description?: string; deprecationReason?: string; extensions?: Record; resolve: (parent, args, context, info) => ResolveValue; }; ``` * `type`: [Type Parameter](./field-builder#type-parameter) * `args`: a map of arg name to arg definition. Create arg definitions with the [`InputFieldBuilder`](./input-field-builder) (`t.arg`) or with [`builder.args`](./schema-builder#argsfields). * `nullable`: whether this field can return `null`. Defaults to `true` unless you flip it in the builder — see [Default nullability](../patterns/default-nullability). * `description`: text description of the field, surfaced in tools like GraphiQL. * `deprecationReason`: marks the field deprecated with this reason. * `extensions`: arbitrary extension metadata, read by directives and server plugins. * `resolve`: [Resolver](./field-builder#resolver) ### Type parameter [#type-parameter] A field's type can be any `TypeRef` returned by a [`SchemaBuilder`](./schema-builder) type-definition method, a class used to create an object or interface type, a TypeScript enum used to define a GraphQL enum, or a string matching a key of the `Objects`, `Interfaces`, or `Scalars` maps in `SchemaTypes`. For list fields, wrap any of the above in an array — for example `['Character']`. ### Resolver [#resolver] A function that resolves the value of the field. It should return a value (or promise) matching the field's type. For `Scalars`, `Objects`, and `Interfaces` that is the corresponding shape from `SchemaTypes`. For unions, it may be any member's shape. For enums, the value depends on the enum's definition; see [Enums](../fundamentals/enums). The resolver receives four arguments: * `parent`: the backing model of the current type, as declared in `SchemaTypes`. * `args`: an object matching the field's `args` option. * `context`: the `Context` type from `SchemaTypes`. * `info`: a [`GraphQLResolveInfo`](https://graphql.org/graphql-js/type/#graphqlobjecttype) object describing how the field was queried. ## Scalar helpers [#scalar-helpers] Shortcuts for defining scalar fields. Each works like [`field`](./field-builder#fieldoptions) but omits the `type` option: * `string(options)` * `id(options)` * `boolean(options)` * `int(options)` * `float(options)` * `stringList(options)` * `idList(options)` * `booleanList(options)` * `intList(options)` * `floatList(options)` * `listRef(type, options)` ## Expose helpers [#expose-helpers] Shortcuts for exposing a property of the backing model directly, without a resolver. The `name` argument can be any property of the backing model whose type matches the field. Options are the same as [`field`](./field-builder#fieldoptions), with `type` and `resolve` omitted: * `exposeString(name, options)` * `exposeID(name, options)` * `exposeBoolean(name, options)` * `exposeInt(name, options)` * `exposeFloat(name, options)` * `exposeStringList(name, options)` * `exposeIDList(name, options)` * `exposeBooleanList(name, options)` * `exposeIntList(name, options)` * `exposeFloatList(name, options)` The expose helpers exist only on object and interface field builders, where a backing model is in scope. They are not available on `Query`, `Mutation`, or `Subscription`, whose root value carries no properties to expose. # InputFieldBuilder URL: /docs/api/input-field-builder Reference for the input field builder — the field method and its scalar helpers. The input field builder is the `t` argument passed to the `fields` function of [`builder.inputType`](./schema-builder#inputtypeparam-options). Its methods define the fields of an input object. For a guided introduction, see [Input objects](../fundamentals/inputs). The same builder backs a field's arguments through [`t.arg`](./arg-builder). ## `field(options)` [#fieldoptions] * `options`: `InputFieldOptions` ### `InputFieldOptions` [#inputfieldoptions] ```typescript type InputFieldOptions = { type: ReturnType; required?: boolean; defaultValue?: DefaultValue; description?: string; deprecationReason?: string; extensions?: Record; }; ``` * `type`: [Type Parameter](./input-field-builder#type-parameter) * `required`: whether the field must be provided. Defaults to `false` unless you flip it in the builder — see [Default nullability](../patterns/default-nullability). * `defaultValue`: default value used when the field is omitted. Its type follows the `type` option. * `description`: text description of the field, surfaced in tools like GraphiQL. * `deprecationReason`: marks the field deprecated with this reason. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ### Type parameter [#type-parameter] An input field's type can be any `InputTypeRef` returned by a [`SchemaBuilder`](./schema-builder) method that defines an `InputObject`, `Enum`, or `Scalar`, a TypeScript enum used to define a GraphQL enum, or a string matching a key of the `Scalars` map in [`SchemaTypes`](./schema-builder#schematypes). For list fields, wrap any of the above in an array — for example `['ID']`. ## Scalar helpers [#scalar-helpers] Shortcuts for defining scalar input fields. Each works like [`field`](./input-field-builder#inputfieldoptions) but omits the `type` option: * `string(options)` * `id(options)` * `boolean(options)` * `int(options)` * `float(options)` * `stringList(options)` * `idList(options)` * `booleanList(options)` * `intList(options)` * `floatList(options)` * `listRef(type, options)` # SchemaBuilder URL: /docs/api/schema-builder Reference for SchemaBuilder — its generic, constructor options, and every type-building method. `SchemaBuilder` is the core class of Pothos. You use it to define every type in your schema, then call `toSchema` to produce a [`graphql-js`](https://graphql.org/graphql-js/) `GraphQLSchema`. For a guided walk through configuring the builder, see [SchemaBuilder](../fundamentals/schema-builder) in the fundamentals. ## `constructor(options)` [#constructorschematypesoptions] * typeParam `SchemaTypes`: a type describing the backing models, context, and defaults for your schema. * `options`: `SchemaBuilderOptions` ### `SchemaTypes` [#schematypes] ```typescript type SchemaTypes = { // Shape of the `context` arg in your resolvers. Context?: object; // Shape of the `parent`/root value passed to root fields. Root?: object; // A map of Object type names to their backing models. Objects?: object; // A map of Input type names to their backing models. Inputs?: object; // A map of Interface type names to their backing models. Interfaces?: object; // Map of scalar names to Input and Output shapes. Use it to overwrite the // default scalar types, or to add type information for custom scalars. Scalars?: { [s: string]: { Input: unknown; Output: unknown; }; }; // When `false`, fields are non-nullable by default (requires the matching // `defaultFieldNullability` builder option). DefaultFieldNullability?: boolean; // When `true`, input fields and arguments are required by default (requires // the matching `defaultInputFieldRequiredness` builder option). DefaultInputFieldRequiredness?: boolean; }; ``` ### `SchemaBuilderOptions` [#schemabuilderoptions] ```typescript type SchemaBuilderOptions = {}; ``` The core builder takes no options. Plugins contribute their own, such as `plugins: [...]` and each plugin's config block. See the individual plugin pages for what each adds. ## `queryType(options, fields?)` [#querytypeoptions-fields] Creates the `Query` type with a set of fields. * `options`: `QueryTypeOptions` * `fields?`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ### `QueryTypeOptions` [#querytypeoptions] ```typescript type QueryTypeOptions = { description?: string; fields?: FieldsFunction; }; ``` * `description`: a description of the `Query` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `queryFields(fields)` [#queryfieldsfields] Adds a set of fields to the `Query` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `queryField(name, field)` [#queryfieldname-field] Adds a single field to the `Query` type. * `name`: the name of the field. * `field`: a function that receives a [`FieldBuilder`](./field-builder) and returns a field ref. ## `mutationType(options, fields?)` [#mutationtypeoptions-fields] Creates the `Mutation` type with a set of fields. * `options`: `MutationTypeOptions` * `fields?`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ### `MutationTypeOptions` [#mutationtypeoptions] ```typescript type MutationTypeOptions = { description?: string; fields?: FieldsFunction; }; ``` * `description`: a description of the `Mutation` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `mutationFields(fields)` [#mutationfieldsfields] Adds a set of fields to the `Mutation` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `mutationField(name, field)` [#mutationfieldname-field] Adds a single field to the `Mutation` type. * `name`: the name of the field. * `field`: a function that receives a [`FieldBuilder`](./field-builder) and returns a field ref. ## `subscriptionType(options, fields?)` [#subscriptiontypeoptions-fields] Creates the `Subscription` type with a set of fields. * `options`: `SubscriptionTypeOptions` * `fields?`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ### `SubscriptionTypeOptions` [#subscriptiontypeoptions] ```typescript type SubscriptionTypeOptions = { description?: string; fields?: FieldsFunction; }; ``` * `description`: a description of the `Subscription` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `subscriptionFields(fields)` [#subscriptionfieldsfields] Adds a set of fields to the `Subscription` type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `subscriptionField(name, field)` [#subscriptionfieldname-field] Adds a single field to the `Subscription` type. * `name`: the name of the field. * `field`: a function that receives a [`FieldBuilder`](./field-builder) and returns a field ref. ## `objectType(param, options, fields?)` [#objecttypeparam-options-fields] Defines an object type. `param` can be a class, an `ObjectRef`, or a `SchemaTypes`-registered name; see [Object types](../fundamentals/objects) for how the three forms differ. * `param`: a key of the `Objects` property in `SchemaTypes`, a class, or an `ObjectRef` created by `builder.objectRef`. * `options`: `ObjectTypeOptions` * `fields?`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ### `ObjectTypeOptions` [#objecttypeoptions] ```typescript type ObjectTypeOptions = { description?: string; fields?: FieldsFunction; interfaces?: Interfaces; isTypeOf?: (obj, context, info) => boolean; name?: string; extensions?: Record; }; ``` * `description`: a description of the type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. * `isTypeOf`: recommended when implementing interfaces. Called during execution to decide whether a value of an implemented interface is of this type. * `interfaces`: an array of interfaces this type implements. Each item is an interface param (see the `param` argument of `interfaceType`). * `name`: name of the GraphQL type. Required when `param` is a class. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ## `objectFields(param, fields)` [#objectfieldsparam-fields] Adds a set of fields to an object type. * `param`: a key of the `Objects` property in `SchemaTypes`, a class, or an `ObjectRef` created by `builder.objectRef`. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `objectField(param, name, field)` [#objectfieldparam-name-field] Adds a single field to an object type. * `param`: a key of the `Objects` property in `SchemaTypes`, a class, or an `ObjectRef` created by `builder.objectRef`. * `name`: the name of the field. * `field`: a function that receives a [`FieldBuilder`](./field-builder) and returns a field ref. ## `objectRef(name)` [#objectreftname] Creates a reference to an object type before it is implemented. Use it to break circular references, to build modular schemas without registering every type on `SchemaTypes`, or when writing plugins. * `name`: name of the type this ref represents. Can be overwritten when the ref is implemented. * `T`: the backing model, the shape your resolvers return and Pothos hands back as `parent`. The returned ref carries an `implement` method, so you can define fields directly: `builder.objectRef('Race').implement({ fields: ... })`. ## `interfaceType(param, options, fields?)` [#interfacetypeparam-options-fields] Defines an interface type. * `param`: a key of the `Interfaces` property in `SchemaTypes`, a class, or an `InterfaceRef` created by `builder.interfaceRef`. * `options`: `InterfaceTypeOptions` * `fields?`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ### `InterfaceTypeOptions` [#interfacetypeoptions] ```typescript type InterfaceTypeOptions = { description?: string; fields?: FieldsFunction; interfaces?: Interfaces; resolveType?: (parent, context, info) => string; name?: string; extensions?: Record; }; ``` * `description`: a description of the type. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. * `interfaces`: an array of interfaces this interface extends. Each item is an interface param (see the `param` argument of `interfaceType`). * `resolveType`: returns the name of the concrete type for a given value. An alternative to setting `isTypeOf` on each implementing object type. * `name`: name of the GraphQL type. Required when `param` is a class. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ## `interfaceFields(param, fields)` [#interfacefieldsparam-fields] Adds a set of fields to an interface type. * `param`: a key of the `Interfaces` property in `SchemaTypes`, a class, or an `InterfaceRef` created by `builder.interfaceRef`. * `fields`: a function that receives a [`FieldBuilder`](./field-builder) and returns a map of field names to field refs. ## `interfaceField(param, name, field)` [#interfacefieldparam-name-field] Adds a single field to an interface type. * `param`: a key of the `Interfaces` property in `SchemaTypes`, a class, or an `InterfaceRef` created by `builder.interfaceRef`. * `name`: the name of the field. * `field`: a function that receives a [`FieldBuilder`](./field-builder) and returns a field ref. ## `interfaceRef(name)` [#interfacereftname] Creates a reference to an interface type before it is implemented. Use it to break circular references, to build modular schemas, or when writing plugins. * `name`: name of the type this ref represents. Can be overwritten when the ref is implemented. * `T`: the backing model, the shape shared by every implementing type. ## `unionType(name, options)` [#uniontypename-options] Defines a union type. * `name`: the name of the union. * `options`: `UnionTypeOptions` ### `UnionTypeOptions` [#uniontypeoptions] ```typescript type UnionTypeOptions = { description?: string; types: Member[] | (() => Member[]); resolveType?: (parent, context, info) => MaybePromise; extensions?: Record; }; ``` * `description`: a description of the type. * `types`: the object types included in the union: an array, or a thunk returning one so members can be referenced before they are defined. Each item is an object param (see the `param` argument of `objectType`). * `resolveType`: called when resolving the type of a union value. `parent` is a union of the backing models of the member types. Return the name of the matching member type. Optional if each member type sets `isTypeOf`, but supplying it here is the usual approach. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ## `enumType(param, options)` [#enumtypeparam-options] Defines an enum type. * `param`: a string name for the enum, or a TypeScript enum. * `options`: `EnumTypeOptions` ### `EnumTypeOptions` [#enumtypeoptions] ```typescript type EnumTypeOptions = { description?: string; values?: Values; name?: string; extensions?: Record; }; ``` * `description`: a description of the type. * `values`: either an array of strings (you may need `as const` to get precise value names) or a `GraphQLEnumValueConfigMap`. Required when `param` is not a TypeScript enum. * `name`: required when `param` is a TypeScript enum. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ## `scalarType(name, options)` [#scalartypename-options] Defines a custom scalar. * `name`: a key of the `Scalars` property in `SchemaTypes`. * `options`: `ScalarTypeOptions` ### `ScalarTypeOptions` [#scalartypeoptions] ```typescript type ScalarTypeOptions = { description?: string; // Serializes an internal value to include in a response. serialize?: GraphQLScalarSerializer; // Parses an externally provided value to use as an input. parseValue?: GraphQLScalarValueParser; // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser; extensions?: Readonly>; }; ``` On graphql-js 17 the newer coercion hooks (`coerceOutputValue`, `coerceInputValue`, `coerceInputLiteral`, and `valueToLiteral`) are also accepted and forwarded to the underlying scalar config. This is why `serialize` is optional: supply either `serialize` or `coerceOutputValue`. The hooks are ignored on graphql-js 16, so `serialize`/`parseValue`/`parseLiteral` remain the portable choice. ## `addScalarType(name, scalar, options?)` [#addscalartypename-scalar-options] Registers an existing `GraphQLScalarType` (for example, one from `graphql-scalars`) under a `SchemaTypes` name. * `name`: a key of the `Scalars` property in `SchemaTypes`. * `scalar`: a `GraphQLScalarType`. * `options?`: the same options as `scalarType`, with `serialize` optional since the passed scalar already supplies one. Anything you set here overrides the scalar's own config. ## `inputType(param, options)` [#inputtypeparam-options] Defines an input object type. * `param`: a string name, or an `InputObjectRef` created by `builder.inputRef`. * `options`: `InputTypeOptions` ### `InputTypeOptions` [#inputtypeoptions] ```typescript type InputTypeOptions = { description?: string; fields: InputFieldsFunction; isOneOf?: boolean; extensions?: Record; }; ``` * `description`: a description of the type. * `fields`: a function that receives an [`InputFieldBuilder`](./input-field-builder) and returns a map of field names to field definitions. When `param` is a key of the `Inputs` property in `SchemaTypes`, the shape is type-checked against the registered backing model. * `isOneOf`: marks the type as a `@oneOf` input, where exactly one field may be provided. All fields must be nullable. * `extensions`: arbitrary extension metadata, read by directives and server plugins. ## `inputRef(name)` [#inputreftname] Creates a reference to an input object type before it is implemented. Use it for recursive input types, for modular schemas, or when writing plugins. * `name`: name of the type this ref represents. Can be overwritten when the ref is implemented. * `T`: the backing shape of the input. ## `args(fields)` [#argsfields] Creates a standalone arguments object you can reuse as the `args` option on a field definition. * `fields`: a function that receives an [`ArgBuilder`](./arg-builder) and returns a map of arg names to arg definitions. ## `toSchema(options?)` [#toschemaoptions] Builds and returns a [`GraphQLSchema`](https://graphql.org/graphql-js/type/#graphqlschema) from every type registered on the builder. * `options?`: `BuildSchemaOptions`, carrying build-time options such as `directives` and `extensions`. Plugins add their own keys here. Earlier docs described `toSchema` as taking an array of types. It does not — types register themselves on the builder as you define them, and `toSchema` takes only an optional options object. ## `SchemaBuilder.allowPluginReRegistration` [#schemabuilderallowpluginreregistration] A static `boolean` on the `SchemaBuilder` class. When `true`, a plugin may call `registerPlugin` more than once — useful for hot-module reloading. It defaults to `false` so duplicate copies of a plugin surface as an error rather than a silent conflict. # Arguments URL: /docs/fundamentals/args Declare the arguments a field accepts, and make each one required, optional, or default-valued. A field can take arguments, declared in an `args` map alongside its `type` and `resolve`. Each entry's key is the argument's name, and its value is built with `t.arg`. Pothos derives the arguments' TypeScript types from that map. ## Declaring arguments [#declaring-arguments] ```typescript playground example="fundamentals-args" builder.queryType({ fields: (t) => ({ characters: t.field({ type: [Character], args: { raceId: t.arg.string(), limit: t.arg.int({ required: true, defaultValue: 25 }), excludeIds: t.arg.idList({ required: true, defaultValue: [] }), }, resolve: (_root, args) => { let result = characters; if (args.raceId) { result = result.filter((c) => c.raceId === args.raceId); } const excluded = new Set(args.excludeIds); return result.filter((c) => !excluded.has(c.id)).slice(0, args.limit); }, }), }), }); ``` `characters` takes three arguments. The keys in the `args` map (`raceId`, `limit`, and `excludeIds`) are the argument names clients write in a query, and each value comes from `t.arg`. In the resolver, the second parameter, `args`, holds those values with the types Pothos derived from the map, so `args.limit` is a `number` and `args.excludeIds` is a `string[]`. The general form is `t.arg({ type })`, which takes the argument's `type` and returns the argument. The built-in scalars each have a shorthand: `t.arg.string`, `t.arg.int`, `t.arg.id`, `t.arg.boolean`, and `t.arg.float`. Each of those has a `…List` form as well (`t.arg.stringList`, `t.arg.idList`, and so on) for list arguments. Any other type, such as an enum or input object, uses the general form. ## Required and optional arguments [#required-and-optional-arguments] By default an argument is optional. Pothos follows GraphQL, where an argument with no `!` may be omitted, so `t.arg.string()` produces an argument the resolver sees as `string | null | undefined`. Pass `required: true` to make it non-nullable: ```typescript raceId: t.arg.string({ required: true }), ``` Now the resolver sees `raceId: string`. A list argument's `required` also accepts a `{ list, items }` object, since the list and its items can each be null on their own. As with field nullability, the schema-wide default can be changed with a builder option, covered in [Default nullability](../patterns/default-nullability). ## Default values [#default-values] `defaultValue` gives an argument a value to use when the client leaves it out: ```typescript limit: t.arg.int({ defaultValue: 25 }), ``` GraphQL substitutes the default only when the argument is omitted; if the client passes `null` explicitly, the resolver receives `null`. The default does not change the argument's type: with only `defaultValue` set, `limit` is still `number | null | undefined` in the resolver, because the argument is still optional. The `characters` field above combines `defaultValue` with `required: true`, so `limit` is non-nullable and falls back to the default when omitted, and the resolver sees a plain `number`. ## Input objects as argument types [#input-objects-as-argument-types] An argument's `type` can be any input type: a scalar, an enum, or an input object. A `filter: t.arg({ type: CharacterFilter })` argument, for example, works the same way as the scalar arguments above. [Input objects](./inputs) covers defining them and the options their fields take. # Context URL: /docs/fundamentals/context The per-request context object, how to type it on the builder, and how resolvers read it. The context is the per-request value your server builds for each incoming request and hands to every resolver. It carries the values a resolver needs about the current request, like the signed-in user and the data sources to read from. You declare its type once on the builder's `Context` generic, and Pothos threads that type into every resolver as the third argument: ```typescript playground example="fundamentals-context" interface Context { user?: { id: number }; db: { charactersCreatedBy: (userId: number) => ICharacter[]; }; } const builder = new SchemaBuilder<{ Context: Context; }>({}); builder.queryType({ fields: (t) => ({ myCharacters: t.field({ type: [Character], nullable: true, resolve: (_root, _args, ctx) => ctx.user ? ctx.db.charactersCreatedBy(ctx.user.id) : null, }), }), }); ``` The `Context` interface describes what each request carries: an optional `user` (whatever your auth layer decoded from the request) and a `db` handle the resolvers query. Passing it as the `Context` entry of the builder generic makes it the static type of `ctx` in every resolver, so `myCharacters` reads `ctx.user` and `ctx.db` with full types and no casts. The field returns a list of the [`Character`](./objects) object type, resolving to the characters the signed-in user created, or `null` when nobody is signed in. ## What to put on context [#what-to-put-on-context] Context holds the values that differ from one request to the next: * **The signed-in user.** Whatever your authentication layer decoded from the request, or its absence when the request is anonymous. * **Request-scoped data sources.** A database or API client that varies per request, such as one authenticated as the current user. A client that's the same for every request doesn't need to go here; it can live in module scope (see below). * **Per-request caches.** Dataloaders and other values that should be created once per request and shared by the resolvers handling it. Values that are the same for every request, such as a connection pool, can live in module scope where the factory and resolvers already reach them. ## Creating the context per request [#creating-the-context-per-request] Pothos types the context, but building it is the server's job. With [`graphql-yoga`](https://the-guild.dev/graphql/yoga-server) you pass a `context` function on the server options, and yoga calls it for each request it handles, passing whatever it returns to the resolvers as `ctx`: ```typescript import { initContextCache } from '@pothos/core'; const yoga = createYoga({ schema, context: async ({ request }): Promise => ({ ...initContextCache(), user: await getUser(request.headers.get('authorization')), db, }), }); ``` The factory reads the incoming `request` (decoding the `authorization` header into a `user`) and returns the object that becomes `ctx`. Because it's `async`, the server awaits it before running any resolver. Its return type has to match the `Context` you declared on the builder; the `Promise` annotation is what keeps the two in agreement, since Pothos never sees the factory itself. The `...initContextCache()` spread is explained under [Per-request caches](#per-request-caches). The [First server](../getting-started/first-server) guide sets up the surrounding yoga server. ## Requiring a signed-in user [#requiring-a-signed-in-user] The `myCharacters` field above returns `null` when no one is signed in. A field that instead requires a signed-in user can check `ctx.user` at the top of the resolver and throw when it's missing. The check is an ordinary TypeScript guard, so the compiler narrows `ctx.user` to a defined value for the rest of the body, and the code below the guard reads `ctx.user.id` without a cast: ```typescript resolve: (_root, _args, ctx) => { if (!ctx.user) throw new Error('Sign in to continue'); // ctx.user is { id: number } from here down. return ctx.db.charactersCreatedBy(ctx.user.id); }, ``` A hand-written guard like this works for a single field. For real authorization, use [`plugin-scope-auth`](../plugins/scope-auth), which turns them into declarative auth scopes you attach to fields. ## Per-request caches [#per-request-caches] Context is also where request-scoped caches live. Pothos plugins that memoize per-request values, such as the loaders from [`plugin-dataloader`](../plugins/dataloader), store them in a `WeakMap` keyed on your context object. Spreading `...initContextCache()` into the object your factory returns adds a stable shared key, so those caches keep working even if your server copies or extends the context between creating it and running resolvers. # Enums URL: /docs/fundamentals/enums Define an enum with string-literal values and use it as a field type and argument. ## Defining an enum [#defining-an-enum] An enum is a type whose values are a fixed set of named constants, here the moral alignments a faction can hold. The lightest way to declare one is the array form: an array of string literals. ```typescript playground example="fundamentals-enums" const Alignment = builder.enumType('Alignment', { description: "A faction's moral leaning.", values: ['Good', 'Neutral', 'Evil'], }); ``` `builder.enumType` takes the type's name and an options object. The `values` array lists the enum's value names, and Pothos infers them as the literal union `'Good' | 'Neutral' | 'Evil'` (no `as const` needed, since `enumType` uses a `const` type parameter). That union is the TypeScript type Pothos threads through every field and argument built from the enum. The call returns a reference, `Alignment`, that works as both a field type and an argument type. ## Using the enum [#using-the-enum] An enum reference is used as a field's `type` and as an argument's `type` like any other type: ```typescript const Faction = builder.objectRef('Faction').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), alignment: t.field({ type: Alignment, resolve: (f) => f.alignment }), }), }); builder.queryType({ fields: (t) => ({ factions: t.field({ type: [Faction], args: { alignment: t.arg({ type: Alignment }) }, resolve: (_root, args) => args.alignment ? Factions.filter((f) => f.alignment === args.alignment) : Factions, }), }), }); ``` `Faction.alignment` returns the enum from the backing object, and the `factions` query takes an `alignment` argument to filter by. Because `Alignment` was built from the array form, that argument's type is the literal union `'Good' | 'Neutral' | 'Evil'` (nullable, since the argument is optional), so the resolver compares `f.alignment === args.alignment` against real union members rather than a bare string. The [Arguments](./args) guide covers arguments in more detail. ## Descriptions and deprecations [#descriptions-and-deprecations] The enum itself takes a `description` in either form; the array-form enum above carries one. To describe or deprecate individual values, switch from the array to the object form, where each key maps to a config object: ```typescript const Alignment = builder.enumType('Alignment', { description: "A faction's moral leaning.", values: { Good: { description: 'Sides with the free peoples.' }, Neutral: { description: 'Stays out of the war.' }, Evil: { description: 'Serves Sauron.' }, Chaotic: { deprecationReason: 'Never used in the compendium; removed in v3.' }, }, }); ``` Each key still drives both the GraphQL value name and the TypeScript union, so switching forms changes nothing about how the enum is used. `description` appears in introspection and tooling, and `deprecationReason` marks a value deprecated (here `Chaotic`) while leaving it usable. A value config can also set a `value`, the internal value your resolver works with. ## Backing with a TypeScript enum [#backing-with-a-typescript-enum] The value name a client sees and the value your resolver works with have been the same string so far, but they don't have to be. A GraphQL enum value has a name (the token in the schema, which clients send and receive) and an internal value (what a resolver returns and what an argument is parsed to before it reaches your code). When you already have a TypeScript `enum`, you can hand it to `builder.enumType` directly, and its keys and values fill those two roles: ```typescript enum Alignment { Good = 'GOOD', Neutral = 'NEUTRAL', Evil = 'EVIL', } const AlignmentType = builder.enumType(Alignment, { name: 'Alignment', }); ``` Passing the enum object in place of a name switches Pothos to this form, and `name` becomes required because the enum object carries no name of its own. The keys `Good`, `Neutral`, and `Evil` are the GraphQL enum values that appear in the schema and that clients use. The enum's runtime values `'GOOD'`, `'NEUTRAL'`, and `'EVIL'` are the backing values: a resolver for an `Alignment` field returns `Alignment.Evil`, graphql-js serializes it back to the name `Evil` in the response, and an incoming `Evil` argument reaches your resolver as `'EVIL'`. Clients never see the `'GOOD'`-style strings. The object form's per-value `value` option does the same thing for one value at a time. This form is useful when a codebase already has TypeScript enums you'd rather not maintain twice. For new code the array form is shorter and avoids the name-versus-value split, since there the name and the backing value are a single string. # Fields URL: /docs/fundamentals/fields Defining the fields that make up an object type, and where each field's value comes from. A type's fields are defined by the `fields` function you pass when you implement it. That function receives a field builder (conventionally named `t`) and returns a map whose values each come from one method call on `t`. ## Defining a field [#defining-a-field] Every field is created by a method on `t`. `t.field` is the base method the others are built on. It takes an options object; this example passes the field's `type` and a `resolve` function that produces the value: ```typescript playground example="fundamentals-fields" faction: t.field({ type: Faction, resolve: (character) => factions[character.factionId], }), ``` `faction` returns another object type, so its `type` is the `Faction` ref and the resolver looks the faction up by id. The `resolve` function's first argument is always the backing object; the [Resolvers](./resolvers) guide covers the rest of its signature and what a resolver may return. Every other field method on `t` is `t.field` with some of these options filled in for you. ## Scalar fields [#scalar-fields] The scalar builders (`t.string`, `t.int`, `t.float`, `t.boolean`, and `t.id`) are `t.field` with the `type` already set to the matching scalar, so you supply only the `resolve`: ```typescript age: t.int({ resolve: (character) => REFERENCE_YEAR - character.birthYear, }), ``` `age` computes an `Int` from the character's `birthYear`, so `t.int({ resolve })` produces the same field as `t.field({ type: 'Int', resolve })` would. The other scalar builders work the same way for their types. ## Exposing properties [#exposing-properties] When a field just returns a property already on the backing object, `t.expose*` writes that resolver for you: each helper is the matching scalar builder with a resolver that returns the named property, so you pass the property name instead of a `resolve`: ```typescript id: t.exposeID('id'), name: t.exposeString('name'), bio: t.exposeString('biography'), ``` `t.exposeID`, `t.exposeString`, `t.exposeInt`, `t.exposeFloat`, and `t.exposeBoolean` each take the name of a property to read and produce a field of the matching scalar type. `id` and `name` read the properties of the same name from the `Character` object; `bio` reads the `biography` property, since the property name and the field name don't have to match. Each helper also has a `…List` form (`t.exposeStringList` and so on) for a property that holds an array. ## Lists [#lists] A field's type can be a list by wrapping it in an array. In a resolver you return an array of the matching shape, the way the [`members`](#splitting-fields-across-files) field below returns a list of `Character` with `t.field({ type: [Character] })`. When the backing property already holds an array, the `…List` expose helpers forward it without a resolver: ```typescript titles: t.exposeStringList('titles'), ``` `titles` exposes the `string[]` property as a `[String]` field, the list counterpart of `t.exposeString`. ## Nullability [#nullability] Every field defined so far is nullable. Pothos output fields are nullable by default, so `t.exposeID('id')` produces `id: ID` and any resolver may return `null`. Pass `nullable: false` to require a value: ```typescript id: t.exposeID('id', { nullable: false }), ``` That makes the field `id: ID!`. A list field has two positions that can be null, the list itself and each of its items, so `nullable` also accepts a `{ list, items }` object to set them independently: ```typescript titles: t.exposeStringList('titles', { nullable: { list: false, items: true } }), ``` The default can also be flipped for the whole schema, so fields are non-nullable unless marked; [Default nullability](../patterns/default-nullability) covers how. ## Splitting fields across files [#splitting-fields-across-files] A type doesn't have to define all of its fields in one place. `builder.objectField` adds a single field to a type that's already been referenced, from anywhere in your codebase: ```typescript builder.objectField(Faction, 'members', (t) => t.field({ type: [Character], resolve: (faction) => characters.filter((character) => character.factionId === faction.id), }), ); ``` This defines `Faction.members` in the module that owns characters, instead of alongside the rest of `Faction`. `builder.objectFields` adds several fields at once. The same pairing exists elsewhere: `interfaceField` and `interfaceFields` for interfaces, and `queryField`, `mutationField`, and their plural forms for the root types. These calls can run before or after the type is implemented, and Pothos merges the definitions when the schema builds. [Project layout](../patterns/project-layout) covers organizing a schema this way. # Input objects URL: /docs/fundamentals/inputs Define reusable input types with builder.inputType and use them as argument and field types. An input object is a named type whose fields become the shape of an argument, so a field can take one structured value in place of several separate arguments. You define one with `builder.inputType` and use it anywhere an argument, or another input field, needs a type. ## Defining an input object [#defining-an-input-object] An input object is defined with `builder.inputType`, named the way object types are. It returns a reference, which you declare as a `const` and pass as an argument's `type`, just like an object ref: ```typescript playground example="fundamentals-inputs" const AddCharacterInput = builder.inputType('AddCharacterInput', { fields: (t) => ({ name: t.string({ required: true }), birthYear: t.int(), }), }); builder.mutationType({ fields: (t) => ({ addCharacter: t.field({ type: Character, args: { input: t.arg({ type: AddCharacterInput, required: true }), }, resolve: (_root, { input }) => addCharacter(input), }), }), }); ``` `AddCharacterInput` has two fields, `name` and `birthYear`, and the reference it returns is used as the `input` argument's `type`. Inside the resolver `input` arrives fully typed: `input.name` is a `string` and `input.birthYear` is `number | null | undefined`, following the `required` option on each field. (`addCharacter` is defined on the mutation root, which the [Mutations](./mutations) guide covers.) ## Input fields [#input-fields] An input object's fields are built much like the fields on an object type: a general `t.field({ type })` form, plus a scalar shorthand for each built-in scalar (`t.string`, `t.int`, `t.id`, `t.boolean`, `t.float`, and their `…List` forms). Input fields take the same core options as arguments, including `required` and `defaultValue`, which [Arguments](./args) covers: ```typescript fields: (t) => ({ name: t.string({ required: true }), alignment: t.string({ defaultValue: 'Neutral' }), }), ``` ## Sharing an input across fields [#sharing-an-input-across-fields] A named input object is a reference like any other, so two fields can share one by declaring it as a variable and passing it to each: ```typescript const FactionFilter = builder.inputType('FactionFilter', { fields: (t) => ({ nameContains: t.string(), minMembers: t.int(), }), }); builder.queryFields((t) => ({ factions: t.field({ type: [Faction], args: { filter: t.arg({ type: FactionFilter }) }, resolve: (_root, { filter }) => findFactions(filter), }), factionCount: t.int({ args: { filter: t.arg({ type: FactionFilter }) }, resolve: (_root, { filter }) => findFactions(filter).length, }), })); ``` `FactionFilter` is defined once and used as the `filter` argument on both `factions` and `factionCount`. ## Nested inputs [#nested-inputs] An input field's type can be another input object, so inputs nest: ```typescript const CharacterFilter = builder.inputType('CharacterFilter', { fields: (t) => ({ nameContains: t.string(), faction: t.field({ type: FactionFilter }), }), }); ``` `CharacterFilter` has a `faction` field whose type is the `FactionFilter` from above, declared with `t.field({ type: FactionFilter })` since only the built-in scalars have shorthands. The type can also be a list of an input object, wrapped in an array the same way a list argument is. ## Recursive inputs [#recursive-inputs] An input that refers to itself can't be created with a single `inputType` call: its fields would reference the variable that call is still producing, and TypeScript can't infer a type that contains itself. `builder.inputRef` splits the two steps. It creates the reference first, with the TypeScript shape supplied as a type argument, then `implement` adds the fields: ```typescript playground example="fundamentals-inputs" interface CharacterQueryInput { and?: CharacterQueryInput[]; nameContains?: string; bornAfter?: number; } const CharacterQuery = builder.inputRef('CharacterQuery'); CharacterQuery.implement({ fields: (t) => ({ and: t.field({ type: [CharacterQuery] }), nameContains: t.string(), bornAfter: t.int(), }), }); ``` `builder.inputRef` creates the reference for `CharacterQuery`, and `implement` fills in its fields afterward. The `and` field lists `CharacterQuery` itself, which resolves because the reference already exists by the time the `fields` function runs, and giving the shape up front is what lets TypeScript type a field that points at its own type. This mirrors `builder.objectRef(...).implement(...)` from [Object types](./objects). Input fields are nullable by default, and the shape you give `inputRef` is normalized to match: each optional property on `CharacterQueryInput` (`and?`, `nameContains?`, `bornAfter?`) is treated as `| null | undefined`, so writing plain `?:` is all you need and adding `| null` yourself is redundant. ## One-of inputs [#one-of-inputs] Setting `isOneOf: true` marks an input where the client provides exactly one of the fields, mapping to the `@oneOf` directive from the GraphQL specification. Pothos types the value as a discriminated union, so a resolver sees one field set and the rest as `never`: ```typescript builder.inputType('NameOrId', { isOneOf: true, fields: (t) => ({ name: t.string(), id: t.id() }), }); ``` # Interfaces URL: /docs/fundamentals/interfaces Define an interface, implement it on object types, and resolve the concrete type behind a value with resolveType or isTypeOf. ## Defining an interface [#defining-an-interface] ```typescript playground example="fundamentals-interfaces" interface ICharacterBase { id: string; name: string; } interface IHobbit extends ICharacterBase { kind: 'Hobbit'; shireAddress?: string; } interface IElf extends ICharacterBase { kind: 'Elf'; departed: boolean; } interface IWizard extends ICharacterBase { kind: 'Wizard'; order: string; color: string; } type ICharacter = IHobbit | IElf | IWizard; const Character = builder.interfaceRef('Character'); builder.interfaceType(Character, { description: 'A named being of Middle-earth.', resolveType: (val) => val.kind, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` `builder.interfaceRef('Character')` names the interface and states the backing model behind it, the way `objectRef` does for an object type (see [Object types](./objects)). Here the backing model is `ICharacter`, a union of the shapes that implement the interface; each of them extends `ICharacterBase`, which carries the shared `id` and `name`. `builder.interfaceType` defines the interface's fields with the same field builder object types use, so `t.exposeID('id')` and `t.exposeString('name')` read those two properties off the backing model, and every implementing type inherits them. ## Implementing the interface [#implementing-the-interface] ```typescript playground example="fundamentals-interfaces" const Hobbit = builder.objectRef('Hobbit'); Hobbit.implement({ interfaces: [Character], fields: (t) => ({ shireAddress: t.exposeString('shireAddress', { nullable: true }), }), }); const Elf = builder.objectRef('Elf'); Elf.implement({ interfaces: [Character], fields: (t) => ({ departed: t.exposeBoolean('departed'), }), }); ``` An object type joins the interface by listing it in the `interfaces` option: `Hobbit.implement({ interfaces: [Character], ... })`. `Hobbit` and `Elf` each declare only their own field (`shireAddress`, `departed`) and pick up `id` and `name` from `Character`, so a client can select those on either type without the object redeclaring them. Two separate checks stand behind `interfaces: [Character]`. Pothos checks, at the type level, that the object's backing model is assignable to the interface's: `IHobbit` and `IElf` are arms of the `ICharacter` union, so each is assignable to it, and the option type-checks. GraphQL's own rule, that an implementing type carries every interface field with compatible types, is enforced by graphql-js schema validation; since Pothos fills in the interface's fields for you, that mostly comes up when a type redefines one of them. An interface can implement another interface the same way, by passing `interfaces: [...]` to `builder.interfaceType`. ## Resolving the concrete type [#resolving-the-concrete-type] The response labels every value with its concrete type's name, so something has to decide which implementing type a value is. `resolveType` on the interface does this. When the backing model carries a discriminator it is a one-liner: `val.kind` is `'Hobbit' | 'Elf' | 'Wizard'`, and each string matches an implementing type's name: ```typescript resolveType: (val) => val.kind, ``` The returned string has to match the name of one of the implementing types. When the data has no discriminator to read, `resolveType` can tell the types apart by their shape instead: ```typescript resolveType: (val) => { if ('shireAddress' in val) return 'Hobbit'; if ('departed' in val) return 'Elf'; return 'Wizard'; }, ``` An implementing type can identify itself instead of the interface sorting all of them out. An object type defined from a [class](./objects#definition-styles) can set `isTypeOf` to an `instanceof` check: ```typescript class Hobbit { kind = 'Hobbit' as const; constructor( public id: string, public name: string, public shireAddress?: string, ) {} } builder.objectType(Hobbit, { interfaces: [Character], isTypeOf: (value) => value instanceof Hobbit, fields: (t) => ({ shireAddress: t.exposeString('shireAddress', { nullable: true }), }), }); ``` When the interface has no `resolveType`, graphql-js falls back to calling each implementing type's `isTypeOf` in turn until one returns true. If the interface defines no `resolveType` and its members define no `isTypeOf`, graphql-js has no way to pick a type and reports an error when a query reaches such a field. ## Querying [#querying] A field typed as the interface can return any mix of implementing types (the example schema's `characters` field returns a list of `Character`). A client selects the shared fields directly and reaches a concrete type's own fields through an inline fragment: ```graphql query Characters { characters { __typename id name ... on Hobbit { shireAddress } ... on Elf { departed } } } ``` Inline fragments and the `__typename` meta-field are part of GraphQL itself. `__typename` returns each value's concrete type name (the same name `resolveType` or an implementing type's `isTypeOf` resolved to), and `... on Hobbit` selects its fields only when the value is a `Hobbit`. # Mutations URL: /docs/fundamentals/mutations The Mutation root type, and fields that create, update, or delete data. Mutations are the schema's entry points for writing data: the fields a client calls to create, update, or delete records. `mutationType` defines the Mutation root, and `mutationField`/`mutationFields` add fields to it from anywhere in a schema. Here is a mutation that updates a character's biography: ```typescript playground example="fundamentals-mutations" builder.mutationType({ fields: (t) => ({ updateCharacter: t.field({ type: Character, args: { input: t.arg({ type: builder.inputType('UpdateCharacterInput', { fields: (t) => ({ characterId: t.id({ required: true }), biography: t.string({ required: true }), }), }), required: true, }), }, resolve: (_root, { input }, ctx) => { if (!ctx.user) { throw new Error('Not signed in'); } const entry = Characters.get(Number(input.characterId)); if (!entry) { throw new Error(`No character with id ${input.characterId}`); } if (entry.editorId !== ctx.user.id) { throw new Error("Only the entry's editor can edit it"); } entry.biography = input.biography; return entry; }, }), }), }); ``` The field takes a single `input` argument, an [input object](./inputs) that groups the mutation's values (`characterId` and `biography`) into one named type. Inside the resolver, `if (!ctx.user)` guards on a signed-in user (see [Context](./context#requiring-a-signed-in-user)). The check specific to a write comes next: `entry.editorId !== ctx.user.id` decides whether this particular signed-in user may change this particular record. With both checks passed, the resolver updates the record and returns it. For real authorization, [`plugin-scope-auth`](../plugins/scope-auth) moves checks like these onto the field as declarative auth scopes. ## The Mutation root [#the-mutation-root] `mutationType` defines the root, the way `queryType` defines the query root. Call it once, from the file that assembles your schema: ```typescript builder.mutationType({}); ``` In a schema split across modules, `mutationField` adds a single field to the root and `mutationFields` adds several, the same [cross-module merge](./queries#adding-fields-from-elsewhere) the query root allows: ```typescript // characters.ts — registers its write on the shared root builder.mutationField('updateCharacter', (t) => t.field({ type: Character, args: { input: t.arg({ type: UpdateCharacterInput, required: true }) }, resolve: (_root, { input }, ctx) => updateCharacter(input, ctx), }), ); ``` The root is named `Mutation` by default; pass `name` to `mutationType` to call it something else. A schema without writes can skip the mutation root entirely; of the three root types, only the query root is required by GraphQL's schema validation. When a client sends several mutation fields in one operation, the graphql-js executor runs them one at a time in the order written, each finishing before the next starts. (Query root fields resolve in parallel.) ## Returning what changed [#returning-what-changed] Return the entity the mutation changed. Because the field's type is the object type, the client selects fields on the result and reads the new state in the same round trip: ```graphql mutation UpdateCharacter { updateCharacter(input: { characterId: "1", biography: "Bearer of the One Ring and hero of the War of the Ring." }) { id biography } } ``` When a mutation has no single record to hand back, such as a bulk delete or a batch import, return a small payload type carrying whatever the client needs to know, like a count of the rows affected: `DeleteCharactersPayload { deletedCount: Int! }`. # Object types URL: /docs/fundamentals/objects Object types, the backing model behind each one, and the different ways to define them. ## Defining an object type [#defining-an-object-type] Object refs Classes Builder types ```typescript playground example="fundamentals-objects" interface ICharacter { id: string; name: string; birthYear?: string; biography?: string; editorId: string; } const Character = builder.objectRef('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` ```typescript playground example="fundamentals-objects-variant-classes" class Character { constructor( public id: string, public name: string, public editorId: string, public birthYear?: string, public biography?: string, ) {} } builder.objectType(Character, { name: 'Character', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` ```typescript playground example="fundamentals-objects-variant-builder-types" interface ICharacter { id: string; name: string; birthYear?: string; biography?: string; editorId: string; } const builder = new SchemaBuilder<{ Objects: { Character: ICharacter }; }>({}); builder.objectType('Character', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` Each tab defines the same `Character` object type. To define one we give it a name, a backing model (the TypeScript shape of the data behind the type), and the fields that make it up. In the `objectRef` form shown first, `builder.objectRef('Character')` creates a reference that carries the name and the backing model, and `.implement` supplies the fields. Each field maps data from the backing model to the GraphQL shape a client sees: `t.exposeID('id')` and `t.exposeString('name')` read those two properties straight off the backing model, and the [Fields](./fields) guide covers the field builder in detail. The **Classes** and **Builder types** tabs build the same type from a class and from a registered name, [covered below](#definition-styles). ## Type references [#type-references] `builder.objectRef` returns a type reference: a value that stands in for the `Character` type wherever the schema needs to name it. A reference is not particular to `objectRef`. `builder.objectType` returns one too, a class used to define a type acts as its own reference, and a type name registered on the builder (the **Builder types** tab) references the type by string. Any of these can be used as a field's `type`, which is how a field on one type returns another type. `builder.objectType` takes a reference in each of these forms as its first argument: a class, an existing object ref, or a string name registered on the builder's `Objects` generic. `implement` is the object-ref shorthand: calling `.implement(options)` on a ref is the same as passing that ref and the options to `builder.objectType`. ## The backing model [#the-backing-model] The generic on `objectRef` (or the class's instance type, or the shape registered under a name) is the type's backing model: the value your resolvers return for the type, and the `parent` Pothos hands to every field defined on it. Whatever form the reference takes, it carries this shape, and Pothos checks the type's fields against it. `t.exposeString('name')` compiles only when `name` is a string on `ICharacter`, and any field that returns `Character` has to resolve to a value matching it. The backing model and the GraphQL type are separate things. The backing model is whatever your resolvers work with (a database row, a plain object, a class instance, or even just a string id), while the GraphQL type is the set of fields a client can select. Exposing a property with `t.expose*` is the direct case, where a field reads a value straight off the backing model. Every other field is defined by writing a resolver, and as long as you can compute a field's value from the backing model (and the context), you can add it to the type without changing the backing model. A type backed by nothing but an id can still present a full set of fields, each resolver loading what it needs. The [Fields](./fields) guide covers computed fields and the field builder. ## Object type options [#object-type-options] `builder.objectRef('Character')` takes two things: the type's name and, as its generic, the backing model. Everything else about the type goes to `implement`: the `fields` callback, a `description`, the [interfaces](./interfaces) the type implements, and an `isTypeOf` function for [resolving abstract types](./interfaces). `fields` is the option you pass most often, but it works like any other option on `implement`. When a type doesn't need to be referenced before it's implemented, the two calls chain into a single expression: ```typescript playground example="fundamentals-objects" const Race = builder.objectRef('Race').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), lifespan: t.exposeString('lifespan'), }), }); ``` `builder.objectRef('Race').implement({ ... })` creates the reference and implements it in one statement. Keeping the two calls apart, as `Character` does above, is what lets two types reference each other: the reference exists before either type's fields are defined, so each can name the other. The [Circular references](../patterns/circular-references) pattern covers that case. ## Returning an object type [#returning-an-object-type] A type reference can be the `type` of any field, on any type in the schema. Here the root `Query` type returns a list of `Character`: ```typescript playground example="fundamentals-objects" builder.queryType({ fields: (t) => ({ characters: t.field({ type: [Character], resolve: () => characters, }), }), }); ``` Wrapping the reference in an array (`type: [Character]`) makes the field a list, while a bare `type: Character` returns a single one. Because the field returns `Character`, its resolver has to return values matching the backing model, and TypeScript reports an error if it doesn't. The [Queries](./queries) guide covers the `Query` root itself. ## Definition styles [#definition-styles] The three tabs at the top of the page define the same `Character` type in the three reference forms. All are fully supported, and a single schema can mix them freely. **Object refs.** `builder.objectRef('Character')` states the backing model as a generic and hands back a reference to pass around. **Classes.** `builder.objectType(Character, { ... })` defines the type from a class and uses the class's instance type as the backing model, so `parent` in every resolver is an instance of the class, methods and getters included. This fits well when your app already keeps classes for its data. Because a class is also a value at runtime, you can write `isTypeOf: (value) => value instanceof Character`, an `instanceof` check that identifies the type when an [interface or union](./interfaces) value is resolved (`isTypeOf` is always an option you set; it isn't derived from the class). **Builder types.** Registering a type on the builder's `Objects` generic maps a name to its backing model, so you can refer to the type by that string name (`t.field({ type: 'Character' })`) anywhere in the schema. This keeps your type definitions in one place instead of importing a reference into every file that uses them. Registering the name only tells TypeScript about the type; you still create it at runtime with `builder.objectType('Character', { ... })`. # Queries URL: /docs/fundamentals/queries The schema's Query root type and the fields that serve as a client's read entry points. ## Defining the Query root [#defining-the-query-root] The Query root is the object type whose fields are the entry points for every read a client can make. `builder.queryType()` defines it, and you can pass the root's fields straight in: ```typescript playground example="fundamentals-queries" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); const characters = ['Frodo', 'Samwise', 'Gandalf', 'Aragorn']; builder.queryType({ fields: (t) => ({ hello: t.string({ resolve: () => 'Welcome to the compendium', }), characterCount: t.int({ resolve: () => characters.length, }), }), }); export const schema = builder.toSchema(); ``` `queryType` takes an options object whose `fields` callback defines the root's fields; each entry becomes a field on the root, and `hello` and `characterCount` here return a `String` and an `Int` from their resolvers. The type is named `Query` by default (pass `name` in the options to call it something else). `builder.toSchema()` then builds the standard graphql-js schema. A GraphQL schema has to have a query type. If `queryType` is never called, the schema is built without a query root, and graphql-js rejects it when the schema is validated or executed. ## Adding fields from elsewhere [#adding-fields-from-elsewhere] Query fields can also be defined outside the `queryType` call, so each part of a schema can register its own entry points. `queryField` adds a single field to the root, and `queryFields` adds several at once: ```typescript builder.queryField('newestEntry', (t) => t.string({ resolve: () => 'The Battle of the Pelennor Fields', }), ); ``` ```typescript builder.queryFields((t) => ({ editorCount: t.int({ resolve: () => 3, }), compendiumTitle: t.string({ resolve: () => 'A Compendium of Middle-earth', }), })); ``` `queryField` is called with a field name and a callback that returns one field; `queryFields` gets a callback that returns a map of several. Both add their fields to the same `Query` root as `queryType`, and the fields from every call are merged together. The one constraint is that each field name can only be defined once; if two calls both define `characterCount`, the schema throws when it builds. These calls can run before or after `queryType`. When they run first, the fields wait until `queryType` defines the root and then attach to it. That is what lets a larger schema register its query fields across several modules and call `queryType` once from the file that assembles them: ```typescript // characters.ts import { builder } from './builder'; builder.queryField('characterCount', (t) => t.int({ resolve: () => 4 }), ); ``` ```typescript // index.ts import { builder } from './builder'; import './characters'; builder.queryType(); export const schema = builder.toSchema(); ``` `builder.queryType()` with no arguments defines the root without adding any fields of its own. See [Project layout](../patterns/project-layout) for the full multi-module setup. The mutation and subscription roots work the same way, through `mutationType`/`mutationField`/`mutationFields` and `subscriptionType`/`subscriptionField`/`subscriptionFields`. See [Mutations](./mutations) and [Subscriptions](./subscriptions) for those. # Resolvers URL: /docs/fundamentals/resolvers How resolver functions produce a field's value, and what they can return. A resolver is the function that produces a field's value. When a field appears in a query, the graphql-js executor calls that field's resolver and expects back a value matching the field's type. The `t.expose*` helpers from the [Fields](./fields) guide write a resolver for you that reads a property off the backing object; this guide is about writing the `resolve` function yourself. ```typescript playground example="fundamentals-resolvers" characterCount: t.int({ resolve: () => characters.length, }), ``` `characterCount` is an `Int` field whose resolver returns a number. The executor runs it each time the field is selected in a query, and the returned value becomes the field's value in the response. ## The four arguments [#the-four-arguments] The executor calls a resolver with four positional arguments, `(parent, args, context, info)`: ```typescript character: t.field({ type: Character, nullable: true, args: { id: t.arg.id({ required: true }) }, resolve: (parent, args) => characters.find((character) => character.id === args.id) ?? null, }), ``` * `parent` is the value the field's parent resolver returned, which is the backing model for that type (covered in [Object types](./objects)). On a field of the root `Query`, `Mutation`, or `Subscription` type there is no parent object, so `parent` is the `rootValue` the server passed to the executor, usually `undefined`. * `args` is the field's arguments, already coerced and type-checked against the field's `args` definition. The [Arguments](./args) guide covers declaring them. * `context` is the per-request [context](./context): the object your server builds for each request, where values like the authenticated `ctx.user` and database handles live. * `info` is information about the current query and field selection, which plugins like [Prisma](../plugins/prisma) and [Drizzle](../plugins/drizzle) read to plan a single database query for the fields a request selected. Most resolvers only need one or two of these, and since the parameters are positional you can leave off any trailing ones you don't use. ## What a resolver returns [#what-a-resolver-returns] A resolver returns a value matching the field's type, or a promise for one; the executor awaits the promise before continuing. A synchronous resolver returns its value directly, and an `async` resolver returns a promise for it. ```typescript featuredCharacter: t.field({ type: Character, nullable: true, resolve: async () => { const character = await loadCharacter('1'); return character ?? null; }, }), ``` The field's type decides what counts as a valid value, and Pothos checks it in TypeScript. A scalar field returns that scalar. A field returning an object type returns the [backing model](./objects) for that type rather than a GraphQL-shaped object, so you can hand back a database row or a plain object without reshaping it first. A list field accepts any iterable of those values, and a nullable field may also return `null` or `undefined`; `character` above returns `null` when no character matches the requested id. # Scalars URL: /docs/fundamentals/scalars Use built-in scalars and register custom ones for types like DateTime. GraphQL's five built-in scalars (`ID`, `String`, `Int`, `Float`, and `Boolean`) are registered for you, and you reach them through the scalar field builders (`t.int`, `t.id`, and the rest) and the `t.expose*` helpers the [Fields](./fields) guide covers. Most schemas need at least one scalar beyond those, and `DateTime` is the usual first one. A custom scalar has two parts: a declaration on the builder generic and a runtime implementation. ## Declaring a custom scalar [#declaring-a-custom-scalar] ```typescript playground example="fundamentals-scalars" const builder = new SchemaBuilder<{ Scalars: { DateTime: { Input: Date; Output: Date }; }; }>({}); builder.scalarType('DateTime', { serialize: (value) => value.toISOString(), parseValue: (value) => { if (typeof value !== 'string') { throw new Error('DateTime must be an ISO 8601 string'); } const date = new Date(value); if (Number.isNaN(date.getTime())) { throw new Error('Invalid DateTime'); } return date; }, }); ``` The `Scalars` entry on the builder generic maps the name `DateTime` to its Input and Output TypeScript types; this is a type-level declaration and adds no runtime behavior on its own (the [Schema builder](./schema-builder) guide covers the generic). `builder.scalarType('DateTime', { ... })` supplies that behavior, and because the name has to be one declared on `Scalars`, the implementation and the declared shapes are checked against each other. `serialize` and `parseValue` are the functions Pothos wires into a graphql-js scalar type for graphql-js to call as it executes a request: `serialize` turns the `Date` a resolver returns into the ISO string that goes on the wire, and `parseValue` turns an incoming value back into a `Date`. ## Input vs Output [#input-vs-output] A scalar's Input and Output types don't have to be the same. Output is the type your resolvers return, which `serialize` maps onto the wire; Input is the type they receive, which `parseValue` produces from the value the client sent. For `DateTime` both are `Date` (a resolver returns a `Date` and receives a `Date`), so the two halves are declared identically. They differ when the parsed value and the returned value aren't the same shape. The built-in `ID` is the clearest case: it declares `{ Input: string; Output: bigint | number | string }`. graphql-js coerces whatever the client sends (a string or an integer) to a string, so Input is `string`, while a resolver may return a `string`, a `number`, or a `bigint`, and graphql-js serializes any of them to a string on the wire. Splitting the two types lets each side keep the type it actually works with. ## Using the scalar [#using-the-scalar] Once the scalar is registered, its name works anywhere a type is expected, as a field's `type` or an argument's `type`: ```typescript const Battle = builder.objectRef('Battle').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), location: t.exposeString('location'), foughtOn: t.field({ type: 'DateTime', resolve: (b) => b.foughtOn }), }), }); builder.queryType({ fields: (t) => ({ battlesSince: t.field({ type: [Battle], args: { after: t.arg({ type: 'DateTime' }) }, resolve: (_root, { after }) => Battles.filter((b) => !after || b.foughtOn >= after), }), }), }); ``` `Battle.foughtOn` is a `DateTime` field, so its resolver returns a `Date` and graphql-js serializes it with the `serialize` above. `battlesSince` takes a `DateTime` argument; inside the resolver `after` is `Date | null | undefined`, because graphql-js has already parsed the string the client sent using the `parseValue` you supplied. The [Arguments](./args) guide covers argument definitions and [Queries](./queries) covers the `Query` root. ## graphql-scalars [#graphql-scalars] Writing `serialize` and `parseValue` by hand is fine for one or two scalars. For the common ones (`DateTime`, `EmailAddress`, `URL`, `JSON`, `UUID`, and dozens more), the [`graphql-scalars`](https://github.com/Urigo/graphql-scalars) package ships ready-made implementations, and `builder.addScalarType` registers one against a name you've declared on the generic: ```typescript import { DateTimeResolver } from 'graphql-scalars'; const builder = new SchemaBuilder<{ Scalars: { DateTime: { Input: Date; Output: Date }; }; }>({}); builder.addScalarType('DateTime', DateTimeResolver); ``` `addScalarType` takes an existing `GraphQLScalarType` and reuses its configuration, so you still declare the Input and Output types on the generic but don't write the coercion functions yourself. # SchemaBuilder URL: /docs/fundamentals/schema-builder The central builder every Pothos type is defined on, configured through its SchemaTypes generic and constructor options. Every Pothos schema starts with a `SchemaBuilder`, and every type in the schema is defined through it. The builder holds two kinds of configuration: a `SchemaTypes` generic that shapes what TypeScript infers, and a constructor options object that carries runtime settings like the list of plugins. ## Creating a builder [#creating-a-builder] ```typescript playground example="fundamentals-schema-builder" import SchemaBuilder from '@pothos/core'; interface Context { user?: { id: number; name: string }; } const builder = new SchemaBuilder<{ Context: Context; Scalars: { DateTime: { Input: Date; Output: Date }; }; }>({}); ``` The generic parameter is an object Pothos calls `SchemaTypes`. This one fills in two of its entries: `Context` declares the type of the context object resolvers receive as their third argument (the [Context](./context) guide covers where that object comes from), and `Scalars` declares the TypeScript types for a custom `DateTime` scalar (the [Scalars](./scalars) guide covers implementing it). The constructor takes an options object; with no plugins here it stays empty. You typically create one builder per schema and export it from a shared module, so every file that defines part of the schema imports the same builder. The [Project layout](../patterns/project-layout) pattern shows one way to organize this. ## The SchemaTypes generic [#the-schematypes-generic] The generic is type-level configuration: it changes what TypeScript accepts and infers, and nothing you write in it is passed to the constructor. Every entry is optional; you supply the ones you care about, and Pothos fills in defaults for the rest. Each entry is covered in depth on its own page: * `Context`: the shape of the per-request context object. Covered in [Context](./context). * `Scalars`: the Input/Output TypeScript types for each named scalar. The runtime implementation is registered later with `builder.scalarType`, covered in [Scalars](./scalars). * `DefaultFieldNullability` and `DefaultInputFieldRequiredness`: schema-wide defaults for whether output fields are nullable and input fields are required. Covered in [Default nullability](../patterns/default-nullability). * `Objects`, `Interfaces`, and `Inputs`: maps from type name to backing model (the TypeScript shape behind a type), which let you reference types by name as strings and keep your type definitions in one place. [Object types](./objects) covers this style alongside the others. * Plugin entries: each installed plugin can add its own entries (`AuthScopes` from scope-auth, `PrismaTypes` from prisma, and so on), documented on that plugin's page. Three entries also have a constructor counterpart: `Defaults` (covered in the [v4 migration guide](../migrations/v4)), `DefaultFieldNullability`, and `DefaultInputFieldRequiredness`. Setting one of these to a non-default value makes the matching constructor option (`defaults`, `defaultFieldNullability`, `defaultInputFieldRequiredness`) required, and its value has to match. None of the other core entries corresponds to a constructor option. The context value is created by your server for each request, and scalar implementations and named types are registered through builder methods. ## Constructor options [#constructor-options] The options object holds the runtime configuration. In core, the main option is `plugins`, the list of plugins the builder should use: ```typescript import SchemaBuilder from '@pothos/core'; import RelayPlugin from '@pothos/plugin-relay'; const builder = new SchemaBuilder<{ Context: Context; }>({ plugins: [RelayPlugin], }); ``` Each installed plugin can also add its own keys to the options object. Some are optional (`relay`), and some become required as soon as the plugin package is imported (`prisma` from `@pothos/plugin-prisma`). The [Using plugins](../using-plugins) guide covers installing and configuring plugins. ## Building the schema [#building-the-schema] ```typescript playground example="fundamentals-schema-builder" export const schema = builder.toSchema(); ``` `toSchema` resolves every type registered on the builder and returns a standard graphql-js `GraphQLSchema`, so anything that accepts a `GraphQLSchema` can serve it. Call it after all of your types have been defined, usually at the bottom of the module that imports every type definition. By default the returned schema is sorted lexicographically; pass `toSchema({ sortSchema: false })` to keep definition order instead. `toSchema` accepts a few other options (`directives`, `extensions`), and some plugins add their own build-time options here. # Subscriptions URL: /docs/fundamentals/subscriptions The Subscription root type, and fields that push a stream of values to clients over a long-lived connection. A subscription delivers a stream of values over a long-lived connection, where a query delivers a single response. `builder.subscriptionType` defines the schema's third root type to hold these fields, and `subscriptionField`/`subscriptionFields` register them from other modules. A subscription field is built from two functions: a `subscribe` that opens a stream of events, and a `resolve` that turns each event into the value the client receives. Here is a field that pushes a `Character` every time one is added: ```typescript playground example="fundamentals-subscriptions" builder.subscriptionType({ fields: (t) => ({ characterAdded: t.field({ type: Character, subscribe: (_root, _args, ctx) => ctx.pubSub.subscribe('CHARACTER_ADDED'), resolve: (character) => character, }), }), }); ``` `subscribe` receives the root value, the field's arguments, and context, and returns an async iterable of events. Here that is `ctx.pubSub.subscribe('CHARACTER_ADDED')`, the stream of characters published to that topic. When the subscription starts, graphql-js drives the iteration, running `resolve` once for each value the stream yields. So `resolve`'s first argument is a single emitted event, typed as whatever `subscribe`'s iterable produces: here already a `Character`, so `resolve: (character) => character` passes it through. It could instead project the event into any shape the field's type allows. Opening this in the playground builds the schema and checks the operation, but a browser runs a single operation rather than holding an event stream, so nothing streams there. Serve the schema with a GraphQL server (see [First server](../getting-started/first-server)) to watch `characterAdded` push each new entry. ## Publishing events [#publishing-events] The events a subscription streams come from somewhere, usually a mutation that publishes to the same topic as it writes: ```typescript builder.mutationType({ fields: (t) => ({ addCharacter: t.field({ type: Character, args: { name: t.arg.string({ required: true }) }, resolve: (_root, { name }, ctx) => { if (!ctx.user) { throw new Error('Sign in to add a character'); } const character: ICharacter = { id: Characters.size + 1, name, biography: '', editorId: ctx.user.id, }; Characters.set(character.id, character); ctx.pubSub.publish('CHARACTER_ADDED', character); return character; }, }), }), }); ``` `addCharacter` creates the record, publishes it to the `CHARACTER_ADDED` topic with `ctx.pubSub.publish`, and returns the created `Character`. Every open `characterAdded` subscription then receives that character as its next event. ## Wiring the pub/sub [#wiring-the-pubsub] The pub/sub itself is not part of Pothos; it lives on context, the way any per-request handle does. [`graphql-yoga`](https://the-guild.dev/graphql/yoga-server) ships `createPubSub` for the in-memory case, so the server puts one on context alongside the user and database client: ```typescript import { createPubSub, createYoga } from 'graphql-yoga'; const pubSub = createPubSub(); const yoga = createYoga({ schema, context: () => ({ pubSub }), }); ``` Declaring `pubSub` on the builder's `Context` type is what makes `ctx.pubSub` typed in every resolver, like the rest of [context](./context). ## Filtering [#filtering] A subscriber often wants only part of a stream. Since the field is nullable, `resolve` can return `null` for events that don't apply. This field streams additions but surfaces only the ones made by a given editor: ```typescript builder.subscriptionField('characterAddedByEditor', (t) => t.field({ type: Character, nullable: true, args: { editorId: t.arg.id({ required: true }) }, subscribe: (_root, _args, ctx) => ctx.pubSub.subscribe('CHARACTER_ADDED'), resolve: (character, { editorId }) => character.editorId === Number(editorId) ? character : null, }), ); ``` The server still delivers a message for every event; returning `null` leaves this subscriber's copy empty. When most events on a topic are uninteresting, filter upstream instead: subscribe to a narrower topic, or pass a key to your pub/sub, so the stream never carries them. That mechanism belongs to the pub/sub you wired. ## Authorization [#authorization] Check authorization in `subscribe`, where the stream opens, and trust the result for the connection's lifetime: ```typescript subscribe: (_root, _args, ctx) => { if (!ctx.user) { throw new Error('Sign in to watch the feed'); } return ctx.pubSub.subscribe('CHARACTER_ADDED'); }, ``` The `ctx.user` guard is covered in [Context](./context#requiring-a-signed-in-user). Checks that depend on the individual event go in `resolve` instead. [`plugin-smart-subscriptions`](../plugins/smart-subscriptions) offers a different model, where fields register subscriptions that re-run a query as the underlying data changes. # Unions URL: /docs/fundamentals/unions Define a union of object types with unionType, use it as a field type, and resolve which member each value is. ## Defining a union [#defining-a-union] A union is a single type that stands for one of several object types. A search field is the usual case: a query can return a character, a location, or a quote, and those types share no fields. `builder.unionType` names the union and lists its members: ```typescript playground example="fundamentals-unions" const SearchResult = builder.unionType('SearchResult', { types: [Character, Location, Quote], resolveType: (val) => val.kind, }); ``` `unionType` takes the union's name and an options object. `types` lists the members, here `Character`, `Location`, and `Quote`. Every member has to be an object type; interfaces, scalars, and other unions can't be listed. (`types` can also be a function returning the array, for members defined later in the file.) A union has no fields of its own, so there is no `fields` callback. `resolveType` tells the server which member a given value is. Each value in this schema carries a `kind` discriminator, so `resolveType` returns it directly, the same contract [interfaces](./interfaces) use. The union-specific point is that the name you return has to be one of the types in the union's `types` list. If you leave `resolveType` off, graphql-js falls back to each member type's `isTypeOf` function; when neither is defined, it has no way to tell the members apart and raises an error while executing the field. ## Using a union as a field type [#using-a-union-as-a-field-type] A union reference is used as a field's `type` like any object type: ```typescript playground example="fundamentals-unions" builder.queryType({ fields: (t) => ({ search: t.field({ type: [SearchResult], args: { term: t.arg.string({ required: true }) }, resolve: (_root, { term }) => { const needle = term.toLowerCase(); return Index.filter((hit) => { if (hit.kind === 'Quote') return hit.text.toLowerCase().includes(needle); return hit.name.toLowerCase().includes(needle); }); }, }), }), }); ``` `type: [SearchResult]` makes `search` return a list of union values. The resolver returns whatever mix of characters, locations, and quotes matches the term, and `resolveType` sorts out which member each one is as the field resolves. ## Unions versus interfaces [#unions-versus-interfaces] Both unions and interfaces let one field return more than one object type. The difference is whether the types share fields. An [interface](./interfaces) is a set of fields every implementer carries, like an `id` and a `name`, so a client can select those fields on the abstract type directly. A union groups types that have nothing in common, so a client works entirely through the individual members. The [`plugin-errors`](../plugins/errors) plugin builds on unions for another common case, where a mutation returns either its result or one of several error types. ## Querying a union [#querying-a-union] Because a union has no fields of its own, a client selects fields inside a fragment on each member type: ```graphql query Search { search(term: "frodo") { __typename ... on Character { id name } ... on Location { name terrain } ... on Quote { text } } } ``` `__typename` returns the resolved member's name, which clients use to tell the members apart. The [Interfaces](./interfaces) guide covers these polymorphic selections (inline fragments and `__typename`) in more detail. # First server URL: /docs/getting-started/first-server Serve a Pothos schema over HTTP with graphql-yoga and run a query against it. With a schema built, the next step is to put it behind an HTTP endpoint. These guides use [`graphql-yoga`](https://the-guild.dev/graphql/yoga-server), but the `GraphQLSchema` returned by `toSchema()` should be compatible with any other GraphQL server implementation. ## Install yoga [#install-yoga] npm pnpm yarn bun ```bash npm install graphql-yoga ``` ```bash pnpm add graphql-yoga ``` ```bash yarn add graphql-yoga ``` ```bash bun add graphql-yoga ``` ## The schema [#the-schema] Here's a small schema with a single `hello` field: ```typescript playground example="getting-started-first-server-step-1" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); builder.queryType({ fields: (t) => ({ hello: t.string({ args: { name: t.arg.string() }, resolve: (_root, { name }) => `Hello, ${name ?? 'friend'}.`, }), }), }); export const schema = builder.toSchema(); ``` Save it as `schema.ts`; it exports `schema`, which is all the server imports. The `hello` field takes an optional `name` argument declared with `t.arg.string()` (the [Arguments](../fundamentals/args) guide covers arguments). ## The server [#the-server] `createYoga` takes the schema and returns a request handler you can pass straight to Node's built-in HTTP server: ```typescript playground example="getting-started-first-server-step-2" import { createServer } from 'node:http'; import { createYoga } from 'graphql-yoga'; import { schema } from './schema'; const yoga = createYoga({ schema, context: () => ({}), }); const server = createServer(yoga); server.listen(4000, () => { console.log('Ready at http://localhost:4000/graphql'); }); ``` Run it with `npx tsx server.ts` (or compile with `tsc` and run the output). yoga serves the API at `/graphql`, and by default it also serves the GraphiQL explorer at that same URL — open `http://localhost:4000/graphql` in a browser to load it. The port is whatever you pass to `server.listen`. ## Running a query [#running-a-query] In GraphiQL, run: ```graphql query Greet { hello(name: "Frodo") } ``` yoga responds with: ```json { "data": { "hello": "Hello, Frodo." } } ``` ## The context factory [#the-context-factory] The `context` option is a function that runs while yoga handles a request; whatever it returns becomes the `ctx` value every resolver can read. The server above returns `{}`, so `ctx` is empty. In a real app the factory reads the incoming request and returns what resolvers need, such as the signed-in user and a database client: ```typescript import { initContextCache } from '@pothos/core'; const yoga = createYoga({ schema, context: async ({ request }) => ({ ...initContextCache(), user: await getUser(request.headers.get('authorization')), db, }), }); ``` Spreading `initContextCache()` into the returned object sets up the per-request cache some plugins rely on; including it in every context factory keeps those plugins working. Declaring a matching `Context` type on the builder makes `ctx` fully typed in every resolver. The [Context](../fundamentals/context) guide covers what belongs on context and how to wire the types. # Installation URL: /docs/getting-started/installation Install Pothos and create a small schema you can run. This page walks through installing Pothos and creating a small schema you can run. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/core graphql ``` ```bash pnpm add @pothos/core graphql ``` ```bash yarn add @pothos/core graphql ``` ```bash bun add @pothos/core graphql ``` Plugins are published as separate packages (`@pothos/plugin-relay`, `@pothos/plugin-scope-auth`, etc.). `graphql` is a peer dependency, shared between Pothos and your GraphQL server. ## TypeScript settings [#typescript-settings] Pothos is built around type safety, and turning off strict null checks breaks some of its inference. Make sure `strict` mode is enabled in your `tsconfig.json`: ```json { "compilerOptions": { "strict": true } } ``` ## Hello world [#hello-world] A builder and a `Query` type with one field is enough to create a working schema: ```typescript playground example="getting-started-first-schema-step-1" import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); builder.queryType({ fields: (t) => ({ health: t.string({ resolve: () => 'ok' }), }), }); export const schema = builder.toSchema(); ``` `new SchemaBuilder({})` creates the builder used to define the rest of the schema. The options object is where plugins and their settings will go later. `builder.queryType()` defines the schema's root `Query` type (the root types are covered in more detail in the [Queries](../fundamentals/queries) guide), and `builder.toSchema()` builds a standard graphql-js `GraphQLSchema` that can be passed to any GraphQL server. ## Adding an object type [#adding-an-object-type] Next, we can add an object type based on some data: ```typescript playground example="getting-started-first-schema-step-2" const builder = new SchemaBuilder({}); const Character = builder.objectRef<{ id: string; name: string }>('Character'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.queryType({ fields: (t) => ({ frodo: t.field({ type: Character, resolve: () => ({ id: '1', name: 'Frodo Baggins' }), }), }), }); ``` `objectRef` creates a reference to a new `Character` type. The generic parameter tells Pothos what TypeScript shape the data behind this type will have (Pothos calls this the backing model). `implement` defines the type's fields; `t.exposeID('id')` and `t.exposeString('name')` return properties directly from the backing object. The [Fields](../fundamentals/fields) guide covers the field builder in more detail. Because the `frodo` field returns a `Character`, its resolver needs to return an object matching that shape, and TypeScript will report an error if it doesn't. If you're new to GraphQL itself, [graphql.org/learn](https://graphql.org/learn/) covers the query language and type system these docs assume. # Introduction URL: /docs/getting-started/introduction Build type-safe GraphQL schemas in plain TypeScript. Pothos is a library for building GraphQL schemas in TypeScript. You define objects, fields, resolvers, and inputs on a builder as ordinary values, and `builder.toSchema()` turns them into a standard graphql-js `GraphQLSchema` your server runs. The types in your schema come from your own data. You start from a type you already have (a database row, an API response, a domain type) and build the GraphQL type on top of it. The builder keeps track of these types wherever they are used, so the schema stays type-safe without a separate SDL file or a code-generation step. ## Building from your data [#building-from-your-data] Most types start with `builder.objectRef()`. The generic `T` is the **backing model**, the TypeScript shape your resolvers return and that Pothos hands back as `parent`: ```typescript playground import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); // The data you already have interface ICharacter { id: string; name: string; } const Character = builder.objectRef('Character').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // parent is an ICharacter, so this resolver is typed for free firstName: t.string({ resolve: (parent) => parent.name.split(' ')[0] }), }), }); builder.queryType({ fields: (t) => ({ frodo: t.field({ type: Character, resolve: () => ({ id: '1', name: 'Frodo Baggins' }), }), }), }); export const schema = builder.toSchema(); ``` Because `Character` was created with the `ICharacter` type, resolvers on it receive an `ICharacter` as `parent`, and the `frodo` resolver has to return one. `ICharacter` is the data behind the type and `Character` is what clients query; Pothos keeps them in sync as your code changes. These docs use objectRef as their default. Pothos also supports class-backed types and types registered by name on the builder's generics; [Object types](../fundamentals/objects) covers all three. ## How it compares to schema-first [#how-it-compares-to-schema-first] Schema-first tools (GraphQL Tools, Mercurius, raw `graphql-js`) start from an SDL string, and use code generation to produce TypeScript types for the resolvers that implement it. With Pothos the schema is defined in TypeScript, and the SDL is generated from it by `toSchema()`. Because there is only one definition of the schema, the types and the SDL can't drift apart. ## Where plugins fit [#where-plugins-fit] Core gives you objects, fields, inputs, interfaces, unions, enums, and scalars, enough to build a complete schema. Its only peer dependency is `graphql`. Everything else is a plugin that adds methods to the same builder: * **[Relay](../plugins/relay)**: connections, nodes, and global IDs following the Relay spec. * **[Scope auth](../plugins/scope-auth)**: declarative authorization checks on fields and types. * **[Errors](../plugins/errors)**: model expected failures as part of the schema instead of throwing. * **[Validation](../plugins/validation)**: validate arguments and inputs with Zod or Valibot. * **[Dataloader](../plugins/dataloader)**: batch and cache loads to avoid N+1 queries. * **[Prisma](../plugins/prisma) / [Drizzle](../plugins/drizzle)**: define objects straight from your ORM models. * **[Federation](../plugins/federation)**: build subgraphs for a federated gateway. Plugin methods carry the same type safety as the built-in ones, and plugins are designed to be combined, so you add them as you need them. ## What's next [#whats-next] [Installation](./installation) and [First server](./first-server) go from an empty project to a running endpoint. # GiraphQL to Pothos URL: /docs/migrations/giraphql-pothos Migration guide for upgrading from GiraphQL 2.* to Pothos 3.0 As of 3.0 GiraphQL has been renamed to Pothos. The primary motivation for this rename is to make this library and associated projects, guides, and other content to be more discoverable. GiraphQL is not visually distinct from GraphQL, and has often been interpreted as a typo. Search engines tend to auto-correct the name to GraphQL, making it hard to search for. ## Changes for consumers of GiraphQL [#changes-for-consumers-of-giraphql] * All packages have been moved from the `@giraphql/*` scope to `@pothos/*` scope. * The `GiraphQLSchemaTypes` global typescript scope has been renamed to `PothosSchemaTypes` * Exported types prefixed with `GiraphQL` have had that prefix replaced with `Pothos` For the most part, the easiest way to upgrade is by doing a CASE SENSITIVE search and replace of `giraphql` -> `pothos` and `GiraphQL` -> `Pothos`. The only non-documentation change between the latest version of GiraphQL and the initial version of Pothos (`v3.0.0`) are renaming of types and packages. ## Plugin specific changes [#plugin-specific-changes] ### Prisma plugin [#prisma-plugin] * The generator/provider for prisma types has been renamed to `prisma-pothos-types`. You will need to update your prisma schema to use the new provider: ```prisma generator pothos { provider = "prisma-pothos-types" } ``` ## For plugin authors [#for-plugin-authors] * Some `extensions` fields in the build schemas have been renamed. Specifically: * `giraphQLOptions` has been renamed to `pothosOptions` * `giraphQLConfig` has been renamed to `pothosConfig` # Migrations URL: /docs/migrations List of Pothos migration guides Each major Pothos release has a migration guide covering its breaking changes and the options you can set to keep the previous behavior. Pick the guide for the version you are upgrading from: * [3.\* to (4.0)](./v4) * [GiraphQL (2.\*) to Pothos (3.0)](./giraphql-pothos) * [1.\* to 2.0](./v2) ## Migrating to Pothos from other GraphQL libraries [#migrating-to-pothos-from-other-graphql-libraries] Official migration tooling is still in progress. For now, a few third-party tools can help with incremental migration from common setups. * [Nexus to Pothos codemod](https://github.com/villesau/nexus-to-pothos-codemod) This third-party codemod transforms nexus types, queries, and mutations into their Pothos equivalents. It still needs some manual adjustments to get everything working correctly, but it does much of the mechanical work. * [Pothos Generator](https://github.com/hayes/pothos/tree/main/packages/converter) This is an undocumented CLI that converts a schema into valid Pothos code. Resolvers are all placeholders that throw, so it is most useful for generating input types rather than a complete migration. # v2.0 URL: /docs/migrations/v2 Migration guide for upgrading from GiraphQL 1.* to GiraphQL 2.0 The 2.0 release was mostly focused around re-designing the plugin system so it could be properly documented, and made available for broader adoption. The previous plugin system allowed plugins to use the FieldWrapper base class to wrap fields. Unfortunately the overhead of this wrapping strategy was significantly higher than expected, and could not be optimized in a way that justified the conveniences it provided. ## Breaking changes [#breaking-changes] ### Auth plugin [#auth-plugin] The auth plugin has been replaced by a new `scope-auth` plugin. Unfortunately due to the performance problems with the original field wrapping API, the auth plugin had to be re-designed, and maintaining the existing API at the cost of significant performance overhead did not seem justified. Any existing usage of the `auth` plugin will need to be replaced with the new `scope-auth` plugin. The API of the new `scope-auth` plugin is substantially different, and the specifics of the migration will depend on the exact usage of the original auth plugin. Documentation on the new plugin can be found [here](../plugins/scope-auth). ### Plugin names [#plugin-names] Plugin names have been normalized, and are now exported as the default export of the plugin packages. Change: ```typescript // old import '@pothos/plugin-simple-objects'; const builder = new SchemaBuilder({ plugins: ['PothosSimpleObjects'], }); // new import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'; const builder = new SchemaBuilder({ plugins: [SimpleObjectsPlugin], }); ``` ### Plugin order [#plugin-order] The old plugin API did not make strong guarantees about the order in which plugin hooks would be executed. Plugins are now always triggered in reverse order. The most critical plugins (like `auth-scope`) should appear first in the list of plugins. This ensures that any modifications made by other plugins are applied first, and lets the more important plugins be at the top of the call stack when resolving fields. ### InputFieldBuilder.bool and InputFieldBuilder.boolList [#inputfieldbuilderbool-and-inputfieldbuilderboollist] The `bool` alias on `InputFieldBuilder` has been removed, as it was inconsistent with the other field builders and general naming convention of other methods. Usage of this method should be converted to the canonical `boolean` and `booleanList` methods. Change: ```typescript // Old t.arg.bool({}); t.arg.boolList({}); // New t.arg.boolean(); t.arg.booleanList(); ``` ### args on "exposed" fields [#args-on-exposed-fields] Fields defined with the `expose` helpers no longer accept `args` since they also do not have a resolver. ### Plugin API [#plugin-api] The Plugin API has been completely re-designed and is now [documented here](../plugins/writing-plugins). new instances of plugins are now instantiated each time `toSchema` is called on the `SchemaBuilder`, rather than being tied to the lifetime of the `SchemaBuilder` itself. ## New features [#new-features] * Lots of new documentation * New scope-auth plugin * New directives plugin * New plugin API * Significant performance improvements in smart-subscriptions and scope-auth plugins # v4.0 URL: /docs/migrations/v4 Migration guide for upgrading from Pothos 3.x to Pothos 4.0 ## Overview [#overview] Migrating from Pothos 3.x to 4.0 The `4.0` release of Pothos is largely focused on updating 4 things: 1. Improving outdated defaults to be more consistent and aligned with best practices 2. Updating naming of some config options to be more consistent 3. Updating minimum versions of peer dependencies 4. Updating internal types to support some previously challenging plugin patterns While the internals of Pothos have almost entirely been re-written, the public API surface should have minimal changes for most users. The first 2 sets of changes will cover the majority of changes relevant to the majority of applications. To make the upgrade as simple as possible, some options were added to maintain the defaults and option names from `3.x` which are described in the simple upgrade section below. ## New minimum versions [#new-minimum-versions] * `typescript`: `5.0.2` * `graphql`: `16.6.0` * `node`: `18.0` ## Simple upgrade (restore 3.0 options and defaults) [#simple-upgrade-restore-30-options-and-defaults] You can restore the 3.x defaults by adding the Defaults versions to both the SchemaTypes and the builder options: ```ts const builder = new SchemaBuilder<{ Defaults: 'v3'; }>({ defaults: 'v3', }); ``` This will restore all the defaults and config options from previous Pothos versions for both core and plugins. If you are using `@pothos/plugin-validation`, it has been renamed to `@pothos/plugin-zod`, and a new validation plugin will be released in the future. ```diff - import ValidationPlugin from '@pothos/plugin-validation'; + import ZodPlugin from '@pothos/plugin-zod'; const builder = new SchemaBuilder({ - plugins: [ValidationPlugin], + plugins: [ZodPlugin], }); ``` ## Manual update [#manual-update] There are a number of new defaults and changes to options for various plugins. To fully upgrade to 4.0 see the full list of breaking changes below: ## Breaking API changes [#breaking-api-changes] This section covers breaking API changes that can be automatically reverted by using the Simple Upgrade process described above. Changes to types and classes outside the main Pothos API are described in the next section. Those changes will primarily affect other plugins and tools written for Pothos, but may be relevant to some type helpers you have created. ## `@pothos/core` [#pothoscore] ### Default field nullability [#default-field-nullability] In previous versions of Pothos, fields were non-nullable by default. This is inconsistent with the rest of the GraphQL ecosystem, so the default is being changed to make fields nullable by default. To restore the previous behavior you can set the `defaultFieldNullability` option when creating your builder: ```ts export const builder = new SchemaBuilder<{ DefaultFieldNullability: false; }>({ defaultFieldNullability: false, }); ``` Alternatively, fields can be updated to add `nullable: false` to the fields options. ### Default ID scalar types [#default-id-scalar-types] The default types for the built in `ID` Scalar has been changed to more closely match the behavior of JavaScript GraphQL server implementations: ```ts interface IDType { Input: string; Output: number | string | bigint; } ``` This will make working with IDs in arguments and input types easier by avoiding unnecessary type checks to see if an `ID` is a `number` or `string`. When returning an `ID` from a scalar you will be able to return a `string`, `number`, or `bigint`. To restore the previous defaults you can customize the `ID` scalar types when creating your builder: ```ts const builder = new SchemaBuilder<{ Scalars: { ID: { Input: number | string; Output: number | string; }; }; }>({}); ``` ## `@pothos/plugin-relay` [#pothosplugin-relay] ### Renamed options [#renamed-options] The base relay plugin options have moved from `relayOptions` to `relay` to be more consistent with options for other plugins. ```diff const builder = new SchemaBuilder<{}>({ - relayOptions: {...} + relay: {...} }) ``` ### New defaults [#new-defaults] A number of the default values for relay options have changed: * `clientMutationId`: Now defaults to `"omit"` and was previously `"required"` * `clientMutationId` was only required in early versions of the relay client, and is no longer recommended. * `cursorType`: Now defaults to `"String"` and was previously `"ID"` * The previous defaults were inconsistent about the type of a cursor. Cursors generally should not be treated as IDs as they are meant to indicate a position in a list, and may contain information specific to other filters or arguments applied to the connection. * `brandLoadedObjects`: Now defaults to `true` and was previously `false` * This change will improve developer experience for most node implementations, as it removes the need for `isTypeOf` to be defined for most nodes. * `edgesFieldOptions.nullable`: Now defaults to `{ list: options.defaultFieldNullability, items: true }` and was previously `{ list: false, items: true }` * `nodeFieldOptions.nullable`: Now defaults to `options.defaultFieldNullability` and was previously `false` * This new default is intended to align with the relay connection spec, which does not expect connections to be NonNullable by default To restore the previous defaults you can pass the old values when setting up the builder: ```ts const builder = new SchemaBuilder<{ // To change edgesFieldOptions.nullable you must also update the type here DefaultEdgesNullability: { list: false; items: true }; }>({ relay: { clientMutationId: 'required', brandLoadedObjects: false, edgesFieldOptions: { nullable: { list: false, items: true }, }, nodeFieldOptions: { nullable: false, }, cursorType: 'ID', // the cursor fields on edges and pageInfo previously defaulted to `String` // but will be overwritten by `cursorType` so you also need to explicitly set them edgeCursorType: 'String', pageInfoCursorType: 'String', // If you are using the new v4 nullability defaults, you may need to change the nullability of mutation fields relayMutationFieldOptions: { nullable: false, }, }, }); ``` ## `@pothos/plugin-prisma` [#pothosplugin-prisma] ### Nullable relations [#nullable-relations] Previously the prisma would allow t.relation to define non-nullable fields using nullable relations. The plugin option now requires an `onNull` option to handle null relations on NonNullable fields To restore the previous behavior you can set the `onNull` option to `'error'`, which will result in a runtime error when the field returns null ```diff t.relation('nullableRelation', { + onNull: 'error', }) ``` Alternatively you can mark the field as nullable: ```diff t.relation('nullableRelation', { + nullable: true, }) ``` `onNull` can also be set to a function that returns either a record matching the type of the relation, or a custom Error to throw when the relation is null. ```ts t.relation('nullableRelation', { onNull: () => loadPlaceholder(), }); ``` ## `@pothos/plugin-directives` [#pothosplugin-directives] `useGraphQLToolsUnorderedDirectives` has been nested inside a `directives` options object: ```diff const builder = new SchemaBuilder<{}>({ - useGraphQLToolsUnorderedDirectives: true + directives: { + useGraphQLToolsUnorderedDirectives: true + } }) ``` ## `@pothos/plugin-errors` [#pothosplugin-errors] ### Renamed options [#renamed-options-1] The base error plugin options have moved from `errorOptions` to `errors` to be more consistent with options for other plugins. ```diff const builder = new SchemaBuilder<{}>({ - errorOptions: {...} + errors: {...} }) ``` ## `@pothos/plugin-scope-auth` [#pothosplugin-scope-auth] ### Renamed options [#renamed-options-2] The base scope-auth plugin options have moved from `scopeAuthOptions` to `scopeAuth` to be more consistent with options for other plugins. The `authScopes` option has been moved to `scopeAuth.authScopes` to keep all options for the plugin in one options object. ```diff const builder = new SchemaBuilder<{}>({ - scopeAuthOptions: {...} - authScopes: (ctx) => ({...}) + scopeAuth: { + ...otherOptions, + authScopes: (ctx) => ({...}) + } }) ``` ## `@pothos/plugin-zod` (previously `@pothos/plugin-validation`) [#pothosplugin-zod-previously-pothosplugin-validation] ### Renamed options [#renamed-options-3] The base validation plugin options have moved from `validationOptions` to `zod` to be more consistent with options for other plugins. ```diff const builder = new SchemaBuilder<{}>({ - validationOptions: {...} + zod: {...} }) ``` ## `@pothos/plugin-authz` has been removed [#pothosplugin-authz-has-been-removed] The `@pothos/plugin-authz` plugin has been removed, because the underlying `@graphql-authz/core` is not actively maintained, and has left critical security vulnerabilities unaddressed. ## Plugin API and type changes [#plugin-api-and-type-changes] Unlike the defaults and config changes, the changes to the types and classes used throughout Pothos can't easily be made backward-compatible with the 3.x releases. Below is a summary of the main changes made to the types and classes that may be used by plugins, helpers, or other libraries. Many of these types and classes are primarily intended for internal use, and should not affect most applications using Pothos, but the changes are documented here to help upgrades for those of you building your own plugins, or using these types in your applications. The 4.0 release is intended to allow Pothos to become more modular and extensible. This requires Refs and many associated type helpers to propagate the SchemaTypes from the builder that originated them, meaning most of the changes listed below are adding `Types extends SchemaTypes` as the first generic argument to the type. ## Classes [#classes] * `InputFieldBuilder` * Removed the `typename` argument from the constructor * Updated field methods to return a new `GenericInputRef` * `InterfaceFieldBuilder` * Removed the `typename` argument from the constructor * `ObjectFieldBuilder` * Removed the `typename` argument from the constructor * `BaseTypeRef` * Added `SchemaTypes` as a new Generic parameter * `EnumTypeRef` * Added `SchemaTypes` as a new Generic parameter * `InputObjectRef` * Added `SchemaTypes` as a new Generic parameter * `InputRef` * Added `SchemaTypes` as a new Generic parameter * `OutputTypeRef` * Added `SchemaTypes` as a new Generic parameter * `ListRef` * Added `SchemaTypes` as a new Generic parameter * `InterfaceRef` * Added `SchemaTypes` as a new Generic parameter * `ObjectRef` * Added `SchemaTypes` as a new Generic parameter * `ScalarRef` * Added `SchemaTypes` as a new Generic parameter * `UnionRef` * Added `SchemaTypes` as a new Generic parameter * `FieldRef` * Added `SchemaTypes` as a new Generic parameter * removed the typename from constructor args * add the builder and Field options as arguments for the constructor * `InputFieldRef` * Added `SchemaTypes` as a new Generic parameter * removed the typename and kind from constructor args * add the builder and Field options as arguments for the constructor * split argument refs into a new `ArgumentRef` class ## Exported types [#exported-types] * `*FieldThunk` * Updated to return a `GenericFieldRef` * `FieldMap` * Updated to `Record>;` * `InputFieldMap` * Updated to `Record>;` * `InputFieldsFromShape` * Added `SchemaTypes` as a new Generic parameter * `InputShapeFromField` * Updated to accept a `GenericFieldRef` ## Field options [#field-options] The global interfaces for FieldOptions no longer include the `resolve` option, which has moved to the `InferredFieldOptions` interface to allow plugins to replace or change the resolve functions types globally. This means that when extending the `FieldOptionsByKind` interface, if you previously extended one of the built in Field option interfaces, you will need to update your types to include the `resolve` function types as well: ```diff export interface FieldOptionsByKind< Types extends SchemaTypes, ParentShape, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveShape, ResolveReturnShape, > { - CustomObjectObject: CustomOptions & - PothosSchemaTypes.ObjectFieldOptions< - Types, - ParentShape, - Type, - Nullable, - Args, - ResolveReturnShape - >; + CustomObjectObject: CustomOptions & + PothosSchemaTypes.ObjectFieldOptions< + Types, + ParentShape, + Type, + Nullable, + Args, + ResolveReturnShape + > & + InferredFieldOptionsByKind< + Types, + Types['InferredFieldOptionsKind'], + ParentShape, + Type, + Nullable, + Args, + ResolveReturnShape + >; } ``` The `InferredFieldOptionsByKind` interface can be used to get the `resolve` option by default, but will also work for plugins that replace the `resolve` function with a different options for configuring how a field is resolved. Some custom object types may want to explicitly define a `resolve` option type, or omit it entirely (eg, the SimpleObject plugin does not use resolvers). # Circular references URL: /docs/patterns/circular-references Two types that reference each other, untangled with objectRef declared up front and implemented later. A `Character` that lists its `Faction`s and a `Faction` that lists its members reference each other, which runs into TypeScript's need to declare a value before using it. Splitting `objectRef` from `implement` gives each type a reference to use before either one's fields exist. ```typescript playground example="patterns-circular-references" const Character = builder.objectRef('Character'); const Faction = builder.objectRef('Faction'); Character.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), factions: t.field({ type: [Faction], resolve: (c) => factions.filter((faction) => c.factionIds.includes(faction.id)), }), }), }); Faction.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), members: t.field({ type: [Character], resolve: (f) => characters.filter((character) => f.memberIds.includes(character.id)), }), }), }); ``` `objectRef` introduces a typed reference *before* the type is implemented. Both refs exist by the time the first `implement` runs, so each can reference the other freely. ## Why this works [#why-this-works] The two-way reference happens at `implement` time. By then both `objectRef` calls have run, so each type already has a reference to the other. This scales to any number of mutually-referential types: declare all the `objectRef` calls first, then all the `implement` calls. ## In a modular layout [#in-a-modular-layout] Splitting `objectRef` and `implement` is also what lets a modular schema cross file boundaries without import cycles: each module declares its `objectRef`, then implements it once the referenced types are in scope. See [Project layout](./project-layout) for the file shape this enables. ## Referencing types by name [#referencing-types-by-name] If you'd rather not pass references around, you can register both type names on the builder's `Objects` generic and refer to each by its string name: ```typescript const builder = new SchemaBuilder<{ Objects: { Character: ICharacter; Faction: IFaction }; }>({}); builder.objectType('Character', { fields: (t) => ({ factions: t.field({ type: ['Faction'], resolve: (character) => findFactions(character.id), }), }), }); builder.objectType('Faction', { fields: (t) => ({ members: t.field({ type: ['Character'], resolve: (faction) => findMembers(faction.id), }), }), }); ``` A string name like `'Faction'` is resolved when the schema builds, so neither definition needs a value from the other in scope, whether they sit in one file or across many. The [Builder types](../fundamentals/objects#definition-styles) style covers registering names this way. # Default nullability URL: /docs/patterns/default-nullability Flip Pothos's nullable-by-default to non-nullable when that matches your codebase better. By default, Pothos fields and arguments are nullable. To make a field non-nullable you pass `nullable: false`; to make an arg required you pass `required: true`. This matches GraphQL's wire-level default — everything is nullable unless marked `!`. In many codebases most values are non-nullable, so most fields end up as `Type!`. Flipping the default once on the builder saves passing `nullable: false` on every field. ```typescript playground example="patterns-default-nullability" const builder = new SchemaBuilder<{ DefaultFieldNullability: false; }>({ defaultFieldNullability: false, }); ``` `DefaultFieldNullability` on the generic and `defaultFieldNullability` in the constructor options have to agree; Pothos refuses to compile if they don't. With both set to `false`, every field declared from this builder defaults to non-nullable. ## Using the flipped default [#using-the-flipped-default] Once the default is non-nullable, you mark the genuinely-optional fields explicitly: ```typescript Race.implement({ fields: (t) => ({ // Non-nullable now, so SDL reads `id: ID!`. id: t.exposeID('id'), name: t.exposeString('name'), // Opt back into nullable for genuinely optional fields. motto: t.exposeString('motto', { nullable: true }), }), }); ``` ## Flipping argument defaults too [#flipping-argument-defaults-too] `DefaultFieldNullability` only affects output fields. Argument and input-field requiredness has its own slot, `DefaultInputFieldRequiredness`, which controls whether input fields and arguments default to required: ```typescript const builder = new SchemaBuilder<{ DefaultFieldNullability: false; DefaultInputFieldRequiredness: true; }>({ defaultFieldNullability: false, defaultInputFieldRequiredness: true, }); ``` With both flipped, the codebase reads as if everything is non-nullable and required by default, closer to TypeScript's defaults than to GraphQL's. ## When not to flip [#when-not-to-flip] Nullability affects how a thrown error propagates: a nullable field can become `null` in place, while a non-nullable field pushes the error up to its nearest nullable ancestor. Defaulting to non-nullable means a single failing resolver can blank out a larger part of the response. The [Handling errors](./handling-errors) guide covers this in full. If your schema serves clients that can't gracefully handle partial responses (legacy mobile clients, brittle codegen), the nullable default may be the safer choice. When clients handle partial responses well, the non-nullable default keeps the schema aligned with your TypeScript types. # Handling errors URL: /docs/patterns/handling-errors Throw from resolvers, mask errors in production, and know when to graduate to plugin-errors. Error handling has three layers, each adding control over what the client sees. Most schemas stop at the first or second; the third is a typed-union approach for when error types are part of the API contract. ## Layer 1: throw from the resolver [#layer-1-throw-from-the-resolver] A resolver that throws produces `null` for the field and one entry in the response's `errors` array. The thrown message reaches the client unless your server masks it. ```typescript playground example="patterns-handling-errors-step-1" builder.queryType({ fields: (t) => ({ team: t.field({ type: Team, args: { id: t.arg.id({ required: true }) }, resolve: (_root, { id }) => { const team = Teams.get(Number(id)); if (!team) { // Plain Error. Yoga returns the message in dev. throw new Error(`No team with id ${id}`); } return team; }, }), }), }); ``` When the field is non-nullable, the executor can't write `null` into it, so the error propagates up to the nearest nullable ancestor, which becomes `null` instead; if there is none, the whole response's `data` is `null`. This is graphql-js's behavior, the same for every GraphQL server. ## Layer 2: mask in production [#layer-2-mask-in-production] In development, the server returns the raw thrown message — useful for debugging. In production, you want generic messages so internal details don't leak. `graphql-yoga`'s `maskedErrors` option controls this: ```typescript // In dev — see the actual message const yoga = createYoga({ schema, maskedErrors: false, }); ``` ```typescript // In prod — replace with "Unexpected error" and log the original const yoga = createYoga({ schema, maskedErrors: process.env.NODE_ENV !== 'production' ? false : true, }); ``` Don't ship `maskedErrors: false` to production. Stack traces, internal IDs, and database error messages all flow through GraphQL errors when masking is off. Default-on with an env-flag escape hatch is the safe pattern. For Apollo Server, the equivalent is the `includeStacktraceInErrorResponses` option (default off in production builds); Mercurius wraps errors via `errorFormatter`. ## Layer 3: typed result unions [#layer-3-typed-result-unions] The first two layers treat errors as exceptions. For errors that are part of the API (a username already taken, a full roster, a missing team), clients are better served by a typed result union: ```graphql type Mutation { renameTeam(input: RenameTeamInput!): RenameTeamResult! } union RenameTeamResult = Team | NotFoundError | PermissionError ``` The client can `switch (result.__typename)` and render the right UI for each case. `@pothos/plugin-errors` produces a union like this from thrown errors. You list the error classes on the field's `errors` option, and the plugin generates a union of the success type plus one member per error. ```typescript class NotFoundError extends Error {} class PermissionError extends Error {} builder.mutationField('renameTeam', (t) => t.field({ type: Team, errors: { types: [NotFoundError, PermissionError] }, args: { /* ... */ }, resolve: (_root, args, ctx) => { // throw new NotFoundError() or new PermissionError() for expected failures }, }), ); ``` See [`plugin-errors`](../plugins/errors) for the full setup. ## When to graduate [#when-to-graduate] The boundaries are usually clear: * **Layer 1 alone** is enough for prototypes and internal tools. * **Layer 1 + Layer 2** is the right baseline for production. Throw clear messages, mask them in prod, log the originals server-side. * **Layer 3** fits when errors are part of the contract: a mutation has expected failure modes that clients render specifically. You don't have to pick one. Schemas usually mix: most resolvers throw plain `Error`s and rely on masking; a few mutations use typed unions where the failure modes are well-known. # Inferring types URL: /docs/patterns/inferring-types Pull TypeScript types back out of Pothos refs to write helpers, fixtures, and utility functions. Pothos refs carry their backing model as a type parameter. `$inferType` and `$inferInput` are the escape hatches that let you pull that type back out for use in plain TypeScript code. ```typescript playground example="patterns-inferring-types" const Race = builder.objectRef('Race').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), lifespan: t.exposeString('lifespan'), }), }); const RaceFilter = builder.inputType('RaceFilter', { fields: (t) => ({ nameContains: t.string(), immortal: t.boolean(), }), }); type RaceShape = typeof Race.$inferType; type RaceFilterShape = typeof RaceFilter.$inferInput; ``` `RaceShape` is `IRace`. `RaceFilterShape` is `{ nameContains?: string | null; immortal?: boolean | null }`. The pattern is similar to Drizzle's `InferSelectModel` and `InferInsertModel`. ## Where it's useful [#where-its-useful] The most common cases: * **Resolver helpers.** A function that operates on `Race`-shaped values can take the inferred type instead of duplicating it. * **Test fixtures.** Generate mock data that's guaranteed to match the ref's backing model. * **Argument validators.** A Zod schema or other validator can take the input type as its starting point. ```typescript function matches(race: typeof Race.$inferType, filter: typeof RaceFilter.$inferInput): boolean { if (filter.nameContains && !race.name.toLowerCase().includes(filter.nameContains.toLowerCase())) { return false; } if (filter.immortal != null && (race.lifespan === 'immortal') !== filter.immortal) { return false; } return true; } ``` ## Inferring SchemaTypes [#inferring-schematypes] For helpers that work across types, `builder.$inferSchemaTypes` gives you the merged `SchemaTypes` of a builder. Use it when writing a function that takes any field-builder context: ```typescript type BuilderTypes = typeof builder.$inferSchemaTypes; function createIdField( fields: (t: PothosSchemaTypes.ObjectFieldBuilder) => FieldMap, ) { // ... } ``` The `PothosSchemaTypes` namespace refers to the builder's generic type machinery. You need it only when writing a helper that has to be type-aware about the surrounding schema. ## Not a substitute for the source types [#not-a-substitute-for-the-source-types] `$inferType` is most useful as an *escape hatch* — pulling a type out for one specific helper or fixture. If you find yourself inferring the same type in five places, you probably want the original interface to be exported and reused. ```typescript // Better: export interface IRace { /* ... */ } const Race = builder.objectRef('Race'); // Worse: const Race = builder.objectRef<{ id: string; name: string; /* ... */ }>('Race'); type IRace = typeof Race.$inferType; // Round trip through Pothos to get back to a plain TS type ``` The first form keeps `IRace` as the source of truth. The second uses Pothos's machinery as a glorified type alias. # Printing and codegen URL: /docs/patterns/printing-and-codegen Emit an SDL file from a Pothos schema and generate client types with graphql-code-generator. A Pothos schema is a TypeScript module, but two downstream consumers usually want an SDL string: schema-aware tooling (linters, diff viewers, federation gateways) and `graphql-code-generator`, which produces client types from the schema. Both can read the SDL directly from disk. ## Printing to a file [#printing-to-a-file] `printSchema` from `graphql` serializes a `GraphQLSchema` to SDL. `lexicographicSortSchema` sorts types and fields alphabetically. Without it the output order tracks source order, so unrelated edits produce noisy diffs. ```typescript import { writeFileSync } from 'node:fs'; import { lexicographicSortSchema, printSchema } from 'graphql'; import { schema } from './schema'; writeFileSync('./schema.graphql', printSchema(lexicographicSortSchema(schema))); ``` Wire this into a script in `package.json`: ```json { "scripts": { "schema": "tsx scripts/print-schema.ts" } } ``` Running `npm run schema` after a schema change refreshes the SDL file. Check it into source control; diffs in `schema.graphql` are easier to review than diffs across a hundred TypeScript files. The playground sandbox has no filesystem to write to, but the same two calls work standalone: swap the file write for a `console.log` and you can see exactly the SDL `printSchema` produces: ```typescript playground example="patterns-printing-schema" export const schema = builder.toSchema(); console.log(printSchema(lexicographicSortSchema(schema))); ``` Open the console panel after running it to see the sorted SDL. ## Setting up graphql-code-generator [#setting-up-graphql-code-generator] Clients usually want typed query documents. `graphql-code-generator` reads your operations and the schema, then emits typed hooks or query functions. npm pnpm yarn bun ```bash npm install --save graphql npm install --save -D @graphql-codegen/cli @graphql-codegen/client-preset ``` ```bash pnpm add graphql pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset ``` ```bash yarn add graphql yarn add --dev @graphql-codegen/cli @graphql-codegen/client-preset ``` ```bash bun add graphql bun add --dev @graphql-codegen/cli @graphql-codegen/client-preset ``` Point the codegen at the SDL file you just printed: ```typescript // codegen.ts import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { schema: './schema.graphql', documents: ['src/**/*.tsx'], generates: { './src/gql/': { preset: 'client', plugins: [], }, }, }; export default config; ``` Running `graphql-codegen` regenerates the types under `src/gql/`. Most teams add it to a pre-commit hook or to the dev server's watch loop. ## Reading the schema directly [#reading-the-schema-directly] If you'd rather skip the SDL file, the codegen can import the schema module: ```typescript import { printSchema } from 'graphql'; import { schema } from './src/schema'; const config: CodegenConfig = { schema: printSchema(schema), // ... }; ``` The trade-off is that `codegen.ts` now depends on every transitive import of `schema.ts`, typically including your database client. Checking the SDL file into source control decouples the two and makes the schema visible in PR diffs. Most projects do both: print to file as part of the schema-update flow, and have codegen read from the file. ## Custom scalars [#custom-scalars] If your schema uses scalars beyond the built-ins, the codegen needs a TypeScript type for each — without one, every custom scalar field collapses to `any`: ```typescript const config: CodegenConfig = { // ... config: { scalars: { DateTime: 'Date', UUID: 'string', JSON: 'unknown', }, }, }; ``` The map matches the GraphQL scalar name to the TypeScript type clients should see for it. ## Federation and other consumers [#federation-and-other-consumers] The same printed SDL feeds: * Federation gateways and supergraph composition tools. * Schema-diff CI checks (does this PR break a published schema?). * Linters like `graphql-eslint`. * IDE integrations that pre-validate queries. # Project layout URL: /docs/patterns/project-layout Grow a single-file schema into a modular layout once it stops fitting in one file. You can spread a Pothos schema across as many files as you like. The smallest project lives in one `schema.ts`; larger ones split across many. This page covers moving from one file to several. ```typescript playground example="patterns-project-layout-step-1" const builder = new SchemaBuilder({}); const Race = builder.objectRef('Race').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name') }), }); const Character = builder.objectRef('Character').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), race: t.field({ type: Race, resolve: (c) => Races.get(c.raceId)! }), }), }); builder.queryType({ fields: (t) => ({ characters: t.field({ type: [Character], resolve: () => Characters }), races: t.field({ type: [Race], resolve: () => [...Races.values()] }), }), }); export const schema = builder.toSchema(); ``` A single file works well for a small schema. As it grows, splitting it across files keeps each type easy to find, rather than scrolling past a hundred-line `Character` definition to reach the `Race` type. ## A modular layout [#a-modular-layout] Most projects settle on the same shape: a shared `builder` module, one file per domain entity, and a `schema.ts` that imports each module before building the schema. ```typescript playground example="patterns-project-layout-step-2" // builder.ts — the one place the SchemaBuilder is constructed. export const builder = new SchemaBuilder({}); builder.queryType({}); ``` ```typescript // race.ts — owns the Race type and the queries that return it. import { builder } from './builder'; import { type IRace, Races } from './data'; export const Race = builder.objectRef('Race').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.queryFields((t) => ({ races: t.field({ type: [Race], resolve: () => [...Races.values()], }), })); ``` ```typescript // character.ts — owns Character + its queries. Cross-domain reference // (race) lives here, not in race.ts. import { builder } from './builder'; import { Characters, type ICharacter, Races } from './data'; import { Race } from './race'; export const Character = builder.objectRef('Character').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), race: t.field({ type: Race, resolve: (c) => Races.get(c.raceId)! }), }), }); builder.queryFields((t) => ({ characters: t.field({ type: [Character], resolve: () => Characters, }), })); ``` ```typescript // schema.ts — imports every domain module for its side effects, // then builds. import { builder } from './builder'; import './race'; import './character'; export const schema = builder.toSchema(); ``` The shape: * **`builder.ts`** is the one file that calls `new SchemaBuilder`. Every other module imports `builder` from here. There's exactly one builder per schema. * **One file per domain entity**, holding the object type, its fields, and the query/mutation entrypoints that return it. Cross-domain references (a `Character` returning a `Race`) live in the file that depends on the other type, not in the one being depended on. * **`schema.ts`** is the entry point. It imports each domain module for its side effects, then calls `builder.toSchema()`. The whole graph is registered by the time `toSchema()` runs. ## Why side-effect imports [#why-side-effect-imports] The domain modules call `builder.objectRef(...).implement(...)` and `builder.queryFields(...)` at the top level. These are side effects: they register the type and its fields on the shared builder. `schema.ts` only needs each module loaded before `toSchema()`; it doesn't use any of their exports. If you'd rather avoid side-effect imports, every module can export its types and `schema.ts` can list them explicitly: ```typescript import { Race } from './race'; import { Character } from './character'; const _types = [Race, Character]; // ensures both modules are loaded export const schema = builder.toSchema(); ``` Both produce the same schema; use whichever your team prefers. ## When to split further [#when-to-split-further] Once a single domain module crosses 200–300 lines, split *inside* it. `builder.objectField` adds a field to an existing type from anywhere, so a separate `character/weapons.ts` can attach a `wieldedWeapons` field to `Character` without crowding `character.ts`: ```typescript // character/weapons.ts builder.objectField(Character, 'wieldedWeapons', (t) => t.field({ type: [Weapon], resolve: (c) => loadWeaponsFor(c.id), }), ); ``` This is most useful for cross-domain attachments: a `character × item` field belongs naturally in neither the character module nor the item module. ## Two reference layouts [#two-reference-layouts] For larger schemas, the repo's `examples/` directory has two trees worth reading: * [`examples/lord-of-the-rings`](https://github.com/hayes/pothos/tree/main/examples/lord-of-the-rings) — core-only Pothos, no plugins. Demonstrates the layout above with rich interfaces, unions, and cross-domain references. * [`examples/ultimate-league`](https://github.com/hayes/pothos/tree/main/examples/ultimate-league) — Drizzle + scope-auth + validation. Shows how plugin configuration sits alongside the same layout. Both are complete, runnable projects. # Reusable fields URL: /docs/patterns/reusable-fields Share field, argument, and input shapes across multiple Pothos types without using an interface. When several types share a common field or argument shape but you don't want to introduce a GraphQL interface to model it, write a helper that returns the shared definition. Pothos exposes the underlying builders so the helper stays type-safe. ## Sharing fields across object types [#sharing-fields-across-object-types] `builder.objectFields(ref, builderFn)` adds fields to an existing object type. Wrap a few of them in a function that takes a list of refs: ```typescript playground example="shared-fields-pattern" function addAuditFields< Refs extends readonly ObjectRef[], >(refs: Refs) { for (const ref of refs) { builder.objectFields(ref, (t) => ({ id: t.field({ type: 'ID', resolve: (parent) => parent.id }), createdAt: t.field({ type: 'DateTime', resolve: (parent) => parent.createdAt }), })); } } const Team = builder.objectRef('Team').implement({ fields: (t) => ({ name: t.exposeString('name'), }), }); const Player = builder.objectRef('Player').implement({ fields: (t) => ({ name: t.exposeString('name'), }), }); addAuditFields([Team, Player]); ``` Both types now expose `id` and `createdAt` without duplicating the field definitions. The generic constraint on the helper guarantees every passed ref has the backing-model fields the closures rely on. `TypesWithDefaults` is `PothosSchemaTypes.ExtendDefaultTypes`, the fully-extended types object every builder method operates on. The same approach works for interfaces: `builder.interfaceFields(ref, builderFn)` adds fields to an interface type, so a helper that needs to cover both can call whichever builder matches the ref it's given. ## Sharing arguments across fields [#sharing-arguments-across-fields] Arguments are inferred from the literal `args:` object Pothos sees, so you can't add them after the fact the way you can with `objectFields`. Instead, write a helper that returns the args map and spread it into each field: ```typescript playground example="reusable-args-pattern" function pagination(t: { arg: ArgBuilder }) { return { limit: t.arg.int({ required: true, defaultValue: 25 }), offset: t.arg.int({ required: true, defaultValue: 0 }), }; } builder.queryFields((t) => ({ teams: t.field({ type: [Team], args: { ...pagination(t) }, resolve: (_root, args) => listTeams(args), }), players: t.field({ type: [Player], args: { ...pagination(t), teamId: t.arg.id() }, resolve: (_root, args) => listPlayers(args), }), })); ``` The helper takes an object shaped like the field builder's `arg` property (`t.arg`), so the returned args carry their full type. Spreading the result preserves the inference Pothos relies on. ## Sharing input fields [#sharing-input-fields] Input fields work the same way: ```typescript playground example="reusable-input-pattern" function timestampInputs(t: InputFieldBuilder) { return { createdAfter: t.field({ type: 'DateTime', required: false }), createdBefore: t.field({ type: 'DateTime', required: false }), }; } builder.inputType('GameFilter', { fields: (t) => ({ ...timestampInputs(t), teamId: t.id(), }), }); builder.inputType('PlayerFilter', { fields: (t) => ({ ...timestampInputs(t), position: t.string(), }), }); ``` Each input type ends up with its own version of the timestamp fields plus whatever it adds on top. ## When an interface fits better [#when-an-interface-fits-better] If the shared fields represent a real abstraction your clients can query against (every entity has an `id`, every audit-logged record has `createdAt`/`updatedAt`), the cleaner answer is a GraphQL interface. Clients can fragment over the interface, codegen tools see the relationship, and the schema documents the polymorphism. The helper pattern fits when the shared fields are an implementation detail rather than an abstraction worth exposing. # Add GraphQL plugin URL: /docs/plugins/add-graphql Bring types and whole schemas from an existing graphql-js service into a Pothos builder while you migrate to code-first. The add-graphql plugin brings types from an existing executable GraphQL schema into a Pothos builder. Point it at a `GraphQLObjectType`, a handful of types, or a whole `GraphQLSchema` and Pothos re-registers them as its own, so you can migrate a service built on nexus, graphql-tools, or plain graphql-js one piece at a time while new fields are written in Pothos. There are two entry points. The `add` builder option pulls types in *bulk* when you construct the builder; the `addGraphQL` methods pull them *one at a time* and hand back a [ref](../fundamentals/objects) you can customize and use in field definitions. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-add-graphql ``` ```bash pnpm add @pothos/plugin-add-graphql ``` ```bash yarn add @pothos/plugin-add-graphql ``` ```bash bun add @pothos/plugin-add-graphql ``` ```typescript import AddGraphQLPlugin from '@pothos/plugin-add-graphql'; const builder = new SchemaBuilder({ plugins: [AddGraphQLPlugin], }); ``` ## Import an existing schema [#import-an-existing-schema] Pass `add` when constructing the builder to fold existing types into the schema at build time. `add.schema` imports every type in a `GraphQLSchema`; `add.types` imports a specific list. Either way, any type reachable through a field, interface, or union member is imported recursively, so you don't have to enumerate dependencies. ```typescript playground example="add-graphql-plugin" const legacySchema = new GraphQLSchema({ types: [LegacyTeam] }); // Registering Team on the Objects generic lets you reference it by name. const builder = new SchemaBuilder<{ Objects: { Team: ITeam }; }>({ plugins: [AddGraphQLPlugin], add: { // Import every type in the schema; dependencies come along recursively. schema: legacySchema, }, }); builder.queryType({ fields: (t) => ({ teams: t.field({ type: ['Team'], resolve: () => [...Teams.values()], }), }), }); ``` `add.types` accepts an array of `GraphQLNamedType` (objects, interfaces, unions, enums, scalars, and input objects all qualify), or a name-keyed record of the same: ```typescript const builder = new SchemaBuilder({ plugins: [AddGraphQLPlugin], add: { types: [legacySchema.getType('Team')!, legacySchema.getType('Division')!], }, }); ``` Imported types have to be *referenced* to be useful. Register them on the builder's generic `SchemaTypes` and you can name them as strings anywhere Pothos expects a type, as `teams` does with `['Team']` above. This shortcut covers **object, interface, and scalar** types only; for unions, enums, and inputs, use the `addGraphQL` methods below to get a ref instead. A type is imported only if no type of the same name is already registered on the builder before the schema is built. A name you define yourself always wins, so the plugin never clobbers your own types, but it also silently skips an imported type whose name you've already taken. Importing a schema that has its own `Query`, `Mutation`, or `Subscription` merges those root fields by calling `builder.queryType` (or `mutationType`/`subscriptionType`) for you. Add your own root fields with `builder.queryFields` so they merge with the imported ones. Defining your own `builder.queryType` instead does not error; the plugin silently skips the imported root type, dropping its fields. ## Refs for individual types [#refs-for-individual-types] The `addGraphQL` methods import one type and return a ref (an `ObjectRef`, `InterfaceRef`, and so on) that behaves exactly like a ref you built from scratch. Use it as a field's `type`, and customize the imported type on the way in. Passing a generic `Shape` is recommended: it types the ref's backing model so resolvers stay checked. ```typescript playground example="add-graphql-plugin-step-2" // addGraphQLInput returns an InputObjectRef you can hang on any arg. const PlayerFilter = builder.addGraphQLInput<{ position?: 'HANDLER' | 'CUTTER' }>(LegacyPlayerFilter); // addGraphQLObject returns an ObjectRef. Customize fields as you import: // null drops a field, and new entries are merged in alongside the rest. const Player = builder.addGraphQLObject(LegacyPlayer, { fields: (t) => ({ fullName: null, displayName: t.exposeString('fullName'), }), }); builder.queryType({ fields: (t) => ({ players: t.field({ type: [Player], args: { filter: t.arg({ type: PlayerFilter }), }, resolve: (_parent, { filter }) => [...Players.values()].filter( (player) => !filter?.position || player.position === filter.position, ), }), }), }); ``` The `fields` shape works like a patch over the imported type: return `null` for a field to drop it, return a field ref to add or replace one, and every field you don't mention is imported unchanged. Everything else Pothos can carry over (description, deprecation reasons, `isTypeOf`/`resolveType`, extensions, default values, and `isOneOf` on inputs) comes across automatically from the source type. ## The type methods [#the-type-methods] Each method takes the source `GraphQLType` plus optional options, and returns the matching ref: | Method | Source type | Returns | | ---------------------------- | ------------------------ | ----------------------- | | `addGraphQLObject` | `GraphQLObjectType` | `ObjectRef` | | `addGraphQLInterface` | `GraphQLInterfaceType` | `InterfaceRef` | | `addGraphQLUnion` | `GraphQLUnionType` | `UnionRef` | | `addGraphQLEnum` | `GraphQLEnumType` | `EnumRef` | | `addGraphQLInput` | `GraphQLInputObjectType` | `InputObjectRef` | Every method's options are the standard options for that type kind, minus the part derived from the source, plus a `name` to rename the type as you import it: * **Objects and interfaces** take everything an object or interface type takes except `fields`, which is replaced by the patch-style shape above (`fieldName: null` to drop, a ref to add or replace). In practice the options that take effect are `name` and `extensions` (merged into the source's). `description`, `isTypeOf`/`resolveType`, and `interfaces` always come from the source type, so set those on the source rather than passing them here. * **Unions** take a `types` override to spell out the members yourself instead of importing the source's. * **Enums** take a `values` override to remap the values. * **Inputs** take the same patch-style `fields` shape as objects. If the source type isn't already typed with a generic, cast it to the expected kind so the method's `Shape` binds, for example `addGraphQLObject(schema.getType('Player') as GraphQLObjectType)`. ```typescript const NodeRef = builder.addGraphQLInterface(LegacyNode, { name: 'Entity', }); const SearchResult = builder.addGraphQLUnion(LegacySearchResult); const Division = builder.addGraphQLEnum<'EAST' | 'WEST'>(LegacyDivision); ``` ## Scalars [#scalars] There's no `addGraphQLScalar` method, because Pothos already has one. Register an existing scalar with the core `builder.addScalarType`: ```typescript builder.addScalarType('DateTime', LegacyDateTime); ``` Scalars imported through the `add` option or referenced by name from the `Scalars` generic work the same way; the dedicated method just exists for the ref-returning case. # Complexity plugin URL: /docs/plugins/complexity Score fields by cost and cap the complexity, depth, and breadth of incoming queries. A single GraphQL request can ask for an unbounded amount of work: deep nesting, wide selection sets, list fields that fan out into more list fields. The complexity plugin scores every field in a query and rejects requests that cost too much, before any resolver runs. You set the ceilings once on the builder (or per-request in `toSchema`), and tune the cost of individual fields with a `complexity` option where the defaults are wrong. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-complexity ``` ```bash pnpm add @pothos/plugin-complexity ``` ```bash yarn add @pothos/plugin-complexity ``` ```bash bun add @pothos/plugin-complexity ``` ## Capping query cost [#capping-query-cost] Register the plugin and pass a `complexity` option to the builder. `limit` sets the three ceilings a query must stay under; `defaultComplexity` and `defaultListMultiplier` set the baseline cost every field starts from. ```typescript playground example="complexity-plugin-step-1" const builder = new SchemaBuilder({ plugins: [ComplexityPlugin], complexity: { defaultComplexity: 1, defaultListMultiplier: 10, limit: { complexity: 100, depth: 5, breadth: 30, }, }, }); ``` The three limits guard against different shapes of abuse: * **`complexity`** is the maximum total cost, summed across every selected field. * **`depth`** is the maximum nesting depth of the selection set. * **`breadth`** is the total number of fields selected across the entire query. `limit` can also be a function that receives the context, so you can raise or lower the ceilings per request (a higher budget for an authenticated internal service, a tighter one for anonymous traffic): ```typescript complexity: { limit: (ctx) => ({ complexity: ctx.trusted ? 5000 : 500, depth: 10, breadth: 50, }), }, ``` When the limits belong to the server that builds the schema rather than the schema definition itself, pass the same options to `toSchema` (or `buildSchema`) instead of the builder: ```typescript const schema = builder.toSchema({ complexity: { limit: { complexity: 500, depth: 10, breadth: 50, }, }, }); ``` A query with no `complexity` limit set is never rejected, no matter how expensive; the plugin only enforces the ceilings you provide. Set at least a `complexity` limit before shipping a public endpoint. ## How complexity is calculated [#how-complexity-is-calculated] Complexity is computed before any root-level field (query, mutation, or subscription) resolves, from the shape of the query alone; no resolver runs during scoring. The cost of a query is the sum of the cost of each selected field. When a field has sub-selections, the cost of those sub-selections is multiplied by the field's multiplier, then added to the field's own cost. The default multiplier is `1` for scalar fields and `defaultListMultiplier` (10 above) for list fields, a rough stand-in for the n+1 fan-out a list implies. The default query costs `121`, with a depth of `3` and a breadth of `4`: ```graphql query Roster { teams { # 121 = teams(1) + 10 * (name 1 + roster 11) name # 1 roster { # 11 = roster(1) + 10 * name(1) name # 1, at depth 3 } } } ``` That's over the `complexity` limit of 100, so the plugin throws before resolving `teams`. Delete the `roster` block and the cost drops to `11`, under the limit, and the data comes back. ## Scoring individual fields [#scoring-individual-fields] The defaults treat every field the same, but some fields are more expensive to resolve than others. Set a `complexity` option on any field to override its baseline. It takes three forms: ```typescript playground example="complexity-plugin-step-2" // A flat base cost for an expensive aggregate; its list sub-selections // still use the default multiplier of 10. leaderboard: t.field({ type: [Player], complexity: 20, resolve: () => [...Players.values()], }), ``` A plain number sets the field's own cost. To override the multiplier applied to its sub-selections as well, pass an object with `field` and `multiplier`: ```typescript // A roster is cheap to load once the team is in memory: override both the // field cost and the default list multiplier of 10 with hand-tuned values. roster: t.field({ type: [Player], complexity: { field: 2, multiplier: 5 }, resolve: (team) => [...Players.values()].filter((p) => p.teamId === team.id), }), ``` `complexity` can also be a function of the field's arguments and the context, so a field that returns more rows costs more: ```typescript // Cost scales with how many rows the caller asks for. players: t.field({ type: [Player], args: { first: t.arg.int(), }, complexity: (args) => ({ field: 5, multiplier: args.first ?? 5 }), resolve: (_parent, { first }) => [...Players.values()].slice(0, first ?? undefined), }), ``` To change the baseline for *every* field at once, set `fieldComplexity` on the builder, a function `(args, ctx, field) => number | { field, multiplier }` that runs for any field without its own `complexity` option. When it's set, `defaultComplexity` and `defaultListMultiplier` are ignored. ## Customizing the limit error [#customizing-the-limit-error] By default the plugin throws a `PothosValidationError` naming the ceiling that was exceeded: `Query exceeds maximum complexity (complexity: 121, max: 100)`. Provide `complexityError` on the builder to return your own error or message: ```typescript complexity: { complexityError: (kind, result, info) => { // kind is 'Complexity', 'Depth', or 'Breadth' return `Query too expensive (${kind}): ${result.complexity}/${result.maxComplexity}`; }, }, ``` The function receives the error kind, a `result` carrying the query's `complexity`, `depth`, and `breadth` alongside the `maxComplexity`, `maxDepth`, and `maxBreadth` limits it was checked against, and the GraphQL `info` object. Return (or throw) an `Error`, or return a string to have the plugin throw it for you. Set `disabled: true` on the builder's `complexity` option to skip the check entirely, which turns limits off in a trusted environment without pulling the plugin out of the build. ## Measuring a query yourself [#measuring-a-query-yourself] `complexityFromQuery` scores a query outside of execution: log costs, reject requests at the edge, or assert on complexity in tests. ```typescript import { complexityFromQuery } from '@pothos/plugin-complexity'; const complexity = complexityFromQuery(query, { schema, // Complexity can depend on the context and arguments, so pass valid values // when a field's cost function reads them. Both are optional and default to // empty objects. ctx: {}, variables: {}, }); ``` ## Options [#options] Every option lives under the builder's `complexity` object (or the one passed to `toSchema`/`buildSchema`): | Option | Purpose | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | The `{ complexity, depth, breadth }` ceilings a query must stay under. Either an object or a function that receives the context. Fields you omit are not enforced. | | `defaultComplexity` | Baseline cost for every field without its own `complexity` option. Defaults to `1`. | | `defaultListMultiplier` | Baseline multiplier applied to a list field's sub-selections. Defaults to `10`. | | `fieldComplexity` | `(args, ctx, field) => number \| { field, multiplier }`, a default cost calculation for all fields. Overrides `defaultComplexity` and `defaultListMultiplier` when set. | | `complexityError` | `(kind, result, info) => Error \| string`, the error to throw when a limit is exceeded. Defaults to a `PothosValidationError`. | | `disabled` | Set `true` to skip the complexity check entirely. Unlike the other options, this is read only from the builder's `complexity` option; passing it to `toSchema`/`buildSchema` has no effect. | The per-field `complexity` option accepts a `number`, an object `{ field?, multiplier? }`, or a function `(args, ctx, field) => number | { field?, multiplier? }`. # Dataloader plugin URL: /docs/plugins/dataloader Batch object and relation loads through a dataloader to kill N+1 queries, using loadableObject and the loadable field methods. A GraphQL query that walks a list and then a field on each item fans out into one database round-trip per item, the N+1 problem. The dataloader plugin folds those calls into one. You define a type with `builder.loadableObject`, resolvers return *ids* instead of objects, and the plugin routes each id into a DataLoader, which coalesces every id loaded in a tick into a single `load` call. ## Install [#install] npm pnpm yarn bun ```bash npm install --save dataloader @pothos/plugin-dataloader ``` ```bash pnpm add dataloader @pothos/plugin-dataloader ``` ```bash yarn add dataloader @pothos/plugin-dataloader ``` ```bash bun add dataloader @pothos/plugin-dataloader ``` `loadableObject`, the `loadable*` field methods, and the helpers below all come from the plugin once it's on the builder. ## Loadable objects [#loadable-objects] `builder.loadableObject` defines an object type whose backing rows are fetched through a dataloader. `load` receives the ids collected across the whole query; fields resolving to that type return an id and the plugin does the fetching. ```typescript playground example="dataloader-plugin" import DataloaderPlugin from '@pothos/plugin-dataloader'; const builder = new SchemaBuilder({ plugins: [DataloaderPlugin], }); const Player = builder.loadableObject('Player', { // Called once per request with every id the query touched. Return one result // per id, in the same order — an Error in a slot fails just that player. load: async (ids: string[]) => { console.log('loading players', ids); return ids.map((id) => players.get(id) ?? new Error(`No player ${id}`)); }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), number: t.exposeInt('number'), }), }); builder.queryType({ fields: (t) => ({ // Return the id; the loader turns it into a Player. player: t.field({ type: Player, args: { id: t.arg.string({ required: true }) }, resolve: (_root, args) => args.id, }), // A list of ids batches into a single load call. roster: t.field({ type: [Player], args: { ids: t.arg.stringList({ required: true }) }, resolve: (_root, args) => args.ids, }), }), }); ``` Annotate the `ids` parameter (and `context`, if you use it). Pothos reads the load function's types to constrain what resolvers for this type may return. A resolver can return a `string`, `number`, or `bigint` key, and the loader fetches it. When you already have the record in hand, return the full object instead; Pothos detects that it isn't a key and skips the loader. Lists may mix the two: `[...ids, alreadyLoadedPlayer]` resolves the keys through the loader and passes the object straight through. `load` must return results in the **same order** as the ids it was given, because the plugin maps results back to ids positionally, so a shuffled result array hands clients the wrong records. Fetching from a database rarely preserves order; the [`sort` option](#sorting-load-results) below does the reordering for you. See the [dataloader batch-function docs](https://github.com/graphql/dataloader#batch-function) for the contract in full. ## Batching relations [#batching-relations] A one-to-many like a team's roster is a `loadableGroup` field: `load` runs once with every parent id in the query and returns a flat list, and `group` sorts each row back to the parent it belongs to. ```typescript playground example="dataloader-plugin-step-2" const Team = builder.loadableObject('Team', { load: async (ids: string[]) => { console.log('loading teams', ids); return ids.map((id) => teams.get(id) ?? new Error(`No team ${id}`)); }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // One load call fetches the roster for every team in the query at once. // `load` returns a flat list; `group` sorts each player back to its team. players: t.loadableGroup({ type: Player, load: async (teamIds: string[]) => { console.log('loading rosters', teamIds); return players.filter((player) => teamIds.includes(player.teamId)); }, group: (player: IPlayer) => player.teamId, resolve: (team: ITeam) => team.id, }), }), }); ``` Use `loadableGroup` when your query returns a flat list, since group-by is cheaper than nesting. If your data source already returns a list *per* parent (a `Player[][]`, one array per id, in id order), use `loadableList` instead: ```typescript builder.objectField(Team, 'players', (t) => t.loadableList({ type: Player, // called with every team id; returns Player[][], one array per id, in order load: (teamIds: string[], context) => context.rostersByTeamId(teamIds), resolve: (team) => team.id, }), ); ``` For a plain many-to-one (or a field with its own dataloader) use `t.loadable`. Its `type` may be a single type or a list; `load` is called with the keys your `resolve` returns: ```typescript builder.objectField(Player, 'team', (t) => t.loadable({ type: Team, load: (ids: string[], context) => context.loadTeams(ids), resolve: (player) => player.teamId, }), ); ``` `loadableInterface` and `loadableUnion` mirror `loadableObject` for interface and union types, and take the same `load`/`sort` options. ## Field arguments [#field-arguments] A field's `load` normally has no access to its arguments, because the dataloader aggregates calls across selections and aliases that may not share arguments. Pass `byPath: true` to aggregate only calls at the same query path, which do share arguments, and `load` gains a third `args` parameter: ```typescript builder.objectField(Team, 'topScorers', (t) => t.loadable({ type: [Player], byPath: true, args: { limit: t.arg.int({ required: true }) }, load: (ids: string[], context, args) => context.loadTopScorers(ids, args.limit), resolve: (team) => team.id, }), ); ``` ## Dataloader options [#dataloader-options] Pass options straight through to the underlying dataloader with `loaderOptions`, on a loadable type or any loadable field. See the [dataloader API](https://github.com/graphql/dataloader#api) for the full set. ```typescript const Player = builder.loadableObject('Player', { loaderOptions: { maxBatchSize: 20 }, load: (ids: string[], context) => context.loadPlayers(ids), fields: (t) => ({ id: t.exposeID('id') }), }); ``` ## Sorting load results [#sorting-load-results] Rather than hand-sorting `load` results into id order, give any loadable type or field a `sort` function that returns a row's key. The plugin builds the id-to-result map for you: ```typescript const Player = builder.loadableObject('Player', { load: (ids: string[], context) => context.loadPlayers(ids), sort: (player) => player.id, fields: (t) => ({ id: t.exposeID('id') }), }); ``` `sort` throws if a result is an `Error`, because an error has no key to sort by. Don't use `sort` on a loader whose results may include per-item errors; return them in id order yourself instead. ## Caching resolved values [#caching-resolved-values] When a resolver returns a full object it skips the loader, so that object never enters the loader's cache and a later selection re-fetches it. `cacheResolved` primes the cache with anything a resolver returns; pass a function that maps the object to its key: ```typescript const Player = builder.loadableObject('Player', { load: (ids: string[], context) => context.loadPlayers(ids), cacheResolved: (player) => player.id, fields: (t) => ({ id: t.exposeID('id') }), }); ``` When you need both `cacheResolved` and `sort`, defining the key extractor twice is redundant. Provide a single `toKey` and set the other two to `true`: ```typescript const Player = builder.loadableObject('Player', { load: (ids: string[], context) => context.loadPlayers(ids), toKey: (player) => player.id, cacheResolved: true, sort: true, fields: (t) => ({ id: t.exposeID('id') }), }); ``` ## Using the loader directly [#using-the-loader-directly] Every loadable type exposes its dataloader through `getDataloader(context)`. Loaders live on the context, so they aren't shared across requests, so pass the current context to get the right one: ```typescript builder.queryField('player', (t) => t.field({ type: Player, resolve: (_root, _args, context) => Player.getDataloader(context).load('1'), }), ); ``` `loadMany` resolves to `(Player | Error)[]`. GraphQL has no special handling for `Error` objects in a list, so wrap the call in `rejectErrors` to turn each error into a rejected promise the normal resolver flow can surface: ```typescript import { rejectErrors } from '@pothos/plugin-dataloader'; builder.queryField('players', (t) => t.field({ type: [Player], resolve: (_root, _args, context) => rejectErrors(Player.getDataloader(context).loadMany(['1', '2'])), }), ); ``` Your own `load` function may return the same `(Player | Error)[]` shape when it has partial failures; the plugin maps each `Error` to a rejected promise so it errors only that item. ## Loaders on the context [#loaders-on-the-context] To reach loaders straight from `context` rather than a type ref, add helpers to your context type and factory. `initContextCache` keeps the loaders consistent if your server copies the context before resolving: ```typescript import { LoadableRef } from '@pothos/plugin-dataloader'; export interface ContextType { playerLoader: DataLoader; getLoader: (ref: LoadableRef) => DataLoader; load: (ref: LoadableRef, id: K) => Promise; loadMany: (ref: LoadableRef, ids: K[]) => Promise<(Error | V)[]>; } ``` ```typescript import { initContextCache } from '@pothos/core'; import { LoadableRef, rejectErrors } from '@pothos/plugin-dataloader'; export const createContext = (req, res): ContextType => ({ // Prevents duplicate loaders if the server extends the context object. ...initContextCache(), // Getters let each helper read the live context through `this`. get playerLoader() { return Player.getDataloader(this); }, get getLoader() { return (ref: LoadableRef) => ref.getDataloader(this); }, get load() { return (ref: LoadableRef, id: K) => ref.getDataloader(this).load(id); }, get loadMany() { return (ref: LoadableRef, ids: K[]) => rejectErrors(ref.getDataloader(this).loadMany(ids)); }, }); ``` Resolvers then load from the context directly: `context.playerLoader.load('1')`, `context.getLoader(Player).load('2')`, `context.load(Player, '3')`, or `context.loadMany(Player, ['1', '2'])`. ## Relay nodes [#relay-nodes] With the [Relay plugin](./relay) installed, `builder.loadableNode` creates a `Node` that loads through a dataloader like any other loadable object: ```typescript const PlayerNode = builder.loadableNode('PlayerNode', { id: { resolve: (player) => player.id }, load: (ids: string[], context) => context.loadPlayers(ids), fields: (t) => ({ name: t.exposeString('name') }), }); ``` To data-load a Relay connection, combine `builder.connectionObject` for the edge and connection types, a `byPath` `loadable` field so it can read the connection arguments, and `t.arg.connectionArgs`: ```typescript const TeammatesConnection = builder.connectionObject({ type: Player, name: 'TeammatesConnection', }); builder.objectFields(Player, (t) => ({ teammates: t.loadable({ type: TeammatesConnection, byPath: true, args: { ...t.arg.connectionArgs() }, load: async (ids: string[], context, args) => { const teammatesById = await context.loadTeammates(ids); return ids.map((id) => resolveArrayConnection({ args }, teammatesById[id])); }, resolve: (player) => player.id, }), })); ``` ## Splitting refs [#splitting-refs] Two loadable objects that reference each other in their definitions can trip circular-type errors. As with `builder.objectRef`, you can split the declaration from the implementation. `loadableObjectRef` (and `loadableNodeRef` for Relay) take the plugin options up front so the ref can be implemented later: ```typescript const Player = builder.loadableObjectRef('Player', { load: (ids: string[], context) => context.loadPlayers(ids), }); Player.implement({ fields: (t) => ({ id: t.exposeID('id') }), }); ``` Because the ref carries the load behavior, it also works with `builder.objectType(Player, { ... })` and any other method that implements a ref, letting you layer additional behavior on the same loadable type. See [Circular references](../patterns/circular-references) for the broader pattern. ## Subscriptions [#subscriptions] Under a subscription, loaders live on the subscription's context, so values stay cached for its whole lifetime. Clear them between events with `clearAllDataLoaders`: ```typescript import { clearAllDataLoaders } from '@pothos/plugin-dataloader'; clearAllDataLoaders(context); ``` # Directives plugin URL: /docs/plugins/directives Attach schema directives to Pothos types and fields for downstream tools like graphql-tools to consume. Schema directives are annotations on your types and fields (`@rateLimit`, `@auth`, `@deprecated`) that downstream tooling reads and acts on. Pothos is code-first, so it never executes a directive itself; the directives plugin records what you declare into each type's `extensions`, where a schema transformer like [graphql-tools](https://the-guild.dev/graphql/tools) can pick it up. You declare the directives you use on the `Directives` generic, then attach them with a `directives` option anywhere the schema allows. Directives predate code-first schemas and are really an SDL-first idea. Use this plugin to keep a directive-based tool (rate limiting, auth, caching) working against a Pothos schema. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-directives ``` ```bash pnpm add @pothos/plugin-directives ``` ```bash yarn add @pothos/plugin-directives ``` ```bash bun add @pothos/plugin-directives ``` ```typescript playground example="directives-plugin" import SchemaBuilder from '@pothos/core'; import DirectivePlugin from '@pothos/plugin-directives'; interface ITeam { id: number; name: string; } const Teams = new Map([ [1, { id: 1, name: 'Comet' }], [2, { id: 2, name: 'Nova' }], ]); const builder = new SchemaBuilder<{ Directives: { rateLimit: { locations: 'OBJECT' | 'FIELD_DEFINITION'; args: { limit: number; duration: number }; }; auth: { locations: 'FIELD_DEFINITION'; args: { role: string }; }; }; }>({ plugins: [DirectivePlugin], directives: { useGraphQLToolsUnorderedDirectives: true, }, }); const Team = builder.objectRef('Team'); Team.implement({ // Object form: a map of directive name to its args. directives: { rateLimit: { limit: 60, duration: 60 }, }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` ## Declaring directives [#declaring-directives] Each entry in the `Directives` generic names a directive and describes two things: the `locations` where it may appear, and the `args` it accepts. Pothos uses this to type-check every place you apply the directive: attach `rateLimit` somewhere its `locations` don't allow, or omit an arg, and you get a compile error. The valid locations are the standard GraphQL directive locations: * `ARGUMENT_DEFINITION` * `ENUM_VALUE` * `ENUM` * `FIELD_DEFINITION` * `INPUT_FIELD_DEFINITION` * `INPUT_OBJECT` * `INTERFACE` * `OBJECT` * `SCALAR` * `SCHEMA` * `UNION` Every builder method that defines one of those locations gains a `directives` option: object types, interfaces, unions, enums and their values, input objects and their fields, scalars, fields, and args. `SCHEMA` is the one exception; it lives on `toSchema` (see [Schema directives](#schema-directives) below). ## Applying directives [#applying-directives] For fields and types, pass `directives` in the same options object you already use. Both formats below apply the same directives: ```typescript // Object form — a map of directive name to its args. directives: { rateLimit: { limit: 5, duration: 60 }, } // Array form — a list of { name, args } entries. directives: [{ name: 'rateLimit', args: { limit: 5, duration: 60 } }] ``` Prefer the array form when order matters or a directive repeats on the same location, since it preserves both. The object form is more concise and reads well when each directive appears once. The playground schema mixes them: object form on the `Team` type and its `teams` field, array form for the `auth` directive on `renameTeam`. ```typescript playground example="directives-plugin" builder.queryType({ fields: (t) => ({ teams: t.field({ type: [Team], directives: { rateLimit: { limit: 5, duration: 60 }, }, resolve: () => [...Teams.values()], }), }), }); builder.mutationType({ fields: (t) => ({ renameTeam: t.field({ type: Team, // Array form: preserves order and allows repeats. directives: [{ name: 'auth', args: { role: 'admin' } }], args: { id: t.arg.id({ required: true }), name: t.arg.string({ required: true }), }, resolve: (_root, { id, name }) => { const team = Teams.get(Number(id)); if (!team) { throw new Error(`No team with id ${id}`); } team.name = name; return team; }, }), }), }); ``` ## Schema directives [#schema-directives] `SCHEMA`-location directives attach to the schema itself, so they go on `toSchema` under `schemaDirectives`, not `directives`, which is reserved for the plugin's own options: ```typescript export const schema = builder.toSchema({ schemaDirectives: { rateLimit: { limit: 1000, duration: 60 }, }, }); ``` ## Output format [#output-format] The plugin writes the directive data onto the `extensions` of the underlying GraphQL type; it doesn't enforce the directive itself. A downstream transformer reads those extensions and implements the directive's behavior. By default the plugin uses the extensions format Gatsby uses ([described here](https://github.com/graphql/graphql-js/issues/1343#issuecomment-479871020)), which [older versions of `graphql-tools` did not support](https://github.com/ardatan/graphql-tools/issues/2534). If your directive ships a schema visitor built against an older graphql-tools (the rate-limit directive below is one), set `useGraphQLToolsUnorderedDirectives` on the builder to emit the format those visitors expect. `useGraphQLToolsUnorderedDirectives` does not preserve the order directives were declared in. That's fine for most schemas, but if a directive's behavior depends on ordering, leave it off and keep the default format. ## Wiring a real directive [#wiring-a-real-directive] Applying a directive is a two-step handoff: declare and attach it with this plugin, then run the tool's schema transformer over the built schema. `graphql-rate-limit-directive` follows that pattern: ```typescript import { rateLimitDirective } from 'graphql-rate-limit-directive'; const { rateLimitDirectiveTransformer } = rateLimitDirective(); // builder.toSchema() carries the directive data in its extensions; // the transformer reads it and installs the rate-limiting resolvers. export const schema = rateLimitDirectiveTransformer(builder.toSchema()); ``` The transformer and directive package live outside Pothos, so this step can't run in the playground sandbox, though the schema half above does. Any graphql-tools-based directive plugs in the same way. # Errors plugin URL: /docs/plugins/errors Turn thrown and returned errors into typed result unions, with per-field, per-item, and custom union handling. Some failures are part of your API: "team not found," "name already taken," "insufficient funds." The errors plugin turns those into typed result unions instead of entries in the GraphQL `errors` array, so clients can `switch` on `__typename` and render each case. You register each error class as a Pothos object type, list those classes on a field's `errors` option, and the plugin wraps the field in a union of a success type plus one member per error. This is the third layer described in [Handling errors](../patterns/handling-errors). Use it when a field's failure modes are worth spelling out in the schema. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-errors ``` ```bash pnpm add @pothos/plugin-errors ``` ```bash yarn add @pothos/plugin-errors ``` ```bash bun add @pothos/plugin-errors ``` Set `target` to `es6` or higher in your `tsconfig.json`. The plugin matches thrown errors with `instanceof`, which breaks under the default `es3` target because TypeScript rewrites the prototype chain for classes that extend `Error`. ## A field with errors [#a-field-with-errors] Add `errors: { types: [...] }` to any field. The resolver throws as usual; the plugin catches instances of the listed classes and resolves them to their object type. ```typescript playground example="errors-plugin" const builder = new SchemaBuilder({ plugins: [ErrorsPlugin], errors: { defaultTypes: [], // onResolvedError: (error) => console.error('Handled error:', error), }, }); const Team = builder.objectRef('Team').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.objectType(Error, { name: 'Error', fields: (t) => ({ message: t.exposeString('message'), }), }); builder.queryType({ fields: (t) => ({ team: t.field({ type: Team, errors: { types: [Error], }, args: { id: t.arg.id({ required: true }), }, resolve: (_parent, { id }) => { const team = Teams.get(Number(id)); if (!team) { throw new Error(`No team with id ${id}`); } return team; }, }), }), }); ``` The `team` field no longer returns `Team` directly. The plugin replaces its type with a generated union: ```graphql type Team { id: ID! name: String! } type Error { message: String! } type Query { team(id: ID!): QueryTeamResult } union QueryTeamResult = Error | QueryTeamSuccess type QueryTeamSuccess { data: Team! } ``` The successful value moves under a `data` field on a generated `QueryTeamSuccess` type; each error class becomes a sibling union member. Clients select the branch they need with inline fragments: ```graphql query { team(id: "1") { __typename ... on QueryTeamSuccess { data { id name } } ... on Error { message } } } ``` Prefer throwing: a thrown value is untyped, so the resolver's return type stays `Team`. The plugin also unwraps errors you *return* rather than throw, but returning an `Error` widens the resolver's return type. Save the return form for [`errorUnion`](#manual-error-unions) and [`errorUnionField`](#custom-error-unions) below, which type the errors into the signature. ## A shared Error interface [#a-shared-error-interface] Listing raw `Error` on every field works, but it forces clients to know each concrete type. To avoid that, define an `Error` interface that every error implements, plus a base type in `defaultTypes` so it is merged into every field automatically. Clients can then always fall back to `... on Error { message }` and add narrower fragments only for the errors they render specifically, which keeps adding new error types from breaking existing queries. ```typescript playground example="errors-plugin-step-2" const builder = new SchemaBuilder({ plugins: [ErrorsPlugin], errors: { // BaseError is merged into every field that opts into error handling. defaultTypes: [Error], }, }); // One interface every error type implements. Clients can always fall back // to `... on Error { message }` and add narrower fragments when they care. const ErrorInterface = builder.interfaceRef('Error').implement({ fields: (t) => ({ message: t.exposeString('message'), }), }); // The catch-all error, registered in defaultTypes above. builder.objectType(Error, { name: 'BaseError', interfaces: [ErrorInterface], }); class NotFoundError extends Error { constructor(public readonly id: string) { super(`No team with id ${id}`); this.name = 'NotFoundError'; } } builder.objectType(NotFoundError, { name: 'NotFoundError', interfaces: [ErrorInterface], fields: (t) => ({ id: t.exposeString('id'), }), }); ``` A field opts into just the defaults with `errors: {}`, or adds its own on top; `errors: { types: [NotFoundError] }` handles both `NotFoundError` and the default `BaseError`. Because both implement the `Error` interface, one `... on Error { message }` fragment covers every branch. ## Field options [#field-options] The `errors` option on a field accepts: | Option | Purpose | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `types` | Error classes to catch on this field. Merged with the builder's `defaultTypes`. | | `result` | Options for the generated success object type. Standard object-type options plus a `name` to rename it. | | `dataField` | Options for the success type's payload field. Standard field options plus a `name` (default `data`). | | `union` | Options for the generated union type. Standard union options plus a `name`. | | `directResult` | Non-list fields only. Puts the field's own object type into the union directly instead of wrapping it in a `…Success` type. Throws at build time if the field type is not an object type. | For example, `directResult: true` flattens `QueryTeamResult = Error | QueryTeamSuccess` into `QueryTeamResult = Error | Team`, dropping the `data` indirection when the payload is already an object type. ## Builder options [#builder-options] Pass an `errors` object when constructing the builder to set defaults for every field: | Option | Purpose | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `defaultTypes` | Error classes included on every field that uses `errors` or `itemErrors`. | | `directResult` | Default for the field-level `directResult` option (non-list fields only). | | `onResolvedError` | Called with each error the plugin handles; the hook for logging or metrics. It never fires for errors the plugin doesn't catch. | | `defaultResultOptions` | Defaults for every generated success type, including a `name` function to control naming. | | `defaultUnionOptions` | Defaults for every generated union type, including a `name` function. | | `defaultItemResultOptions` | Like `defaultResultOptions`, but for the per-item success types created by [`itemErrors`](#per-item-list-errors). | | `defaultItemUnionOptions` | Like `defaultUnionOptions`, but for the per-item union types created by `itemErrors`. | | `unsafelyHandleInputErrors` | Lets the plugin catch errors thrown during argument validation. See [Validation errors](#validation-errors); it has security implications. | The `name` functions receive `{ parentTypeName, fieldName }` and return the generated type name, so you can rename every result and union type in one place: ```typescript const builder = new SchemaBuilder({ plugins: [ErrorsPlugin], errors: { defaultTypes: [Error], defaultResultOptions: { name: ({ fieldName }) => `${fieldName}Success`, }, defaultUnionOptions: { name: ({ fieldName }) => `${fieldName}Result`, }, }, }); ``` ## Per-item list errors [#per-item-list-errors] For a list field, a single failure normally nulls or errors the whole list. Use `itemErrors` instead of `errors` to wrap *each item* in its own union, so one bad row surfaces in place while the rest resolve. The options are identical to `errors`; they apply per item. Return an `Error` (or a listed subclass) in the array to mark that slot as failed. That return works at runtime, but `itemErrors` reuses the plain field-option types, so the resolver's return type stays `ITeam[]`, and strict TypeScript may need a cast or a widened return annotation, the same throw-vs-return trade-off as field-level [`errors`](#a-field-with-errors). ```typescript builder.queryType({ fields: (t) => ({ standings: t.field({ type: [Team], itemErrors: {}, resolve: () => [ { id: 1, name: 'Comet' }, new Error('Aurora withdrew'), { id: 3, name: 'Vertex' }, ], }), }), }); ``` ```graphql type Query { standings: [QueryStandingsItemResult!]! } union QueryStandingsItemResult = Error | QueryStandingsItemSuccess type QueryStandingsItemSuccess { data: Team! } ``` `itemErrors` also works with sync and async iterators (with `graphql@>=17` or any executor that supports the `@stream` directive). A yielded error becomes an error item; if the generator itself throws, that error is added as the final item and no further results are produced for the field: ```typescript builder.queryType({ fields: (t) => ({ liveScores: t.field({ type: ['Int'], itemErrors: {}, resolve: async function* () { yield 3; yield 5; yield new Error('feed dropped'); yield 8; }, }), }), }); ``` Combine `errors` and `itemErrors` to handle both a failure of the whole field and failures of individual items. The field gets an outer result union whose success payload is itself a list of per-item unions: ```typescript builder.queryType({ fields: (t) => ({ standings: t.field({ type: [Team], errors: {}, itemErrors: {}, resolve: () => [{ id: 1, name: 'Comet' }, new Error('Aurora withdrew')], }), }), }); ``` ```graphql type Query { standings: QueryStandingsResult! } union QueryStandingsResult = Error | QueryStandingsSuccess type QueryStandingsSuccess { data: [QueryStandingsItemResult!]! } union QueryStandingsItemResult = Error | QueryStandingsItemSuccess type QueryStandingsItemSuccess { data: Team! } ``` ## Custom error unions [#custom-error-unions] When a field has more than one success type, `t.errorUnionField` (and `t.errorUnionListField` for lists) lets you spell out every union member yourself, mixing success types and error types. Errors are *returned* here, so they're part of the resolver's typed return: ```typescript const CreateResult = builder.objectRef<{ id: string; created: true }>('CreateResult').implement({ isTypeOf: (obj) => 'created' in obj, fields: (t) => ({ id: t.exposeString('id'), created: t.exposeBoolean('created'), }), }); const UpdateResult = builder.objectRef<{ id: string; updated: true }>('UpdateResult').implement({ isTypeOf: (obj) => 'updated' in obj, fields: (t) => ({ id: t.exposeString('id'), updated: t.exposeBoolean('updated'), }), }); builder.mutationType({ fields: (t) => ({ registerTeam: t.errorUnionField({ types: [CreateResult, UpdateResult, ValidationError], resolve: (_parent, { name, action }) => { if (name.length < 3) return new ValidationError('Name too short'); if (action === 'create') return { id: '123', created: true }; return { id: '123', updated: true }; }, }), processRegistrations: t.errorUnionListField({ types: [CreateResult, UpdateResult, ValidationError], resolve: (_parent, { operations }) => operations.map((op) => op.invalid ? new ValidationError('Invalid') : op.action === 'create' ? { id: op.id, created: true } : { id: op.id, updated: true }, ), }), }), }); ``` Union members are resolved with standard Pothos type resolution, so you have three ways to tell the members apart: * **Class-based types** resolve automatically via `instanceof`. Most error types fall here; `builder.objectType(ValidationError, …)` needs no `isTypeOf`. * **`isTypeOf`** discriminates plain object types, as `CreateResult` and `UpdateResult` do above. * **A custom `resolveType`** on the `union` option handles anything more involved. It runs after the plugin's internal error lookup: ```typescript t.errorUnionField({ types: [CreateResult, UpdateResult, ValidationError], union: { resolveType: (value) => { if (value instanceof ValidationError) return 'ValidationError'; if ('created' in value) return 'CreateResult'; return 'UpdateResult'; }, }, resolve: () => { /* ... */ }, }); ``` ## Manual error unions [#manual-error-unions] `builder.errorUnion` builds a reusable union type up front, for several fields that share the same success-or-error shape. Any field that returns it automatically handles both returned and thrown errors. ```typescript const TeamResult = builder.errorUnion('TeamResult', { types: [Team, NotFoundError, ValidationError], }); builder.queryField('team', (t) => t.field({ type: TeamResult, args: { id: t.arg.string({ required: true }) }, resolve: (_parent, { id }) => { // thrown errors are wrapped if (!id) throw new ValidationError('id required', 'id'); // returned errors are wrapped too if (id === 'unknown') return new NotFoundError('team not found'); return { id, name: 'Comet' }; }, }), ); ``` `errorUnion` accepts: * `types`: the member types (object refs, error classes, and so on). * `omitDefaultTypes`: set `true` to exclude the builder's `defaultTypes` from this union (default `false`). * `resolveType`: an optional custom resolver, called after the plugin's internal error-map check. * Every other standard union type option. ## Working with other plugins [#working-with-other-plugins] ### Validation errors [#validation-errors] The [validation plugin](./validation) throws before your resolver runs, so those errors bypass `errors` by default. Enabling `unsafelyHandleInputErrors` lets the plugin catch them and return structured results for validation failures. `unsafelyHandleInputErrors` wraps errors at a higher level than field resolution, which means it runs *before* field auth checks, so a request that fails validation returns a typed error without those auth checks running. Enable it only when you understand that trade-off. With it enabled, define a type for your validation error the same way you would any other, then list it on the field: ```typescript const InputValidationIssue = builder .objectRef('InputValidationIssue') .implement({ fields: (t) => ({ message: t.exposeString('message'), path: t.stringList({ resolve: (issue) => issue.path?.map((p) => String(p)), }), }), }); builder.objectType(InputValidationError, { name: 'InputValidationError', interfaces: [ErrorInterface], fields: (t) => ({ issues: t.field({ type: [InputValidationIssue], resolve: (err) => err.issues, }), }), }); builder.queryField('registerTeam', (t) => t.boolean({ errors: { types: [InputValidationError], }, args: { name: t.arg.string({ validate: z.string().min(3, 'Too short'), }), }, resolve: () => true, }), ); ``` ### Dataloader [#dataloader] List the errors plugin **before** the [dataloader plugin](./dataloader). A field whose `errors` returns a `loadableObject` or `loadableNode` will then catch errors thrown while loading the ids returned by `resolve`. List fields are the exception: errors while loading objects from a list of ids are associated with each item, not the field, so the plugin does not wrap them. A future dataloader option may raise a field-level error when any item fails to load, which would let the errors plugin handle that case too. ### Prisma [#prisma] List the errors plugin **before** the [Prisma plugin](./prisma) so `errors` works with every Prisma field-builder method. You can put `errors` on any field, but an error while pre-loading a relation always surfaces on the field that ran the query. Some relations fall back to their own query, so those fields may still error if the relation wasn't pre-loaded. Nested-relation detection keeps working when those relations use the errors plugin themselves. # Federation plugin URL: /docs/plugins/federation Turn a Pothos schema into an Apollo Federation 2 subgraph, with entities, external-type extensions, and composable subgraph output. Federation lets you split one graph across services. The federation plugin turns a Pothos schema into an Apollo Federation 2 subgraph: you mark object types as *entities*, describe how each is loaded by reference, and emit a subgraph schema the gateway can compose. This page covers the Pothos API; for what the federation terms mean, see the [Apollo docs](https://www.apollographql.com/docs/federation/v2/). Examples use an Ultimate League graph split across subgraphs: a `teams` service that owns `Team`, a `roster` service that extends `Player`, and a `ratings` service that references both. ## Install [#install] The plugin needs the [directives plugin](./directives) and `@apollo/subgraph` alongside it: npm pnpm yarn bun ```bash npm install --save @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph ``` ```bash pnpm add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph ``` ```bash yarn add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph ``` ```bash bun add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph ``` Add `@apollo/server` too if you serve the subgraph with Apollo; it is not required if you run a different server: npm pnpm yarn bun ```bash npm install --save @apollo/server ``` ```bash pnpm add @apollo/server ``` ```bash yarn add @apollo/server ``` ```bash bun add @apollo/server ``` ## Setup [#setup] List the directives plugin before the federation plugin. If you use resolver-wrapping plugins like [scope-auth](./scope-auth), the federation plugin should come *after* them so it sees the final resolvers: ```typescript import SchemaBuilder from '@pothos/core'; import DirectivesPlugin from '@pothos/plugin-directives'; import FederationPlugin from '@pothos/plugin-federation'; const builder = new SchemaBuilder({ plugins: [DirectivesPlugin, FederationPlugin], }); ``` ## Defining entities [#defining-entities] An entity is an object type another service can reference or extend. Defining one is two steps: declare the object type as you normally would with `objectRef`, then promote it with `builder.asEntity` by giving it a `key` and a `resolveReference`. ```typescript const TeamType = builder.objectRef('Team').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), city: t.exposeString('city'), }), }); builder.asEntity(TeamType, { key: builder.selection<{ id: string }>('id'), resolveReference: ({ id }) => teams.find((team) => team.id === id), }); ``` Keys are built with `builder.selection`, which **must** be called with a generic argument spelling out the types of every field in the key. `key` also accepts an array when an entity has more than one key. `resolveReference` receives an object of the key's shape and returns the backing model; the gateway calls it whenever another service references this `Team` by key. Type the key with the scalar shapes your server *produces*, not the shapes your resolvers return. Apollo Server serializes every `ID` to a `string`, so a key over an `id` field is `selection<{ id: string }>('id')` even if your data holds `id` as a number. ## Extending external entities [#extending-external-entities] To add fields to an entity owned by *another* service, call `builder.externalRef` and then `implement` the returned ref. `externalRef` takes the entity name, a key selection, and a resolver that loads the local data for a given key. The resolver's return type becomes the backing model (the `parent` your added fields resolve against), and the key describes which fields the gateway selects from the owning service to build that `parent`. ```typescript const PlayerRef = builder.externalRef( 'Player', builder.selection<{ id: string }>('id'), (entity) => { const stats = playerStats.find(({ id }) => id === entity.id); // extend the referenced key with data this service owns return stats && { ...entity, ...stats }; }, ); PlayerRef.implement({ // external fields let `requires`/`provides` reference data owned elsewhere externalFields: (t) => ({ salary: t.int(), seasons: t.int(), }), fields: (t) => ({ id: t.exposeID('id'), goals: t.exposeInt('goals'), contractValue: t.int({ // pull external fields into this resolver with a `requires` directive; // they arrive as the first resolver arg requires: builder.selection<{ salary?: number; seasons?: number }>('salary seasons'), resolve: (data) => (data.salary ?? 0) * (data.seasons ?? 0), }), }), }); ``` Fields listed under `externalFields` are declared as `@external`; they exist on the entity elsewhere and are only referenced here, by `requires` (above) or `provides` (below). A field's `requires` selection then makes those values available as the first resolver argument. To mark a key's external field as non-resolvable (`resolvable: false`), wrap the selection with `builder.keyDirective`: ```typescript const PlayerRef = builder.externalRef( 'Player', builder.keyDirective(builder.selection<{ id: string }>('id'), false), ); ``` ## Adding a provides directive [#adding-a-provides-directive] `@provides` lets a field promise that its result already carries certain fields of a referenced entity, so the gateway skips a round trip. Implement the referenced type as an external ref that lists the provided field under `externalFields`, then set the field's type to `Ref.provides('...')` instead of the bare ref. The generic works like `builder.selection`, and using `.provides` both emits the annotation and ensures the resolved value includes the provided data. ```typescript const TeamType = builder.externalRef('Team', builder.selection<{ id: string }>('id')).implement({ externalFields: (t) => ({ // the field this service will provide name: t.string(), }), fields: (t) => ({ id: t.exposeID('id'), }), }); const RatingType = builder.objectRef('Rating'); RatingType.implement({ fields: (t) => ({ id: t.exposeID('id'), score: t.exposeInt('score'), team: t.field({ // TeamType.provides<...> annotates the field and requires the resolved // value to include the provided `name` type: TeamType.provides<{ name: string }>('name'), resolve: (rating) => ({ id: rating.teamID, name: teamNames.find((team) => team.id === rating.teamID)!.name, }), }), }), }); ``` The provided field must be one of the external ref's `externalFields`; you can only provide what the entity declares. ## Field and type directives [#field-and-type-directives] Several federation directives are plain options on a field or type definition rather than separate API calls: ```typescript t.field({ type: 'String', shareable: true, tag: ['public'], inaccessible: true, override: { from: 'roster' }, }); ``` | Option | Directive | Applies to | | ---------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | `shareable` | `@shareable` | fields, object types | | `tag` | `@tag` | fields, object types, input fields | | `inaccessible` | `@inaccessible` | fields, types, input fields, enum values | | `override` | `@override`; `{ from, label? }` names the service being overridden | fields | | `authenticated` | `@authenticated` | fields, object/interface/scalar/enum types | | `requiresScopes` | `@requiresScopes` | same as `authenticated` | | `policy` | `@policy` | same as `authenticated` | | `cost` | `@cost` | fields, object/scalar/enum types, input fields, enum values | | `listSize` | `@listSize` with `{ assumedSize?, slicingArguments?, sizedFields?, requireOneSlicingArgument? }` | fields | `requiresScopes` and `policy` take a nested array (`string[][]`) of scopes/policies. Type them by setting the `FederationScopes` and `FederationPolicies` generics on the builder. See the official Federation docs for each directive's semantics. ## Interface entities and @interfaceObject [#interface-entities-and-interfaceobject] Federation 2.3 added interface entities. Pass an interface ref to `asEntity` to give an interface a key: ```typescript const Media = builder.interfaceRef<{ id: string }>('Media').implement({ fields: (t) => ({ id: t.exposeID('id'), // ...shared fields }), }); builder.asEntity(Media, { key: builder.selection<{ id: string }>('id'), resolveReference: ({ id }) => loadMediaById(id), }); ``` To add fields to every implementor of an interface owned by another subgraph, define an `@interfaceObject`: declare it as an object type (not an interface) and set `interfaceObject: true` on `asEntity`. ```typescript const Media = builder.objectRef<{ id: string }>('Media').implement({ fields: (t) => ({ id: t.exposeID('id'), // new fields here apply to every implementor of Media }), }); builder.asEntity(Media, { interfaceObject: true, key: builder.selection<{ id: string }>('id'), resolveReference: (ref) => ref, }); ``` ## Building the subgraph schema [#building-the-subgraph-schema] Call `builder.toSubGraphSchema` instead of `toSchema`; it adds the federation-specific `_entities`/`_service` queries and the `@link` directive. `linkUrl` defaults to `https://specs.apollo.dev/federation/v2.6`; `federationDirectives` defaults to the set of directives your schema actually uses. ```typescript import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; const schema = builder.toSubGraphSchema({ // override the federation version if you need an older one linkUrl: 'https://specs.apollo.dev/federation/v2.3', // usually left to default — the plugin infers this from your schema federationDirectives: ['@key', '@external', '@requires', '@provides'], }); const server = new ApolloServer({ schema }); startStandaloneServer(server, { listen: { port: 4000 } }) .then(({ url }) => { console.log(`🚀 Subgraph ready at ${url}`); }) .catch((error) => { throw error; }); ``` For a runnable example that composes several Pothos subgraphs into one supergraph, see [the federation test example](https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example). If you print the schema to a string (for Managed Federation or manual composition with `rover`), use `printSubgraphSchema` from `@apollo/subgraph`. The default graphql-js printer drops the directives federation relies on, so a plainly-printed schema will not compose. ## composeDirective [#composedirective] To preserve a custom directive through composition, pass `composeDirectives` to `toSubGraphSchema`. It needs a matching `@link` (via `schemaDirectives`) pointing at your directive's spec, plus a real `GraphQLDirective` implementation: ```typescript import { DirectiveLocation, GraphQLDirective } from 'graphql'; export const schema = builder.toSubGraphSchema({ // emits @composeDirective(name: "@custom") composeDirectives: ['@custom'], // @composeDirective requires an @link to your directive's URL schemaDirectives: { link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] }, }, // and an actual implementation of the directive directives: [ new GraphQLDirective({ name: 'custom', locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], }), ], }); ``` # Grafast plugin URL: /docs/plugins/grafast Build fields with Grafast plans instead of resolvers, and resolve interfaces and unions with planType. [Grafast](https://grafast.org/grafast/) is a planning-based GraphQL executor: instead of a resolver per field, you write a *plan* that Grafast optimizes across the whole operation before running it. This plugin swaps the Pothos field builder from `resolve` to `plan`, so every field you define expects a plan. You still define objects, interfaces, and unions the same way, with `builder.objectRef` and friends; only the field bodies change. This package is experimental and will have breaking changes in the near future. This plugin does not work with most other Pothos plugins. Many plugins add runtime behavior by wrapping resolvers, and a plan-based field has no resolver to wrap. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-grafast grafast@>=0.1.1-beta.24 ``` ```bash pnpm add @pothos/plugin-grafast grafast@>=0.1.1-beta.24 ``` ```bash yarn add @pothos/plugin-grafast grafast@>=0.1.1-beta.24 ``` ```bash bun add @pothos/plugin-grafast grafast@> 0.1.1-beta.24 ``` ## Setup [#setup] Two things distinguish a Grafast build. `InferredFieldOptionsKind: 'Grafast'` tells the builder to expect `plan` on fields instead of `resolve`, and Grafast reads its context from a global `Grafast.Context` namespace, so you declare your context type in both places. ```typescript import SchemaBuilder from '@pothos/core'; import GrafastPlugin from '@pothos/plugin-grafast'; interface LeagueContext { // request-scoped values your plans read from } declare global { namespace Grafast { interface Context extends LeagueContext {} } } const builder = new SchemaBuilder<{ // Expect plans instead of resolvers. InferredFieldOptionsKind: 'Grafast'; Context: LeagueContext; }>({ plugins: [GrafastPlugin], }); ``` ## Adding plans to fields [#adding-plans-to-fields] A field's `plan` receives step versions of its arguments (each prefixed with `$`) and returns a step. Combine steps with helpers like `lambda` to compute a value. See the [Grafast documentation](https://grafast.org/grafast/) for the full catalog of steps. ```typescript import { lambda } from 'grafast'; builder.queryType({ fields: (t) => ({ pointDifferential: t.int({ args: { scored: t.arg.int({ required: true }), conceded: t.arg.int({ required: true }), }, plan: (_, { $scored, $conceded }) => lambda([$scored, $conceded], ([scored, conceded]) => scored - conceded), }), }), }); ``` ## Using resolvers [#using-resolvers] You can still write a plain `resolve`, but the plan-based executor does not pass the fourth `GraphQLResolveInfo` argument, so a resolver here sees only `parent`, `args`, and `context`. ```typescript builder.queryType({ fields: (t) => ({ pointDifferential: t.int({ args: { scored: t.arg.int({ required: true }), conceded: t.arg.int({ required: true }), }, resolve: (_, { scored, conceded }) => scored - conceded, }), }), }); ``` Don't use a resolver to load data; that defeats the point of planning. Use one only when it reads more clearly than a field that would otherwise be a one-line `lambda` plan. ## Abstract types [#abstract-types] Interfaces and unions usually need a plan to resolve an incoming record to its concrete type. You attach that plan with `.withPlan` on the ref, whose `planType` returns a `$__typename` step naming the concrete member. For the deeper model behind these plans, see the [Grafast polymorphism docs](https://grafast.org/grafast/polymorphism). ### Interfaces [#interfaces] Declare the interface, attach a plan that reads the type name off the record, then implement it. Object types join the interface the usual way through `interfaces`. ```typescript import { get, loadOne } from 'grafast'; interface MemberData { id: string; kind: 'Player' | 'Coach'; } // The plan resolves each record to its concrete type via the `kind` field. const Member = builder.interfaceRef('Member').withPlan({ planType: ($record) => ({ $__typename: get($record, 'kind'), }), }); Member.implement({ fields: (t) => ({ id: t.exposeID('id'), }), }); export const Player = builder.objectRef('Player').implement({ interfaces: [Member], }); export const Coach = builder.objectRef('Coach').implement({ interfaces: [Member], }); ``` A field returning the interface loads the record; the interface's own plan takes it from there: ```typescript const members = [ { id: '1', kind: 'Player' }, { id: '2', kind: 'Coach' }, ] satisfies MemberData[]; function getMembersById(ids: readonly string[]): (MemberData | null)[] { return ids.map((id) => members.find((m) => m.id === id) ?? null); } builder.queryFields((t) => ({ member: t.field({ type: Member, args: { id: t.arg.string({ required: true }), }, plan: (_, $args) => loadOne($args.$id, getMembersById), }), })); ``` ### Unions [#unions] A union works the same way: build it with `builder.unionType`, then attach the resolving plan with `.withPlan`: ```typescript interface SponsorData { id: string; kind: 'Sponsor'; } export const Sponsor = builder.objectRef('Sponsor').implement({ fields: (t) => ({ id: t.exposeID('id'), }), }); export const Entity = builder .unionType('Entity', { types: [Player, Coach, Sponsor], }) .withPlan({ planType: ($record) => ({ $__typename: get($record, 'kind'), }), }); ``` ### Loading records with `planForType` [#loading-records-with-planfortype] If the field that returns an abstract type has only an id to work with, let the type's plan do the loading. Give `planType` an explicit specifier step, load the record inside the plan, and return a `planForType` that hands each concrete type its data. Fields returning the type then only need to produce the id. `planForType` is not fully type-safe; it will accept plans that resolve to data for the wrong type. This API is likely to change. ```typescript import { get, inhibitOnNull, loadOne, type Step } from 'grafast'; type EntityData = MemberData | SponsorData; const entities = [ { id: '1', kind: 'Player' }, { id: '2', kind: 'Coach' }, { id: '3', kind: 'Sponsor' }, ] satisfies EntityData[]; function getEntitiesById(ids: readonly string[]): (EntityData | null)[] { return ids.map((id) => entities.find((e) => e.id === id) ?? null); } export const Entity = builder .unionType('Entity', { types: [Player, Coach, Sponsor], }) .withPlan({ planType: ( // An explicit specifier type lets query fields return just the id. $specifier: Step, ) => { const $record = inhibitOnNull(loadOne($specifier, getEntitiesById)); return { $__typename: get($record, 'kind'), planForType: () => $record, }; }, }); builder.queryFields((t) => ({ entity: t.field({ type: Entity, args: { id: t.arg.string({ required: true }), }, // The Entity plan loads the record, so this returns only the id. plan: (_, $args) => $args.$id, }), })); ``` ## Plan reference [#plan-reference] `.withPlan` is available on interface, union, and object refs. The plan object it takes accepts: | Key | Where | Purpose | | ------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `planType` | interface / union / object | Returns the plan for the type. On abstract types it returns `{ $__typename }`, plus `planForType`, which is optional when the plan's specifier is the full record type but required when `planType` takes a narrowed specifier (an id, as in the `planForType` example above); on object types it returns the step Pothos hands to the type's fields. | | `$__typename` | returned from abstract `planType` | Step resolving to the concrete member's type name. | | `planForType` | returned from abstract `planType` | Loads the record for the resolved concrete type, so fields returning the abstract type can return only a specifier. | | `toSpecifier` | interface / union | Transforms the source step into the specifier step passed to `planType`. | | `assertStep` | object | Asserts the step passed to the object's `planType`, either a Step subclass or an assertion function. | # Plugins URL: /docs/plugins List of plugins for Pothos {/* The plugin catalog at /docs/plugins is served by the app route `app/(docs)/docs/plugins/page.tsx`, which renders the rich, categorized `PluginsPage` from `components/plugins/plugins.ts`. Next.js resolves that static route ahead of this MDX catch-all, so nothing authored here would ever render — this file exists only so fumadocs keeps a clickable "Plugins" node in the sidebar that points at /docs/plugins. Do not re-add a card grid; edit `components/plugins/plugins.ts` (the single source of truth) instead. */} # Mocks plugin URL: /docs/plugins/mocks Replace field resolvers with inline mocks at schema-build time for tests and frontend development. A field's resolver isn't always ready when you need to query it: the data feed isn't wired up yet, or a test needs a deterministic value instead of a live one. The mocks plugin swaps a resolver out at schema-build time. You pass a `mocks` map to `builder.toSchema`, keyed by type name then field name, and the plugin replaces the matching field's resolver with your function wherever it runs. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-mocks ``` ```bash pnpm add @pothos/plugin-mocks ``` ```bash yarn add @pothos/plugin-mocks ``` ```bash bun add @pothos/plugin-mocks ``` Add the plugin to the builder. It contributes no field-builder methods; the whole surface is the `mocks` option on `builder.toSchema`. ```typescript import MocksPlugin from '@pothos/plugin-mocks'; const builder = new SchemaBuilder({ plugins: [MocksPlugin], }); ``` ## Mocking a field [#mocking-a-field] Define your schema as usual. Any field whose resolver isn't ready can throw, and the mock will stand in for it. Here the `standings` query and the `Team.form` field both throw in their real resolvers, and the schema is only queryable because the `mocks` map replaces them. ```typescript playground example="mocks-plugin" const Team = builder.objectRef('Team').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), wins: t.exposeInt('wins'), // The recent-form feed isn't built yet, so the real resolver throws. form: t.string({ resolve: () => { throw new Error('form not implemented'); }, }), }), }); builder.queryType({ fields: (t) => ({ // The live standings feed isn't wired up yet — the resolver throws until it is. standings: t.field({ type: [Team], resolve: () => { throw new Error('standings feed not implemented'); }, }), }), }); export const schema = builder.toSchema({ mocks: { Query: { standings: () => [ { id: 1, name: 'Comet', wins: 9 }, { id: 2, name: 'Aurora', wins: 7 }, { id: 3, name: 'Vertex', wins: 6 }, ], }, Team: { form: () => 'WWLWD', }, }, }); ``` The key path is `mocks[typeName][fieldName]`. `Query.standings` mocks a root field; `Team.form` mocks a field on an object type, exactly the same way; mocks aren't limited to the root types. A mock receives the standard resolver arguments, so it can read `parent`, `args`, `context`, and `info`. The plugin types `parent` as `unknown`, so cast it to the field's backing model before reading properties: ```typescript mocks: { Team: { form: (parent, args, context, info) => ((parent as ITeam).wins > 7 ? 'WWWWW' : 'WLWDW'), }, } ``` Fields you don't list keep their real resolver, so you can mock a single not-yet-built field and leave the rest of the schema live. ## Rebuilding with different mocks [#rebuilding-with-different-mocks] Mocks live in the `toSchema` call, not the field definition, so the same builder produces different schemas depending on what you pass. Build once with mocks for a test, once without for production, or build a fresh schema per test case with the exact values that case needs: ```typescript // Production: real resolvers run. export const schema = builder.toSchema(); // A test: pin the values this case asserts on. const mockedSchema = builder.toSchema({ mocks: { Query: { standings: () => [{ id: 1, name: 'Comet', wins: 9 }], }, }, }); ``` ## Mocking subscribe [#mocking-subscribe] A mock can be a plain function (which replaces `resolve`) or an object with both `resolve` and `subscribe` keys, for mocking a subscription field's event source. The types expect both keys (the runtime tolerates a missing one, but supply both to match the types). Nest them to feed a subscription a canned async iterator: ```typescript builder.subscriptionType({ fields: (t) => ({ scoreUpdates: t.int({ resolve: (score) => score, subscribe: () => { throw new Error('score feed not implemented'); }, }), }), }); builder.toSchema({ mocks: { Subscription: { scoreUpdates: { resolve: (parent, args, context, info) => parent, subscribe: async function* () { yield 1; yield 2; yield 3; }, }, }, }, }); ``` ## Mock shapes [#mock-shapes] Each entry under `mocks[typeName][fieldName]` is one of: | Value | Replaces | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | A function `(parent, args, context, info) => value` | The field's `resolve`. | | `{ resolve, subscribe }` | The field's `resolve` and `subscribe`; the object form takes both keys, for subscription fields whose event source you want to mock. | # Relay plugin URL: /docs/plugins/relay Build a Relay-compliant schema with the Node interface, global IDs, connections, and Relay mutations. Relay is a set of GraphQL conventions: a `Node` interface every object can be refetched through, opaque global IDs, and cursor-paginated connections. The relay plugin adds the builder methods that generate these: `builder.node` turns a type into a node addressable by a global ID, and `t.connection` defines a cursor-paginated field along with its `Connection`, `Edge`, and `PageInfo` types. The examples below model an Ultimate League: `Team` and `Player` nodes, with each team's roster exposed as a connection. ## Setup [#setup] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-relay ``` ```bash pnpm add @pothos/plugin-relay ``` ```bash yarn add @pothos/plugin-relay ``` ```bash bun add @pothos/plugin-relay ``` Register the plugin and pass a `relay` options object; empty is fine to start. ```typescript playground example="relay-plugin" import SchemaBuilder from '@pothos/core'; import RelayPlugin, { resolveArrayConnection } from '@pothos/plugin-relay'; const builder = new SchemaBuilder({ plugins: [RelayPlugin], relay: {}, }); ``` The builder key is `relay`. Older versions used `relayOptions`; that name is only accepted in v3-compatibility mode, and new code should use `relay`. ## Nodes [#nodes] A node is any object reachable by a single opaque global ID. Call `builder.node` to turn a type into one: you point at an `objectRef` (or a class), define how to read its `id`, and supply a loader that hydrates a node from that id. ```typescript playground example="relay-plugin" const Team = builder.objectRef('Team'); builder.node(Team, { id: { resolve: (team) => team.id, }, loadOne: (id) => Teams.get(id) ?? null, loadMany: (ids) => ids.map((id) => Teams.get(id) ?? null), fields: (t) => ({ name: t.exposeString('name'), city: t.exposeString('city'), }), }); ``` `builder.node` creates an object type that implements the `Node` interface, and creates the `Node` interface itself the first time it's used. The `id.resolve` function returns a string or number, which the plugin encodes into a global ID. It also wires up two query fields (`node(id:)` and `nodes(ids:)`) that refetch any node directly from its global ID by calling your loader. Define exactly **one** of the loader methods: * `loadOne` / `loadMany` load a node (or list of nodes) by id, with a per-request cache so the same id is only loaded once. * `loadWithoutCache` / `loadManyWithoutCache` skip the cache. Use these if caching is undesirable or you already load through a dataloader. When you back a node with a **class** rather than an `objectRef`, pass a `name` so the type has a GraphQL name: ```typescript class Player { id: string; name: string; } builder.node(Player, { name: 'Player', // required when the type parameter is a class id: { resolve: (player) => player.id }, loadOne: (id) => loadPlayer(id), fields: (t) => ({ name: t.exposeString('name') }), }); ``` ### Resolving the node type [#resolving-the-node-type] The `node` field returns the `Node` interface, so Pothos needs to map a loaded object back to its concrete type. By default (`brandLoadedObjects: true`) any object returned from a `load*` method is tagged with a hidden symbol that the default `resolveType` reads, so most schemas never write an `isTypeOf` check. You still need `isTypeOf` for `union` and `interface` fields that return manually-loaded node objects where no custom `resolveType` knows the type. A node may also define its own `isTypeOf`: ```typescript builder.node(Player, { isTypeOf: (value) => value instanceof Player, // ... }); ``` When the type parameter is a class, `isTypeOf` defaults to an `instanceof` check (falling back to the prototype's constructor), so class-backed nodes often need nothing here, though declaring it explicitly is clearer. Turning off `brandLoadedObjects` means the default `resolveType` can no longer identify loaded nodes. Only disable it if every node defines its own `isTypeOf`. ### Parsing node ids [#parsing-node-ids] Node ids arrive as strings. Supply a `parse` function on the `id` field to convert them (say, to a number) before they reach your loader: ```typescript builder.node(Player, { id: { resolve: (player) => player.id, parse: (id) => Number.parseInt(id, 10), }, // `id` is now a number in loadOne loadOne: (id) => loadPlayerByNumber(id), fields: (t) => ({ name: t.exposeString('name') }), }); ``` ## Global IDs [#global-ids] Global IDs let a client refetch anything by a single opaque string. The plugin adds field and argument builders for them. `t.globalID` and `t.globalIDList` produce fields whose resolver returns either a global ID string or an object with `id` and `type` (a type name, or any ref usable as a type parameter): ```typescript import { encodeGlobalID } from '@pothos/plugin-relay'; builder.queryFields((t) => ({ featuredTeamId: t.globalID({ resolve: () => ({ id: 1, type: 'Team' }), }), rosterIds: t.globalIDList({ resolve: () => [{ id: 1, type: 'Player' }], }), })); ``` On the input side, `t.arg.globalID` and `t.arg.globalIDList` accept a global ID string from the client and hand your resolver a decoded `{ id, typename }`: ```typescript builder.queryFields((t) => ({ roster: t.field({ type: [Player], args: { teamId: t.arg.globalID({ required: true }), extra: t.arg.globalIDList(), }, resolve: (_parent, args) => { console.log(`type ${args.teamId.typename}, id ${args.teamId.id}`); return loadRoster(args.teamId.id); }, }), })); ``` Restrict which node types an argument accepts with `for`, either a single ref or an array: ```typescript teamId: t.arg.globalID({ for: Team, // or [Team, Player] required: true, }), ``` For working with global IDs directly, the plugin exports `encodeGlobalID(typename, id)` and `decodeGlobalID(globalID)`: ```typescript import { decodeGlobalID } from '@pothos/plugin-relay'; builder.mutationFields((t) => ({ renamePlayer: t.field({ type: Player, args: { id: t.arg.id({ required: true }), name: t.arg.string({ required: true }), }, resolve: (_parent, args) => { const { typename, id } = decodeGlobalID(args.id); return renamePlayer(id, args.name); }, }), })); ``` ### Custom id encoding [#custom-id-encoding] To encode ids differently from the built-in base64 scheme, pass `encodeGlobalID` and `decodeGlobalID` into the `relay` options: ```typescript const builder = new SchemaBuilder({ plugins: [RelayPlugin], relay: { encodeGlobalID: (typename, id) => `${typename}:${id}`, decodeGlobalID: (globalID) => { const [typename, id] = globalID.split(':'); return { typename, id }; }, }, }); ``` ### Exposing extra node fields [#exposing-extra-node-fields] `t.node` and `t.nodeList` add standalone node fields anywhere. Their `id`/`ids` return values match `t.globalID`: a global ID string or an `{ id, type }` object. Loading goes through the same per-request cache, so a node used in several places loads once. ```typescript builder.queryFields((t) => ({ featuredPlayer: t.node({ id: () => ({ id: 1, type: 'Player' }), }), rivalTeams: t.nodeList({ ids: () => [{ id: 1, type: 'Team' }, { id: 2, type: 'Team' }], }), })); ``` ## Connections [#connections] `t.connection` defines a cursor-paginated field. It creates the `Connection` and `Edge` object types, adds the `before`, `after`, `first`, and `last` arguments, and creates `PageInfo` the first time it's used. Here a team exposes its roster: ```typescript playground example="relay-plugin" players: t.connection({ type: Player, resolve: (team, args) => resolveArrayConnection( { args }, [...Players.values()].filter((player) => player.teamId === team.id), ), }), ``` The full form takes two extra option objects (one for the `Connection` type, one for the `Edge` type) for naming and adding fields: ```typescript t.connection( { type: Player, resolve: /* ... */ }, { name: 'TeamRosterConnection', // default: Parent + capitalize(field) + 'Connection' fields: (tc) => ({ /* extra Connection fields — use the tc builder */ }), edgesField: {}, // customize the edges field }, { name: 'TeamRosterEdge', // default: Connection name + 'Edge' fields: (te) => ({ /* extra Edge fields — use the te builder */ }), nodeField: {}, // customize the node field }, ); ``` ### Connection helpers [#connection-helpers] Three helpers build the `edges`/`pageInfo` shape from common data shapes, so you don't assemble it by hand. `resolveArrayConnection` slices a fully-materialized array: ```typescript import { resolveArrayConnection } from '@pothos/plugin-relay'; t.connection({ type: Player, resolve: (_parent, args) => resolveArrayConnection({ args }, loadAllPlayers()), }); ``` `resolveOffsetConnection` drives a limit/offset API and caps how much a single query can pull: ```typescript import { resolveOffsetConnection } from '@pothos/plugin-relay'; t.connection({ type: Player, resolve: (_parent, args) => resolveOffsetConnection({ args }, ({ limit, offset }) => loadPlayers(offset, limit)), }); ``` It accepts a few sizing options alongside `args`: ```typescript { args: ConnectionArguments; defaultSize?: number; // defaults to 20 maxSize?: number; // defaults to 100 totalCount?: number; // required to support `last` without `before` } ``` `resolveCursorConnection` drives true cursor pagination against any store that supports limits, ordering, and filtering. Annotate the callback argument with `ResolveCursorConnectionArgs` so the return type infers correctly: ```typescript import { resolveCursorConnection, ResolveCursorConnectionArgs } from '@pothos/plugin-relay'; t.connection({ type: Player, resolve: (_parent, args) => resolveCursorConnection( { args, toCursor: (player) => player.joinedAt.toISOString() }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => db.players.findMany({ take: limit, where: { joinedAt: { lt: before, gt: after } }, orderBy: { joinedAt: inverted ? 'desc' : 'asc' }, }), ), }); ``` ### Reusing connection and edge objects [#reusing-connection-and-edge-objects] To share one `Connection` type across several fields, build it once with `builder.connectionObject` and pass it to a plain field with `t.arg.connectionArgs()` for the standard args: ```typescript const PlayersConnection = builder.connectionObject( { type: Player, name: 'PlayersConnection' }, { name: 'PlayersEdge' }, // Edge options (optional); defaults to name + 'Edge' ); builder.queryFields((t) => ({ players: t.field({ type: PlayersConnection, args: { ...t.arg.connectionArgs() }, resolve: (_parent, args) => resolveArrayConnection({ args }, loadAllPlayers()), }), })); ``` `builder.edgeObject` creates a reusable `Edge` type on its own, which you can then pass into `connectionObject`: ```typescript const PlayersEdge = builder.edgeObject({ name: 'PlayersEdge', type: Player }); const PlayersConnection = builder.connectionObject( { type: Player, name: 'PlayersConnection' }, PlayersEdge, ); ``` ### Fields on every connection [#fields-on-every-connection] `builder.globalConnectionField` and `builder.globalConnectionFields` add a field to *every* `Connection` type, such as a `totalCount`: ```typescript builder.globalConnectionField('totalCount', (t) => t.int({ nullable: false, resolve: (parent) => parent.totalCount }), ); ``` For that `parent.totalCount` to type-check, declare the extra property on the `Connection` generic so every connection resolver is required to return it: ```typescript const builder = new SchemaBuilder<{ Connection: { totalCount: number }; }>({ plugins: [RelayPlugin], relay: {}, }); ``` The connection helpers don't know about your custom properties, so they won't return them. Merge the extra fields in after calling a helper: `return result && { totalCount: players.length, ...result };` ### Nullability of edges and nodes [#nullability-of-edges-and-nodes] Set the nullability of the `edges` field and the `node` field globally through the `relay` options; the `DefaultEdgesNullability` and `DefaultNodeNullability` generics must match the option values: ```typescript const builder = new SchemaBuilder<{ DefaultEdgesNullability: false; DefaultNodeNullability: true; }>({ plugins: [RelayPlugin], relay: { edgesFieldOptions: { nullable: false }, nodeFieldOptions: { nullable: true }, }, }); ``` `edges` defaults to `{ list: defaultFieldNullability, items: true }` and `node` to `defaultFieldNullability` (itself `true` by default). Override per connection with `edgesNullable` and `nodeNullable`: ```typescript t.connection({ type: Player, edgesNullable: { items: true, list: false }, nodeNullable: false, resolve: (_parent, args) => resolveArrayConnection({ args }, loadAllPlayers()), }); ``` The same two keys work on `builder.connectionObject`. Set `nodesOnConnection: true` in the `relay` options to also add a flattened `nodes` field to every `Connection`. ## Relay mutations [#relay-mutations] `builder.relayMutationField` generates a Relay-compliant mutation: an input object carrying a `clientMutationId`, a payload object carrying the matching `clientMutationId`, and the mutation field wiring them together. ```typescript builder.relayMutationField( 'signPlayer', { inputFields: (t) => ({ playerId: t.id({ required: true }), teamId: t.id({ required: true }), }), }, { nullable: false, // adjust the mutation field's nullability here resolve: async (_root, args, ctx) => { const player = await signPlayer(args.input.playerId, args.input.teamId); return { success: Boolean(player) }; }, }, { outputFields: (t) => ({ success: t.boolean({ resolve: (result) => result.success }), }), }, ); ``` Which produces: ```graphql input SignPlayerInput { clientMutationId: ID! playerId: ID! teamId: ID! } type SignPlayerPayload { clientMutationId: ID! success: Boolean } type Mutation { signPlayer(input: SignPlayerInput!): SignPlayerPayload! } ``` The method takes four arguments: the field `name`, then `inputOptions`, `fieldOptions`, and `payloadOptions`. `inputOptions` accepts a ref to an existing input object or two extra keys: `name` to name the generated input, and `argName` to rename the default `input` argument. `payloadOptions` accepts a `name` for the payload object. Whether a `clientMutationId` field is generated (and whether it's required) is controlled by the `clientMutationId` option: `omit` (default), `required`, or `optional`. Capture the generated refs to reuse the input and payload elsewhere: ```typescript const { inputType: SignPlayerInput, payloadType: SignPlayerPayload } = builder.relayMutationField('signPlayer', /* ... */); ``` ## Customizing generated types [#customizing-generated-types] ### Renaming Node and PageInfo [#renaming-node-and-pageinfo] If `Node` or `PageInfo` collide with existing types, rename them with `nodeTypeOptions` and `pageInfoTypeOptions`; both take the standard type options (`name`, `description`, `extensions`): ```typescript const builder = new SchemaBuilder({ plugins: [RelayPlugin], relay: { nodeTypeOptions: { name: 'RelayNode', description: 'A node in the graph' }, pageInfoTypeOptions: { name: 'RelayPageInfo' }, }, }); ``` ### Custom node loading [#custom-node-loading] To change how the `node`/`nodes` query fields load, pass a `resolve` in `nodeQueryOptions` / `nodesQueryOptions`. Each receives a `resolveNode`/`resolveNodes` callback for the default behavior: ```typescript const builder = new SchemaBuilder({ plugins: [RelayPlugin], relay: { nodeQueryOptions: { resolve: (_root, { id }, ctx, info, resolveNode) => id.typename === 'Player' ? loadPlayerNode(id) : resolveNode(id), }, nodesQueryOptions: { // return nodes in the same order the ids were requested resolve: (_root, { ids }, ctx, info, resolveNodes) => resolveNodes(ids), }, }, }); ``` Set `nodeQueryOptions` or `nodesQueryOptions` to `false` to omit that query field entirely. ### Extending the Node interface [#extending-the-node-interface] Add a derived field to the `Node` interface itself via `builder.nodeInterfaceRef`: ```typescript builder.interfaceField(builder.nodeInterfaceRef(), 'extra', (t) => t.string({ resolve: () => 'it works' }), ); ``` ### Builder options reference [#builder-options-reference] Every option on the `relay` object, grouped by what it configures: | Option | Purpose | | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `idFieldName` | Name of the global id field on nodes. Defaults to `id`. | | `idFieldOptions` | Options passed to the generated id field. | | `cursorType` | `String` (default) or `ID`; the type used for cursor fields. | | `clientMutationId` | `omit` (default), `required`, or `optional`; controls `clientMutationId` on Relay mutations. | | `relayMutationFieldOptions` | Default options for `relayMutationField`. | | `nodeQueryOptions` / `nodesQueryOptions` | Options (or `false` to omit) for the `node` / `nodes` query fields. | | `nodeTypeOptions` / `pageInfoTypeOptions` | Options for the `Node` interface / `PageInfo` type, including `name`. | | `clientMutationIdFieldOptions` / `clientMutationIdInputOptions` | Options for the `clientMutationId` payload field / input field. | | `mutationInputArgOptions` | Options for the input arg created for each Relay mutation. | | `cursorFieldOptions` | Options for the `cursor` field on an edge. | | `nodeFieldOptions` / `edgesFieldOptions` | Options for the `node` field on an edge / the `edges` field on a connection. | | `pageInfoFieldOptions` | Options for the `pageInfo` field on a connection. | | `hasNextPageFieldOptions` / `hasPreviousPageFieldOptions` | Options for the `PageInfo` boolean fields. | | `startCursorFieldOptions` / `endCursorFieldOptions` | Options for the `PageInfo` cursor fields. | | `beforeArgOptions` / `afterArgOptions` / `firstArgOptions` / `lastArgOptions` | Options for each connection argument. | | `defaultConnectionTypeOptions` / `defaultEdgeTypeOptions` | Default options for generated `Connection` / `Edge` types. | | `defaultPayloadTypeOptions` / `defaultMutationInputTypeOptions` | Default options for generated Relay `Payload` / `Input` types. | | `defaultConnectionFieldOptions` | Default options for fields defined with `t.connection`. | | `nodesOnConnection` | Add a flattened `nodes` field to every `Connection`. | | `brandLoadedObjects` | Tag loaded nodes so the default `resolveType` can identify them. Defaults to `true`. | | `encodeGlobalID` / `decodeGlobalID` | Override the global ID encoding scheme. | # Scope auth URL: /docs/plugins/scope-auth Guard fields and types with authorization scopes, scope loaders, and logical combinations. The scope-auth plugin lets you name the checks your app cares about ("is this a member," "is this staff," "can this user manage that team") and attach them to any field or type with an `authScopes` option, so your authorization rules live on the schema itself. Checks run before the resolver, and their results are cached per request. You define the scopes once on the builder, then require them wherever they matter: ```typescript playground example="scope-auth-plugin" Team.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // Only signed-in members see the standings. wins: t.exposeInt('wins', { authScopes: { member: true, }, }), // Scouting reports are staff-only. scoutingReport: t.exposeString('scoutingReport', { authScopes: { staff: true, }, }), }), }); ``` Open the example and empty the **Context** tab: `wins` and `scoutingReport` now fail while `id` and `name` still resolve. The plugin doesn't assume a particular auth model, so the same setup supports role-based, permission-based, or ownership-based schemes. ## Setup [#setup] Install the plugin and list it in `plugins`. When you combine it with other plugins, put scope-auth **first** so plugins that wrap resolvers don't run before the auth check. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-scope-auth ``` ```bash pnpm add @pothos/plugin-scope-auth ``` ```bash yarn add @pothos/plugin-scope-auth ``` ```bash bun add @pothos/plugin-scope-auth ``` Two pieces make up the configuration: the `AuthScopes` type, which names your scopes and the parameter each one takes, and the **scope initializer**, which builds those scopes for the current request. ```typescript import SchemaBuilder from '@pothos/core'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; const builder = new SchemaBuilder<{ Context: { user?: { id: string; role: 'member' | 'staff' } }; // Each scope name maps to the type of the parameter its loader takes. AuthScopes: { public: boolean; member: boolean; staff: boolean; }; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { authScopes: async (context) => ({ public: true, member: !!context.user, staff: context.user?.role === 'staff', }), }, }); ``` The scope names are yours; `public`, `member`, `staff` are arbitrary labels, not plugin keywords. Add as many as you need. The [relay plugin](./relay) is the notable exception to the "scope-auth first" rule. List relay *before* scope-auth so `authScopes` functions receive already-parsed `globalID`s rather than raw strings. ### Terminology [#terminology] A few terms recur below: * **Scope**: one unit of authorization you can require on a field or type. * **Scope map**: the object you pass to `authScopes`, scope names to parameters. * **Scope loader**: a function that resolves a scope from a parameter, ideal for permission services. * **Scope parameter**: the value passed to a loader (the values in a scope map). * **Scope initializer**: the `authScopes` function on the builder that creates a request's scopes. ## Booleans vs scope loaders [#booleans-vs-scope-loaders] A scope in the initializer is either a **boolean** (the request has it or not) or a **loader**, a function that takes a parameter and returns `MaybePromise`. Booleans decide request-wide facts up front; loaders answer parameterized questions on demand. ```typescript authScopes: async (context) => ({ // Eagerly evaluated once per request. member: !!context.user, // A loader: called with the parameter each field supplies. canManageTeam: (teamId: string) => context.permissions.canManage(context.user, teamId), }); ``` A loader runs only when a field requires it, and its result is cached per request by scope name and parameter. If you already know a loader would always fail for this request, short-circuit it with `false` to skip the work entirely: ```typescript authScopes: async (context) => ({ // Bots never have permissions — don't even build the loader for them. canManageTeam: context.user ? (teamId: string) => context.permissions.canManage(context.user!, teamId) : false, }); ``` You can also defer a plain boolean by wrapping it in a function (`() => context.user.isStaff()`) so it's evaluated on first use rather than during initialization. ## Requiring a scope [#requiring-a-scope] `authScopes` attaches to root fields, object fields, and interface fields alike. Listing several scopes checks them together; by default the request passes if it has **any** of them. ```typescript builder.mutationType({ fields: (t) => ({ reportScore: t.boolean({ authScopes: { staff: true }, resolve: () => true, }), }), }); ``` ### Default scopes for a whole type [#default-scopes-for-a-whole-type] To apply the same requirement to every field of a type, put `authScopes` in the type options instead of on each field: ```typescript Team.implement({ authScopes: { member: true }, fields: (t) => ({ name: t.exposeString('name'), wins: t.exposeInt('wins'), }), }); ``` Type-level scopes and field-level scopes both run before a field resolves. When a type scope fails you get one error per affected field, though the check itself runs only once. ### Overriding the type default [#overriding-the-type-default] Add scopes on a field to require *more* than the type default. To *drop* the type default for one field, set `skipTypeScopes`, optionally alongside a field `authScopes` to replace it wholesale: ```typescript Team.implement({ authScopes: { member: true }, fields: (t) => ({ // Public even though the type defaults to members-only. name: t.exposeString('name', { skipTypeScopes: true, }), wins: t.exposeInt('wins'), }), }); ``` `skipInterfaceScopes` does the same for scopes inherited from interfaces. ## Combining scopes with $any and $all [#combining-scopes-with-any-and-all] A scope map is an OR by default. Use the built-in `$any` and `$all` keys to build precise boolean logic; they take scope maps and nest freely: ```typescript Team.implement({ fields: (t) => ({ scoutingReport: t.exposeString('scoutingReport', { authScopes: { $all: { $any: { staff: true, member: true }, public: true, }, }, }), }), }); ``` That requires `public` **and** (`staff` **or** `member`). To flip the top-level default from `any` to `all`, set the `DefaultAuthStrategy` type and the matching `defaultStrategy` option: ```typescript const builder = new SchemaBuilder<{ Context: Context; AuthScopes: { member: boolean; staff: boolean }; DefaultAuthStrategy: 'all'; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { defaultStrategy: 'all', authScopes: async (context) => ({ member: !!context.user, staff: context.user?.role === 'staff', }), }, }); ``` ## Dynamic scopes [#dynamic-scopes] ### Field-specific parameters [#field-specific-parameters] When you can't enumerate every scope ahead of time (a permission service, a per-resource check), pass a parameter to a loader. The map value is the argument: ```typescript builder.mutationField('renameTeam', (t) => t.boolean({ args: { teamId: t.arg.string({ required: true }) }, authScopes: (_parent, args) => ({ canManageTeam: args.teamId }), resolve: () => true, }), ); ``` The parameter types come straight from the `AuthScopes` type you declared on the builder. ### Scopes that depend on the parent [#scopes-that-depend-on-the-parent] When the required scopes depend on the resolved value, use a function for `authScopes`. On a field it receives the same arguments as the resolver; returning a boolean is a shortcut to allow or deny without naming other scopes: ```typescript Team.implement({ fields: (t) => ({ scoutingReport: t.exposeString('scoutingReport', { authScopes: (team, _args, context) => { // The team's own coach always has access. if (context.user?.id === team.coachId) { return true; } // Everyone else needs staff. return { staff: true }; }, }), }), }); ``` A field `authScopes` function runs every time the field resolves (including once per alias) because it depends on the resolver arguments. A type can take a function too. It receives `(parent, context)` and returns a scope map, evaluated lazily on the first field of each instance: ```typescript Team.implement({ authScopes: (team) => (team.isPublic ? { public: true } : { staff: true }), fields: (t) => ({ name: t.exposeString('name'), }), }); ``` Setting scopes based on a field's *return* value isn't supported directly; the check runs before the resolver. Move the check onto the returned type instead, and use [`runScopesOnType`](#running-scopes-on-the-type) so it fires once per object. ## Granting access with $granted [#granting-access-with-granted] Sometimes a field should be reachable *because of how it was reached*. `$granted` scopes are one-off grants passed down from a parent field or type, separate from your normal scopes, never inherited by nested children. A field grants scopes with `grantScopes`; the child type requires them with `$granted`: ```typescript builder.queryType({ fields: (t) => ({ featuredTeam: t.field({ type: Team, grantScopes: ['readTeam'], resolve: () => getFeaturedTeam(), }), }), }); Team.implement({ authScopes: { member: true, $granted: 'readTeam', }, fields: (t) => ({ name: t.exposeString('name'), }), }); ``` A `Team` normally needs `member`, but anyone arriving through `featuredTeam` reads it anyway. `grantScopes` can also be a function of the resolver arguments. A type can grant scopes to its own fields, sharing one condition across a group of fields without repeating it: ```typescript Team.implement({ grantScopes: (team, context) => { if (context.user?.id === team.coachId) { return ['coach', 'readTeam']; } return team.isDraft ? [] : ['readTeam']; }, fields: (t) => ({ name: t.exposeString('name', { authScopes: { $granted: 'readTeam' } }), wins: t.exposeInt('wins', { authScopes: { $granted: 'readTeam' } }), scoutingReport: t.exposeString('scoutingReport', { authScopes: { $granted: 'coach' }, }), }), }); ``` ## Running scopes on the type [#running-scopes-on-the-type] By default every scope, type-level included, is tested at the field level, so a failed type scope surfaces an error on each field. Set `runScopesOnType` to check the type once, on the object itself, either per type or globally in `scopeAuth`: ```typescript const builder = new SchemaBuilder<{ Context: Context; AuthScopes: { member: boolean }; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { // Applies to all object types except Query, Mutation, and Subscription. runScopesOnType: true, authScopes: async (context) => ({ member: !!context.user }), }, }); Team.implement({ runScopesOnType: true, authScopes: { member: true }, fields: (t) => ({ name: t.exposeString('name'), }), }); ``` `runScopesOnType` uses GraphQL's `isTypeOf` and has two limits. It does **not** work with `graphql-jit`, which doesn't support async `isTypeOf` or pass context to it. And fields of a type that sets it can't use `skipTypeScopes` or `skipInterfaceScopes`, since type scopes no longer run at the field level. ## Interfaces [#interfaces] Interfaces declare `authScopes` on their fields exactly like objects. A field runs the checks for *each* interface its type implements, separately; the request must satisfy every one. An object type can set `skipInterfaceScopes: true` to opt out of its interfaces' checks. ## Typed context per scope [#typed-context-per-scope] A scope often narrows what you know about the context; once `member` passes, `user` is non-null. Declare that refinement with the `AuthContexts` type and read it through `t.authField`: ```typescript const builder = new SchemaBuilder<{ Context: { user?: { id: string } }; AuthScopes: { member: boolean }; AuthContexts: { member: { user: { id: string } } }; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { authScopes: async (context) => ({ member: !!context.user }), }, }); builder.queryField('currentUserId', (t) => t.authField({ type: 'ID', authScopes: { member: true }, // context.user is non-null here. resolve: (_parent, _args, context) => context.user.id, }), ); ``` Some plugins add field-builder methods that `t.authField` doesn't cover. For those, `t.withAuth` returns a field builder with the scopes already applied, so you can chain a plugin method onto it: ```typescript builder.queryField('me', (t) => t.withAuth({ member: true }).prismaField({ type: 'User', resolve: (query, _root, _args, context) => prisma.user.findUniqueOrThrow({ ...query, where: { id: context.user.id } }), }), ); ``` ## Customizing the unauthorized error [#customizing-the-unauthorized-error] By default a failed check throws a `ForbiddenError`. Override the message or the error instance globally through `scopeAuth`, or per field: ```typescript const builder = new SchemaBuilder<{ Context: Context; AuthScopes: { member: boolean }; }>({ plugins: [ScopeAuthPlugin], scopeAuth: { unauthorizedError: (_parent, _context, _info, _result) => new Error('Not authorized'), authScopes: async (context) => ({ member: !!context.user }), }, }); ``` The callback receives the field's `parent`, `context`, and `info`, plus a `result` argument carrying the default message and a `failure` describing what went wrong. Return an `Error` (or subclass) or a `string`; a string becomes a `ForbiddenError`. The field-level `unauthorizedError` takes the resolver's arguments plus `result`: ```typescript builder.queryField('roster', (t) => t.field({ type: [Team], authScopes: { member: true }, unauthorizedError: (_parent, _args, _context, _info, _result) => new Error('Sign in to view the roster'), resolve: () => getRoster(), }), ); ``` ### Surfacing errors thrown inside checks [#surfacing-errors-thrown-inside-checks] By default, an error thrown inside an `authScopes` function is *not* caught; it behaves as if thrown from the resolver, bypassing `unauthorizedError` and failing even a passing `$any`. Set `treatErrorsAsUnauthorized` to catch those errors and treat them as a failed scope; the caught error is then attached to the `result` so you can inspect or re-throw it. The `AuthFailure` and `AuthScopeFailureType` exports let you walk the failure tree: ```typescript import ScopeAuthPlugin, { AuthFailure, AuthScopeFailureType } from '@pothos/plugin-scope-auth'; function throwFirstError(failure: AuthFailure) { if ('error' in failure && failure.error) { throw failure.error; } if ( failure.kind === AuthScopeFailureType.AnyAuthScopes || failure.kind === AuthScopeFailureType.AllAuthScopes ) { for (const child of failure.failures) { throwFirstError(child); } } } const builder = new SchemaBuilder<{ Context: Context; AuthScopes: { member: boolean } }>({ plugins: [ScopeAuthPlugin], scopeAuth: { treatErrorsAsUnauthorized: true, unauthorizedError: (_parent, _context, _info, result) => { throwFirstError(result.failure); return new Error('Not authorized'); }, authScopes: async (context) => ({ member: !!context.user }), }, }); ``` ### Returning a value instead of an error [#returning-a-value-instead-of-an-error] To return `null`, an empty list, or any fallback rather than erroring, use `unauthorizedResolver`. It takes the resolver's arguments plus a fifth `ForbiddenError` argument: ```typescript builder.queryField('teams', (t) => t.field({ type: [Team], authScopes: { member: true }, resolve: () => getTeams(), unauthorizedResolver: () => [], }), ); ``` ## When checks run and caching [#when-checks-run-and-caching] Auth results are cached per request so shared scopes cost nothing after the first check: * **Scope initializer**: runs once, the first time any protected field resolves; the result is cached for the request. * **`authScopes` function on a field**: runs on every resolve of that field, since it depends on the resolver arguments. * **`authScopes` function on a type**: runs once per instance in the response, lazily on the first field, then cached for that instance. * **Scope loaders**: run per unique parameter, cached by scope name and parameter. * **`grantScopes` on a field**: runs after the field resolves; not cached. * **`grantScopes` on a type**: runs on the first field of each instance, then cached for that instance. Scopes cache on the *identity* of their parameter. Primitive parameters cache perfectly; if a loader takes an object built inside a scope function you'll miss the cache. Provide a `cacheKey` to derive a stable key: ```typescript const builder = new SchemaBuilder<{ Context: Context; AuthScopes: { member: boolean } }>({ plugins: [ScopeAuthPlugin], scopeAuth: { cacheKey: (value) => JSON.stringify(value), authScopes: async (context) => ({ member: !!context.user }), }, }); ``` `JSON.stringify` handles most objects; for circular references or key-order stability, use something like `faster-stable-stringify`. ## Subscriptions [#subscriptions] When authorizing subscriptions, set `authorizeOnSubscribe` so checks run when the subscription is created rather than when each event resolves: ```typescript scopeAuth: { authorizeOnSubscribe: true, authScopes: async (context) => ({ member: !!context.user }), } ``` ## Testing [#testing] Pass `disableScopeAuth` to `toSchema` to build the schema with every check turned off, for tests that shouldn't thread auth context through every query: ```typescript const schema = builder.toSchema({ disableScopeAuth: true }); ``` ## Reference [#reference] ### `scopeAuth` builder options [#scopeauth-builder-options] | Option | Type | Purpose | | --------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- | | `authScopes` | `(context) => MaybePromise` | The scope initializer. Required. | | `runScopesOnType` | `boolean` | Check type scopes once via `isTypeOf` instead of per field. | | `treatErrorsAsUnauthorized` | `boolean` | Catch errors thrown in checks and treat them as failed scopes. | | `unauthorizedError` | `(parent, context, info, result) => Error \| string` | Global unauthorized error/message. | | `cacheKey` | `(value) => unknown` | Derive a stable cache key for object scope parameters. | | `defaultStrategy` | `'any' \| 'all'` | Top-level combination strategy. Defaults to `any`. | | `authorizeOnSubscribe` | `boolean` | Run subscription checks at subscribe time. | When another plugin already supplies `authScopes`, pass the remaining options through `scopeAuthOptions` instead. ### Builder types [#builder-types] * **`AuthScopes`**: each key names a scope; its value is the type of that scope's parameter. * **`AuthContexts`**: per-scope context refinements read via `t.authField`. * **`DefaultAuthStrategy`**: `'any'` (default) or `'all'`. ### Type and interface options [#type-and-interface-options] | Option | Type | | --------------------- | ----------------------------------------------------------- | | `authScopes` | `ScopeMap` or `(parent, context) => MaybePromise` | | `grantScopes` | `(parent, context) => MaybePromise` | | `runScopesOnType` | `boolean` | | `skipInterfaceScopes` | `boolean` (objects only) | ### Field options [#field-options] | Option | Type | | ---------------------- | ----------------------------------------------------------------------- | | `authScopes` | `ScopeMap` or `(parent, args, context, info) => MaybePromise` | | `grantScopes` | `string[]` or a function of the resolver arguments | | `skipTypeScopes` | `boolean` | | `skipInterfaceScopes` | `boolean` | | `unauthorizedError` | `(parent, args, context, info, result) => Error \| string` | | `unauthorizedResolver` | resolver arguments plus a `ForbiddenError`, returns a fallback value | A **`ScopeMap`** is scope names to parameters, plus the special `$any`, `$all`, and `$granted` keys. The `t.authField` and `t.withAuth` field-builder methods apply scopes while refining context. # Simple objects URL: /docs/plugins/simple-objects Define objects and interfaces from their fields alone with simpleObject and simpleInterface, no separate backing type. Most object types need a backing model, the TypeScript shape your resolvers return. Usually you write that shape as an `interface` and hand it to `builder.objectRef`. The simple objects plugin skips that step: `builder.simpleObject` reads the fields you declare and infers the backing model from them, so a type and its shape are defined in one call. It fits response shapes that only exist to be returned (stats blocks, computed summaries, DTOs) where a hand-written interface would only repeat what the fields already declare. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-simple-objects ``` ```bash pnpm add @pothos/plugin-simple-objects ``` ```bash yarn add @pothos/plugin-simple-objects ``` ```bash bun add @pothos/plugin-simple-objects ``` Add the plugin to the builder: ```typescript import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'; const builder = new SchemaBuilder({ plugins: [SimpleObjectsPlugin], }); ``` Then define types straight from their fields. Here a league standing is assembled from a `TeamStats` block and a `Node` interface, with no `interface` declarations anywhere: ```typescript playground example="simple-objects-plugin" const TeamStats = builder.simpleObject('TeamStats', { fields: (t) => ({ wins: t.int({ nullable: false }), losses: t.int({ nullable: false }), pointDiff: t.int({ nullable: true }), }), }); const Node = builder.simpleInterface('Node', { fields: (t) => ({ id: t.id({ nullable: false }), }), }); const Standing = builder.simpleObject( 'Standing', { interfaces: [Node], fields: (t) => ({ team: t.string(), stats: t.field({ type: TeamStats, nullable: false }), }), }, // Third argument: fields backed by resolvers, with the full inferred parent. (t) => ({ record: t.string({ resolve: (standing) => `${standing.stats.wins}-${standing.stats.losses}`, }), }), ); ``` The resolver on the `standing` query just returns a plain object (`{ id, team, stats: { wins, losses, pointDiff } }`) and TypeScript checks it against the shape Pothos inferred. Get a field name or type wrong and it fails to compile. ## Why simpleObject [#why-simpleobject] `builder.simpleObject` returns an [`ObjectRef`](../fundamentals/objects), the same reference you'd get from `builder.objectRef()`. The difference is where the backing model comes from. With `objectRef` you write the type yourself and Pothos trusts it. With `simpleObject` the fields *are* the type: declare `wins: t.int()` and the backing model gains a required `number`; declare `pointDiff: t.int({ nullable: true })` and it gains an optional `number | null`. Use `simpleObject` for types that only exist in the schema: a `TeamStats` block returned by one query doesn't need a hand-written interface, because the fields already say everything. Use a plain `objectRef` when the backing model is a real domain entity you load, mutate, and pass around. ## Interfaces [#interfaces] `builder.simpleInterface` is the interface counterpart, and simple objects implement interfaces through the same `interfaces` option a regular object uses. An implementing type inherits the interface's inferred fields; `Standing` gets `id` from `Node` without redeclaring it, and the inferred backing model is the intersection of both. Interfaces can also be a thunk, which defers evaluation so two types can reference each other: ```typescript const Standing = builder.simpleObject('Standing', { interfaces: () => [Node], fields: (t) => ({ team: t.string(), }), }); ``` Like `simpleObject`, `simpleInterface` takes an optional third argument for resolver-backed fields, so an interface can carry a computed field its implementers inherit. ## Computed fields [#computed-fields] The `fields` inside the second argument map one-to-one onto the inferred backing model; Pothos resolves each by reading the property of the same name off the parent. When a value is derived, takes arguments, or hits another source rather than being passed straight through, put it in the **third argument** instead. Those fields run through normal resolvers and receive the fully inferred parent, so `record` above can read `standing.stats.wins`. You can add the same kind of field after the fact. Because `simpleObject` hands back an `ObjectRef`, any builder method that extends a ref works on it: ```typescript builder.objectType(Standing, (t) => ({ winPct: t.float({ resolve: (standing) => standing.stats.wins / (standing.stats.wins + standing.stats.losses), }), })); ``` Use whichever reads better: the third argument keeps a type's computed fields next to its declaration; a separate `builder.objectType` call works when the extra fields live in another module. Fields declared in the second argument see the parent as `unknown`. That only surfaces when another plugin inspects the parent; a `plugin-scope-auth` `authScopes` callback on a simple-object field, for instance, gets `unknown` rather than the inferred shape. Move such fields to the third argument, where the parent is fully typed. # Smart subscriptions plugin URL: /docs/plugins/smart-subscriptions Turn any query into a live GraphQL subscription by registering events on the fields and object types it touches. A smart subscription runs a normal query, pushes the first result to the client, and then keeps that result live. Every field and object type the query touches can register the events that should refresh it, and when one fires the plugin re-runs the query and pushes the new data. You opt a query field into this with `smartSubscription: true`, and you register events from a `subscribe` callback on any field or object type. The flow has three steps: 1. Run the query the subscription is based on and push its initial result to the client. 2. As the query resolves, register any subscriptions declared on the fields and object types it used. 3. When a registered event fires, re-run the query and push the updated data. You never decide up front which parts of the schema are subscribable. Any type or field can register an event, so the same query can drive a live view of a whole game or of a single score line. Refetch options (below) narrow a refresh to just the sub-tree that changed, so a single score update doesn't re-run the whole query. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-smart-subscriptions ``` ```bash pnpm add @pothos/plugin-smart-subscriptions ``` ```bash yarn add @pothos/plugin-smart-subscriptions ``` ```bash bun add @pothos/plugin-smart-subscriptions ``` ## Setup [#setup] Register the plugin and supply a `smartSubscriptions` object. Its `subscribe` and `unsubscribe` functions are how the plugin attaches to your event source: a pub/sub bus, a Redis channel, anything that can call back when an event with a given name fires. ```typescript import SchemaBuilder from '@pothos/core'; import SmartSubscriptionsPlugin from '@pothos/plugin-smart-subscriptions'; const builder = new SchemaBuilder<{ Context: Context }>({ plugins: [SmartSubscriptionsPlugin], smartSubscriptions: { // Debouncing toggle: pass null to disable it, any non-null value to enable a short debounce window. debounceDelay: 10, subscribe: (name, context, cb) => context.pubsub.subscribe(name, cb), unsubscribe: (name, context) => context.pubsub.unsubscribe(name), }, }); ``` | Option | Purpose | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `subscribe` | `(name, context, cb) => Promise \| void`. Start listening for `name`; call `cb(err, data)` on each event. | | `unsubscribe` | `(name, context) => Promise \| void`. Stop listening for `name`. | | `debounceDelay` | Toggles debouncing of event bursts before re-running the query. Pass `null` to disable it; any non-null value enables a short debounce window. | ### Wiring an async iterator [#wiring-an-async-iterator] Most pub/sub libraries expose an async iterator per channel instead of a callback. `subscribeOptionsFromIterator` adapts one into the `subscribe`/`unsubscribe` pair for you: ```typescript import SchemaBuilder from '@pothos/core'; import SmartSubscriptionsPlugin, { subscribeOptionsFromIterator, } from '@pothos/plugin-smart-subscriptions'; const builder = new SchemaBuilder<{ Context: Context }>({ plugins: [SmartSubscriptionsPlugin], smartSubscriptions: { debounceDelay: 10, ...subscribeOptionsFromIterator((name, { pubsub }) => pubsub.asyncIterableIterator(name), ), }, }); ``` ## Creating a smart subscription [#creating-a-smart-subscription] Add `smartSubscription: true` to a query field. The plugin mirrors it onto the schema's `Subscription` type under the same name, so `Query.games` gains a matching `Subscription.games`. The field's optional `subscribe` callback registers the events that should refresh the whole query: ```typescript builder.queryFields((t) => ({ games: t.field({ type: [Game], smartSubscription: true, subscribe: (subscriptions, root, args, ctx, info) => { subscriptions.register('game-added'); subscriptions.register('game-removed'); }, resolve: (root, args, ctx) => ctx.Games.all(), }), })); ``` Clients subscribe to it like any other subscription; the selection set is a normal query: ```graphql subscription { games { matchup scores { id points } } } ``` ## Subscriptions on object types [#subscriptions-on-object-types] An object type registers events with a `subscribe` option. It runs once for every instance of that type in the result, so a list of games registers one `game/{id}` event per game. When the query re-runs after an event, `subscribe` runs again for each object in the new result set. Because `subscribe` is a standard object-type option, declare it right on the `implement` call for your [objectRef](../fundamentals/objects): ```typescript const Game = builder.objectRef('Game').implement({ subscribe: (subscriptions, game, context) => { subscriptions.register(`game/${game.id}`); }, fields: (t) => ({ matchup: t.exposeString('matchup'), scores: t.field({ type: [Score], resolve: (game) => game.scores }), }), }); ``` ### Refetch and filter options [#refetch-and-filter-options] `register` takes an options object as its second argument to control what a matched event does: ```typescript const Game = builder.objectRef('Game').implement({ subscribe: (subscriptions, game, context) => { subscriptions.register(`game/${game.id}`, { filter: (value) => value.gameId === game.id, invalidateCache: (value) => context.GameCache.remove(game.id), refetch: () => context.Games.fetchById(game.id), }); }, fields: (t) => ({ matchup: t.exposeString('matchup'), scores: t.field({ type: [Score], resolve: (game) => game.scores }), }), }); ``` | Option | Effect | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filter` | Called with the event value; the refresh only happens when it returns `true`. | | `invalidateCache` | Called before refetching so you can clear stale cache entries, so the reload sees fresh data. | | `refetch` | Refetch just this object. When provided, an event for this object (or any of its children) refreshes only this sub-tree; parts of the query that don't depend on it are left untouched. | ## Subscriptions on fields [#subscriptions-on-fields] When one field has a narrower refresh trigger than its parent type, register its events through the same `subscribe` callback in the field options: ```typescript const Game = builder.objectRef('Game').implement({ fields: (t) => ({ matchup: t.exposeString('matchup'), scores: t.field({ type: [Score], subscribe: (subscriptions, game) => subscriptions.register(`game-scores/${game.id}`), resolve: (game) => game.scores, }), }), }); ``` Fields accept the same `filter` and `invalidateCache` options on `register`. In place of a `refetch` function, set `canRefetch: true` in the field options: the plugin re-runs this field's own resolver (and its children) instead of the rest of the query. ```typescript const Game = builder.objectRef('Game').implement({ fields: (t) => ({ matchup: t.exposeString('matchup'), scores: t.field({ type: [Score], canRefetch: true, subscribe: (subscriptions, game, args, context) => subscriptions.register(`game-scores/${game.id}`, { filter: (value) => value.gameId === game.id, invalidateCache: () => context.GameCache.remove(game.id), }), resolve: (game) => game.scores, }), }), }); ``` ## Known limitations [#known-limitations] The value passed to `filter` and `invalidateCache` is typed as `unknown`, so you'll narrow or cast it yourself. Smart subscriptions also don't work with list fields backed by async generators (the pattern behind `@stream` queries). # SubGraph plugin URL: /docs/plugins/sub-graph Tag types and fields into named sub-graphs, then build a public or internal view of one schema. The sub-graph plugin lets you tag types and fields with named sub-graphs, then ask the builder for just the slice you want. You write each type once; `builder.toSchema({ subGraph })` returns a filtered `GraphQLSchema` with everything outside the named sub-graph removed. The common use is a public API that exposes a subset of a richer internal graph. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-sub-graph ``` ```bash pnpm add @pothos/plugin-sub-graph ``` ```bash yarn add @pothos/plugin-sub-graph ``` ```bash bun add @pothos/plugin-sub-graph ``` ## Tagging and building a view [#tagging-and-building-a-view] Register the plugin, declare your sub-graph names on the `SubGraphs` generic, then set defaults on the builder. `defaultForTypes` puts every type into those sub-graphs unless it says otherwise, and `fieldsInheritFromTypes` makes a field default to its parent type's membership. Individual fields and types opt out with their own `subGraphs` array. ```typescript playground example="sub-graph-plugin" const builder = new SchemaBuilder<{ SubGraphs: 'Public' | 'Internal'; }>({ plugins: [SubGraphPlugin], subGraphs: { // A type with no subGraphs of its own belongs to every sub-graph. defaultForTypes: ['Public', 'Internal'], // A field with no subGraphs inherits its parent type's membership. fieldsInheritFromTypes: true, }, }); // The whole Player type lives only in the Internal graph. const Player = builder.objectRef('Player').implement({ subGraphs: ['Internal'], fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), salary: t.exposeInt('salary'), }), }); const Team = builder.objectRef('Team').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // A single field held back from the Public graph. budget: t.exposeInt('budget', { subGraphs: ['Internal'] }), // Returns an Internal-only type, so it can only live in Internal. roster: t.field({ type: [Player], subGraphs: ['Internal'], resolve: (team) => team.roster, }), }), }); ``` The schema above builds three ways from the same definitions. Call `toSchema` with no `subGraph` for the full graph, or name one to get its view: ```typescript // Everything — the schema you develop against. const schema = builder.toSchema(); // Public: Team.id and Team.name only. budget, roster, and Player are gone. const publicSchema = builder.toSchema({ subGraph: 'Public' }); // Internal: the whole graph, since every type is in Internal too. const internalSchema = builder.toSchema({ subGraph: 'Internal' }); ``` The playground embed above builds the `Public` view, so introspection and the sample query only see `id` and `name` on a team. Add `budget` to the query and it fails to validate; that field exists only in the internal graph. ## Combining sub-graphs [#combining-sub-graphs] `toSchema` also takes a list of sub-graphs. An array is a **union**: a type is kept if it belongs to *any* of the named sub-graphs: ```typescript // Every type/field tagged Internal OR Public. const combined = builder.toSchema({ subGraph: ['Internal', 'Public'] }); ``` The `{ all: [...] }` form is the **intersection**: a type is kept only if it belongs to *every* named sub-graph: ```typescript // Only what is shared by BOTH Internal AND Public. const shared = builder.toSchema({ subGraph: { all: ['Internal', 'Public'] } }); ``` ## Where membership comes from [#where-membership-comes-from] A field's sub-graphs are resolved in order, first match wins: 1. The field's own `subGraphs` array. 2. The parent type's `defaultSubGraphsForFields`. 3. The parent type's own `subGraphs`, if the builder set `fieldsInheritFromTypes: true`. 4. The builder's `subGraphs.defaultForFields`. 5. Otherwise an empty array; the field is in no sub-graph. Set `defaultSubGraphsForFields` on a type to give its fields a starting point that differs from the type itself. A `Query` type can live in every sub-graph while its fields default to none, so each field has to opt in explicitly: ```typescript builder.queryType({ // The Query type is reachable from every sub-graph... subGraphs: ['Public', 'Internal'], // ...but its fields join nothing unless they say so. defaultSubGraphsForFields: [], fields: (t) => ({ teams: t.field({ type: [Team], // Present in the default and Internal schemas, absent from Public. subGraphs: ['Internal'], resolve: () => [...Teams.values()], }), }), }); ``` ### Type and field options [#type-and-field-options] | Where | Option | Purpose | | ------------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Any type | `subGraphs` | The sub-graphs this type belongs to. Falls back to the builder's `defaultForTypes`. | | Object / interface / root type | `defaultSubGraphsForFields` | Default membership for this type's fields, before `fieldsInheritFromTypes`. | | Field | `subGraphs` | The sub-graphs this field belongs to. Falls back through the chain above. | | Nullable arg / input field | `subGraphs` | The sub-graphs this argument or input field belongs to. Non-null args and input fields cannot be removed (see below). | ### Builder options [#builder-options] | Option | Purpose | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subGraphs.defaultForTypes` | Sub-graphs a type joins when it has no `subGraphs` of its own. | | `subGraphs.defaultForFields` | Sub-graphs a field joins when nothing earlier in the chain applies. | | `subGraphs.fieldsInheritFromTypes` | Defaults to `false`. When `true`, a field with no membership of its own inherits its parent type's sub-graphs, but only when the type has no `defaultSubGraphsForFields`. | | `subGraphs.explicitlyIncludeType` | A predicate to force otherwise-unreachable types into a sub-graph (see [Unreachable types](#unreachable-types)). | ## Missing types [#missing-types] Building a sub-graph copies in only the types and output fields that belong to it. Everything else is dropped. Input types are the exception: an argument or input field is only removable when it is nullable, because a resolver still expects its non-null arguments to be present. The plugin throws at build time if you try to strip a non-null argument or input field from a sub-graph that keeps its field. Output fields and structural references fail differently when they point at a dropped type. An output field whose return type was left out is pruned automatically, with no error; that's how `roster`, returning the Internal-only `Player`, vanishes from the Public view above. A runtime error is thrown only when a dropped type is reached as an interface implemented by another interface, a union member, or a non-null argument type; those references can't be rewritten around the missing type. Tag the referencing field or type out of that sub-graph too; fields drop on their own, but the structural references hard-fail. ## Unreachable types [#unreachable-types] Filtering keeps a type only when something in the sub-graph reaches it. A type that nothing references is dropped even if it's tagged for the sub-graph. `explicitlyIncludeType` overrides that: return `true` for any type you want kept regardless of reachability. The case this exists for is federation. When you extend an external reference with the [federation plugin](./federation), the `externalRef` may not be reachable through your own schema, yet the built sub-graph still needs it. Keep every type that carries a resolvable `key` directive: ```typescript import FederationPlugin, { hasResolvableKey } from '@pothos/plugin-federation'; import SubGraphPlugin from '@pothos/plugin-sub-graph'; const builder = new SchemaBuilder<{ SubGraphs: 'Public' | 'Internal'; }>({ plugins: [SubGraphPlugin, FederationPlugin], subGraphs: { explicitlyIncludeType: (type, subGraphs) => hasResolvableKey(type), }, }); ``` # Tracing plugin URL: /docs/plugins/tracing Wrap resolvers with tracing and logging, and export spans to OpenTelemetry, Datadog, New Relic, Sentry, or AWS X-Ray. Tracing measures how long each resolver takes and reports it to your observability backend. In Pothos, you add a `tracing` option to the builder and to individual fields, and the plugin wraps the matching resolvers in a tracer you supply. The plugin only decides *which* fields to trace and hands each resolver to your `wrap` function; you produce the actual span or log, by hand or through one of the provider packages below. The interface has three parts: 1. A `tracing` option on every field, to enable or configure tracing for that field. 2. `tracing.default` on the builder, the fallback for any field that doesn't set `tracing` itself. 3. `tracing.wrap` on the builder, which receives a resolver, its tracing options, and the field config, and returns a wrapped resolver. ## Install [#install] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-tracing ``` ```bash pnpm add @pothos/plugin-tracing ``` ```bash yarn add @pothos/plugin-tracing ``` ```bash bun add @pothos/plugin-tracing ``` ## A traced schema [#a-traced-schema] The two builder options work together: `default` picks the fields worth tracing, and `wrap` decides what tracing does. This setup traces every root field and logs its duration with the `wrapResolver` helper. ```typescript import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isRootField, wrapResolver } from '@pothos/plugin-tracing'; const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { // Trace root fields by default; other fields must opt in. default: (config) => isRootField(config), // Log how long each traced resolver ran. wrap: (resolver, options, config) => wrapResolver(resolver, (error, duration) => { console.log(`Executed resolver ${config.parentType}.${config.name} in ${duration}ms`); }), }, }); ``` Tracing every resolver adds real overhead for little benefit; leaf fields that read a property finish in microseconds. Keep `default` narrow and let expensive fields opt in. ## Enabling tracing on a field [#enabling-tracing-on-a-field] Set `tracing: true` on any field to trace it regardless of the default: ```typescript builder.queryType({ fields: (t) => ({ topScorer: t.string({ args: { teamId: t.arg.id() }, // Trace this field even if the default skips it. tracing: true, resolve: (parent, { teamId }) => Teams.get(Number(teamId))?.topScorer ?? 'Unknown', }), }), }); ``` ### Custom tracing options [#custom-tracing-options] Bare `true`/`false` is enough for a logger, but a real tracer usually wants per-field detail. Widen the `Tracing` generic on the builder to accept an options object, and fields can pass it through the `tracing` option: ```typescript import TracingPlugin, { isRootField, wrapResolver } from '@pothos/plugin-tracing'; export const builder = new SchemaBuilder<{ // `tracing` can now be a boolean or an object with a formatMessage function. Tracing: boolean | { formatMessage: (duration: number) => string }; }>({ plugins: [TracingPlugin], tracing: { /* wrap reads these options — shown below */ }, }); builder.queryType({ fields: (t) => ({ topScorer: t.string({ args: { teamId: t.arg.id() }, // The custom option is now type-checked on the field. tracing: { formatMessage: (duration) => `Looked up the top scorer in ${duration}ms` }, resolve: (parent, { teamId }) => Teams.get(Number(teamId))?.topScorer ?? 'Unknown', }), }), }); ``` The `Tracing` generic defaults to `boolean` when you don't set it. ## Enabling tracing by default [#enabling-tracing-by-default] `tracing.default` decides tracing for any field that doesn't set its own `tracing` option. It receives the field's config, so you can key the decision off the field's shape: ```typescript export const builder = new SchemaBuilder<{ Tracing: boolean | { formatMessage: (duration: number) => string }; }>({ plugins: [TracingPlugin], tracing: { // Trace root fields; leave everything else untraced. default: (config) => isRootField(config), wrap: (resolver) => resolver, // real wrappers are shown below }, }); ``` The plugin exports predicates for the common cases, so you rarely inspect the config by hand: | Helper | Returns `true` for | | ---------------- | ----------------------------------------------------------------------------------- | | `isRootField` | Fields of the `Query`, `Mutation`, and `Subscription` types. | | `isScalarField` | Fields that return a scalar or a list of scalars. | | `isEnumField` | Fields that return an enum or a list of enums. | | `isExposedField` | Fields defined with `t.expose*`, or any field falling back to the default resolver. | Compose them to trace everything expensive while skipping trivial reads: ```typescript tracing: { // Trace root fields and object relations, but not scalars or enums. default: (config) => isRootField(config) || (!isScalarField(config) && !isEnumField(config)), wrap: (resolver, options) => resolver, }; ``` ## Writing a wrapper [#writing-a-wrapper] `tracing.wrap` turns the tracing decision into behavior. It runs with three arguments and returns the resolver the schema will actually call: 1. `resolver` is the field's original resolver. 2. `options` is the tracing options for the field, whether set on the field or returned by `default`. 3. `fieldConfig` is the config object describing the field being wrapped. Resolvers can throw or return a promise, and timing both paths correctly is fiddly, so the plugin ships two helpers that handle it for you. `wrapResolver(resolver, onEnd)` returns a wrapped resolver and calls `onEnd(error, duration)` when it settles, where `error` is `null` on success and `duration` is the elapsed milliseconds: ```typescript export const builder = new SchemaBuilder<{ Tracing: boolean | { formatMessage: (duration: number) => string }; }>({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config), wrap: (resolver, options, config) => wrapResolver(resolver, (error, duration) => { const message = typeof options === 'object' ? options.formatMessage(duration) : `Executed resolver ${config.parentType}.${config.name} in ${duration}ms`; console.log(message); }), }, }); ``` `runFunction(fn, onEnd)` is the same timing logic, but it runs a zero-argument function immediately instead of wrapping a resolver. Use it when you need the resolver arguments, or want to do work before the resolver starts: ```typescript import TracingPlugin, { isEnumField, isRootField, isScalarField, runFunction } from '@pothos/plugin-tracing'; export const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config) || (!isScalarField(config) && !isEnumField(config)), wrap: (resolver, options) => (source, args, ctx, info) => { doSomethingFirst(args); return runFunction( () => resolver(source, args, ctx, info), (error, duration) => { console.log(`Executed resolver for ${info.parentType}.${info.fieldName} in ${duration}ms`); }, ); }, }, }); ``` ## Using resolver arguments in tracers [#using-resolver-arguments-in-tracers] Sometimes the tracing options depend on the resolver's arguments, attaching a team id to a span, say. Both the field-level `tracing` option and `tracing.default` accept a function `(parent, args, context, info) => options`: ```typescript // A tracer that opens a span and attaches custom attributes when they're provided. export const builder = new SchemaBuilder<{ Tracing: false | { attributes?: Record }; }>({ plugins: [TracingPlugin], tracing: { default: (config) => (isRootField(config) ? {} : false), // `options` is the resolved tracing option for the field. wrap: (resolver, options, fieldConfig) => (source, args, ctx, info) => { const span = tracer.createSpan(); if (options.attributes) { span.setAttributes(options.attributes); } return runFunction( () => resolver(source, args, ctx, info), () => { span.end(); }, ); }, }, }); builder.queryType({ fields: (t) => ({ topScorer: t.string({ args: { teamId: t.arg.id() }, // Attach this field's args to the span as an attribute. tracing: (parent, args) => ({ attributes: { args } }), resolve: (parent, { teamId }) => Teams.get(Number(teamId))?.topScorer ?? 'Unknown', }), }), }); ``` `tracing.default` can return the same kind of function to reach the arguments of every matching field: ```typescript tracing: { default: (config) => { // Root fields: attach their args as an attribute. if (isRootField(config)) { return (parent, args) => ({ attributes: { args } }); } // Skip exposed fields entirely. if (isExposedField(config)) { return false; } // Trace, but add no attributes. return {}; }, wrap: /* ... */, }; ``` Returning a function changes when `wrap` runs. Normally `wrap` is called once per field at build time. When the tracing option is a function, its result depends on the resolver arguments, so `wrap` runs on every execution of that field instead. It's usually cheap, but as a rule of thumb, tracing options that don't depend on the resolver arguments are faster. The example above can be reshaped to keep `wrap` at build time: decide *whether* to include args with a static flag, and read the args inside the wrapper where they're free: ```typescript export const builder = new SchemaBuilder<{ Tracing: false | { includeArgs?: boolean }; }>({ plugins: [TracingPlugin], tracing: { default: (config) => (isRootField(config) ? { includeArgs: true } : false), // Static options, so wrap runs once per field at build time. wrap: (resolver, options, fieldConfig) => (source, args, ctx, info) => { const span = tracer.createSpan(); if (options.includeArgs) { span.setAttributes({ args }); } return runFunction( () => resolver(source, args, ctx, info), () => { span.end(); }, ); }, }, }); ``` ## Integrations [#integrations] The plugin ships provider packages that build `wrap` for you. Each exposes a factory that returns a wrapper, plus `AttributeNames`/`SpanNames` enums for instrumenting the execution phase. They all trace *resolvers only*; to time the surrounding GraphQL execution phase you add a server plugin, shown per provider below. The [first-server](../getting-started/first-server) examples all use [graphql-yoga](https://the-guild.dev/graphql/yoga-server), whose envelop plugins the wiring below hooks into; other servers expose an equivalent hook that will look slightly different. ### OpenTelemetry [#opentelemetry] npm pnpm yarn bun ```bash npm install --save @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api ``` ```bash pnpm add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api ``` ```bash yarn add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api ``` ```bash bun add @pothos/tracing-opentelemetry @opentelemetry/semantic-conventions @opentelemetry/api ``` `createOpenTelemetryWrapper(tracer, options)` returns a `wrap` implementation that opens a span per resolver, nesting child spans under their parents automatically. ```typescript import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isRootField } from '@pothos/plugin-tracing'; import { createOpenTelemetryWrapper } from '@pothos/tracing-opentelemetry'; import { tracer } from './tracer'; const createSpan = createOpenTelemetryWrapper(tracer, { includeSource: true, }); export const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config), wrap: (resolver, options) => createSpan(resolver, options), }, }); ``` The wrapper accepts: | Option | Default | Purpose | | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `includeArgs` | `false` | Record the resolver arguments on the span. | | `includeSource` | `false` | Record the field's source location on the span. | | `ignoreError` | `false` | Don't mark the span as errored when the resolver throws. | | `onSpan` | none | `(span, tracingOptions, parent, args, context, info) => void`, called after the span opens so you can add your own attributes. | Use `onSpan` together with a custom `Tracing` generic to attach per-field attributes: ```typescript import { AttributeValue } from '@opentelemetry/api'; import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isRootField } from '@pothos/plugin-tracing'; import { createOpenTelemetryWrapper } from '@pothos/tracing-opentelemetry'; import { tracer } from './tracer'; type TracingOptions = boolean | { attributes?: Record }; const createSpan = createOpenTelemetryWrapper(tracer, { includeSource: true, onSpan: (span, options) => { if (typeof options === 'object' && options.attributes) { span.setAttributes(options.attributes); } }, }); export const builder = new SchemaBuilder<{ Tracing: TracingOptions; }>({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config), wrap: (resolver, options) => createSpan(resolver, options), }, }); builder.queryType({ fields: (t) => ({ topScorer: t.string({ args: { name: t.arg.string() }, tracing: (parent, { name }) => ({ attributes: { name } }), resolve: (parent, { name }) => `Top scorer for ${name ?? 'the league'}`, }), }), }); ``` #### Instrumenting the execution phase [#instrumenting-the-execution-phase] The plugin spans resolvers only. To capture the surrounding execution phase, wrap the server's execute function. This graphql-yoga plugin opens an `EXECUTE` span with the operation name and source: ```typescript import { tracer } from './tracer'; // Import the tracer first if it sets up extra instrumentation. import { print } from 'graphql'; import { createYoga, Plugin } from 'graphql-yoga'; import { createServer } from 'node:http'; import { AttributeNames, SpanNames } from '@pothos/tracing-opentelemetry'; import { schema } from './schema'; const tracingPlugin: Plugin = { onExecute: ({ setExecuteFn, executeFn }) => { setExecuteFn((options) => tracer.startActiveSpan( SpanNames.EXECUTE, { attributes: { [AttributeNames.OPERATION_NAME]: options.operationName ?? undefined, [AttributeNames.SOURCE]: print(options.document), }, }, async (span) => { try { return await executeFn(options); } catch (error) { span.recordException(error as Error); throw error; } finally { span.end(); } }, ), ); }, }; const yoga = createYoga({ schema, plugins: [tracingPlugin] }); const server = createServer(yoga); ``` Envelop's own [`@envelop/opentelemetry`](https://the-guild.dev/graphql/envelop/plugins/use-open-telemetry) plugin can replace the custom plugin. Its drawback: the current version doesn't track parent/child relationships between the spans it creates. Disable its resolver tracing so it doesn't duplicate the plugin's spans: ```typescript import { provider } from './tracer'; // Import the tracer first if it sets up extra instrumentation. import { useOpenTelemetry } from '@envelop/opentelemetry'; import { createYoga } from 'graphql-yoga'; import { createServer } from 'node:http'; import { schema } from './schema'; const yoga = createYoga({ schema, plugins: [ useOpenTelemetry( { // Turn off envelop's resolver tracing to avoid duplicate spans. resolvers: false, variables: false, result: false, }, provider, ), ], }); const server = createServer(yoga); ``` #### Setting up a tracer [#setting-up-a-tracer] This `./tracer` module wires a minimal OpenTelemetry provider that logs spans to the console. Real applications swap the console exporter for one that matches your backend: ```typescript import { diag, DiagConsoleLogger, DiagLogLevel, trace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; export const provider = new NodeTracerProvider({ spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())], }); provider.register(); registerInstrumentations({ // Create spans for incoming http requests automatically. instrumentations: [new HttpInstrumentation({})], }); diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); export const tracer = trace.getTracer('graphql'); ``` ### Datadog [#datadog] Datadog ingests OpenTelemetry, so tracing to Datadog is the OpenTelemetry setup above with an exporter and agent pointed at Datadog. Swap the console exporter for the OTLP HTTP exporter: ```typescript import { trace } from '@opentelemetry/api'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { resourceFromAttributes } from '@opentelemetry/resources'; import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; export const provider = new NodeTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'Pothos-OTEL-example', }), }); provider.addSpanProcessor( new SimpleSpanProcessor( new OTLPTraceExporter({ // Point at your collector if you aren't using the default port. // url: 'http://host:port', }), ), ); provider.register(); registerInstrumentations({ instrumentations: [new HttpInstrumentation({})], }); export const tracer = trace.getTracer('graphql'); ``` Then tell the Datadog agent to receive OTLP over HTTP: ```yaml otlp_config: receiver: protocols: http: endpoint: 0.0.0.0:4318 ``` ### New Relic [#new-relic] npm pnpm yarn bun ```bash npm install --save @pothos/tracing-newrelic newrelic @types/newrelic ``` ```bash pnpm add @pothos/tracing-newrelic newrelic @types/newrelic ``` ```bash yarn add @pothos/tracing-newrelic newrelic @types/newrelic ``` ```bash bun add @pothos/tracing-newrelic newrelic @types/newrelic ``` `createNewrelicWrapper(options)` returns the `wrap` implementation: ```typescript import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isRootField } from '@pothos/plugin-tracing'; import { createNewrelicWrapper } from '@pothos/tracing-newrelic'; const wrapResolver = createNewrelicWrapper({ includeArgs: true, includeSource: true, }); export const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config), wrap: (resolver) => wrapResolver(resolver), }, }); ``` | Option | Default | Purpose | | --------------- | ------- | -------------------------------------------------- | | `includeArgs` | `false` | Record the resolver arguments on the segment. | | `includeSource` | `false` | Record the field's source location on the segment. | #### Instrumenting the execution phase [#instrumenting-the-execution-phase-1] Add the operation name and source as custom attributes on the New Relic transaction. `newrelic` must be imported before anything it instruments: ```typescript import newrelic from 'newrelic'; // newrelic must be imported first. import { print } from 'graphql'; import { createYoga, Plugin } from 'graphql-yoga'; import { createServer } from 'node:http'; import { AttributeNames } from '@pothos/tracing-newrelic'; import { schema } from './schema'; const tracingPlugin: Plugin = { onExecute: ({ args }) => { newrelic.addCustomAttributes({ [AttributeNames.OPERATION_NAME]: args.operationName ?? '', [AttributeNames.SOURCE]: print(args.document), }); }, }; const yoga = createYoga({ schema, plugins: [tracingPlugin] }); const server = createServer(yoga); ``` Envelop's [`@envelop/newrelic`](https://the-guild.dev/graphql/envelop/plugins/use-newrelic) plugin can run alongside the tracing plugin. Disable its resolver tracking so it doesn't duplicate the resolver segments. If you want *all* resolvers tracked, you can use the envelop plugin on its own instead of the Pothos tracing plugin: ```typescript import { useNewRelic } from '@envelop/newrelic'; import { createYoga } from 'graphql-yoga'; import { createServer } from 'node:http'; import { schema } from './schema'; const yoga = createYoga({ schema, plugins: [ useNewRelic({ // The tracing plugin already covers resolvers. trackResolvers: false, }), ], }); const server = createServer(yoga); ``` ### Sentry [#sentry] npm pnpm yarn bun ```bash npm install --save @pothos/tracing-sentry @sentry/node ``` ```bash pnpm add @pothos/tracing-sentry @sentry/node ``` ```bash yarn add @pothos/tracing-sentry @sentry/node ``` ```bash bun add @pothos/tracing-sentry @sentry/node ``` `createSentryWrapper(options)` returns the `wrap` implementation: ```typescript import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isRootField } from '@pothos/plugin-tracing'; import { createSentryWrapper } from '@pothos/tracing-sentry'; const traceResolver = createSentryWrapper({ includeArgs: true, includeSource: true, }); export const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config), wrap: (resolver, options) => traceResolver(resolver, options), }, }); ``` | Option | Default | Purpose | | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `includeArgs` | `false` | Record the resolver arguments on the span. | | `includeSource` | `false` | Record the field's source location on the span. | | `ignoreError` | `false` | Don't mark the span as errored when the resolver throws. | | `onSpan` | none | `(span, tracingOptions, parent, args, context, info) => void`, called after the span opens so you can add your own attributes. | #### Instrumenting the execution phase [#instrumenting-the-execution-phase-2] Open a Sentry span around execution with the operation name and source: ```typescript import { print } from 'graphql'; import { createYoga, Plugin } from 'graphql-yoga'; import { createServer } from 'node:http'; import { AttributeNames } from '@pothos/tracing-sentry'; import * as Sentry from '@sentry/node'; import { schema } from './schema'; Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1, }); const tracingPlugin: Plugin = { onExecute: ({ setExecuteFn, executeFn }) => { setExecuteFn((options) => Sentry.startSpan( { op: 'graphql.execute', name: options.operationName ?? '', forceTransaction: true, attributes: { [AttributeNames.OPERATION_NAME]: options.operationName ?? undefined, [AttributeNames.SOURCE]: print(options.document), }, }, () => executeFn(options), ), ); }, }; const yoga = createYoga({ schema, plugins: [tracingPlugin] }); const server = createServer(yoga); ``` Envelop's [`@envelop/sentry`](https://the-guild.dev/graphql/envelop/plugins/use-sentry) plugin can be combined with the tracing plugin: ```typescript import { useSentry } from '@envelop/sentry'; import { createYoga } from 'graphql-yoga'; import { createServer } from 'node:http'; import { schema } from './schema'; const yoga = createYoga({ schema, plugins: [useSentry({})], }); const server = createServer(yoga); ``` ### AWS X-Ray [#aws-x-ray] npm pnpm yarn bun ```bash npm install --save @pothos/tracing-xray aws-xray-sdk-core ``` ```bash pnpm add @pothos/tracing-xray aws-xray-sdk-core ``` ```bash yarn add @pothos/tracing-xray aws-xray-sdk-core ``` ```bash bun add @pothos/tracing-xray aws-xray-sdk-core ``` `createXRayWrapper(options)` returns the `wrap` implementation. X-Ray needs a parent segment, so this example also traces object relations rather than root fields alone: ```typescript import SchemaBuilder from '@pothos/core'; import TracingPlugin, { isEnumField, isRootField, isScalarField } from '@pothos/plugin-tracing'; import { createXRayWrapper } from '@pothos/tracing-xray'; const traceResolver = createXRayWrapper({ includeArgs: true, includeSource: true, }); export const builder = new SchemaBuilder({ plugins: [TracingPlugin], tracing: { default: (config) => isRootField(config) || (!isScalarField(config) && !isEnumField(config)), wrap: (resolver, options) => traceResolver(resolver, options), }, }); ``` | Option | Default | Purpose | | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `includeArgs` | `false` | Record the resolver arguments on the subsegment. | | `includeSource` | `false` | Record the field's source location on the subsegment. | | `onSegment` | none | `(segment, tracingOptions, parent, args, context, info) => void`, called after the subsegment opens so you can add your own annotations. | #### Instrumenting the execution phase [#instrumenting-the-execution-phase-3] Open a parent X-Ray segment around execution so resolver subsegments nest under it: ```typescript import AWSXRay from 'aws-xray-sdk-core'; import { print } from 'graphql'; import { createYoga, Plugin } from 'graphql-yoga'; import { createServer } from 'node:http'; import { AttributeNames, SpanNames } from '@pothos/tracing-xray'; import { schema } from './schema'; const tracingPlugin: Plugin = { onExecute: ({ setExecuteFn, executeFn }) => { setExecuteFn(async (options) => { const parent = new AWSXRay.Segment('parent'); return AWSXRay.getNamespace().runAndReturn(() => { AWSXRay.setSegment(parent); return AWSXRay.captureAsyncFunc( SpanNames.EXECUTE, (segment) => { if (segment) { segment.addAttribute( AttributeNames.OPERATION_NAME, options.operationName ?? '', ); segment.addAttribute(AttributeNames.SOURCE, print(options.document)); } return executeFn(options); }, parent, ); }); }); }, }; const yoga = createYoga({ schema, plugins: [tracingPlugin] }); const server = createServer(yoga); ``` # Validation plugin URL: /docs/plugins/validation Validate arguments, input fields, and input objects with Zod or any Standard Schema library. GraphQL's type system checks shapes, not rules. It will accept `jersey: 250` or `email: "nope"` as long as the types line up. The validation plugin closes that gap: attach a [Standard Schema](https://standardschema.dev) (a [Zod](https://zod.dev), [Valibot](https://valibot.dev), or [ArkType](https://arktype.io) schema) to any argument, input field, or input type, and Pothos runs it before your resolver ever sees the value. Every attachment point (an argument, an input field, an input type, or a whole field's args) takes the same `validate` option. ## Setup [#setup] npm pnpm yarn bun ```bash npm install --save @pothos/plugin-validation zod ``` ```bash pnpm add @pothos/plugin-validation zod ``` ```bash yarn add @pothos/plugin-validation zod ``` ```bash bun add @pothos/plugin-validation zod ``` Swap `zod` for `valibot` or `arktype`; the plugin only cares that the library implements Standard Schema. Register the plugin like any other: ```typescript import ValidationPlugin from '@pothos/plugin-validation'; import { z } from 'zod'; // or valibot, arktype, ... const builder = new SchemaBuilder({ plugins: [ValidationPlugin], }); ``` ## Validating arguments [#validating-arguments] Pass a schema as the `validate` option on any argument. If the value fails, the resolver never runs and the client gets an error. Here a roster mutation guards the player's name length, email format, and jersey range: ```typescript playground example="validation-plugin" builder.mutationType({ fields: (t) => ({ registerPlayer: t.field({ type: Player, args: { name: t.arg.string({ required: true, validate: z.string().min(2).max(50), }), email: t.arg.string({ required: true, validate: z.email(), }), jersey: t.arg.int({ required: true, validate: z.number().int().min(0).max(99), }), }, resolve: (_root, args) => { const player: IPlayer = { id: String(roster.size + 1), name: args.name, email: args.email, jersey: args.jersey, }; roster.set(player.id, player); return player; }, }), }), }); ``` Every point that accepts `validate:` also has a chained `.validate()` equivalent. The two are interchangeable for plain checks; use chaining when you want to **transform** the value. ## Transforming as you validate [#transforming-as-you-validate] A Standard Schema can change a value's type, and Pothos hands the transformed result to your resolver. Chain `.validate()` to convert a comma-separated string into an array: ```typescript const tags = t.arg.string() .validate(z.string().transform((str) => str.split(',').map((s) => s.trim()))); // In the resolver, args.tags is now string[] ``` Chaining `.validate()` more than once runs the schemas in order, so each transform feeds the next. ## Validating across arguments [#validating-across-arguments] Per-argument schemas can't express "at least one of these." For cross-argument rules, put a `validate` schema on the field itself; it receives the whole args object: ```typescript builder.queryField('findPlayer', (t) => t.boolean({ args: { email: t.arg.string(), jersey: t.arg.int(), }, // Require at least one lookup key validate: z .object({ email: z.string().optional(), jersey: z.number().optional() }) .refine((args) => args.email != null || args.jersey != null, { message: 'Provide an email or a jersey number', }), resolve: () => true, }), ); ``` To **transform** all arguments together, the field-level `validate` option can't help; it can't retype the args. Use `t.validate(args, schema)` instead, which wraps the args map and threads the transformed shape through to the resolver: ```typescript builder.queryField('findPlayer', (t) => t.string({ args: t.validate( { email: t.arg.string(), jersey: t.arg.int() }, z .object({ email: z.string().optional(), jersey: z.number().optional() }) .transform((args) => ({ filter: { email: args.email?.toLowerCase(), jersey: args.jersey }, })), ), // args is now { filter: { email?: string; jersey?: number } } resolve: (_root, args) => JSON.stringify(args.filter), }), ); ``` ## Validating input types [#validating-input-types] Input types take `validate` in both places an argument does: on each field, and on the type as a whole: ```typescript playground example="validation-plugin" const signUpRules = z .object({ email: z.string(), jersey: z.number(), backupJersey: z.number() }) .refine((input: { jersey: number; backupJersey: number }) => input.jersey !== input.backupJersey, { message: 'Primary and backup jersey numbers must differ', }); const SignUp = builder.inputType('SignUp', { fields: (t) => ({ email: t.string({ required: true, validate: z.email(), }), jersey: t.int({ required: true }), backupJersey: t.int({ required: true }), }), validate: signUpRules, }); ``` The field-level `z.email()` runs per field; the type-level `signUpRules` sees every field at once, which is where cross-field rules like "primary and backup jersey numbers must differ" belong. Both levels transform, too. A type-level `.validate(z.object().transform(...))` can reshape the whole input (parsing a raw form into a normalized record), and a chained input-field `.validate()` can convert a value, such as an ISO date string into a `Date`: ```typescript const RawSignUp = builder.inputType('RawSignUp', { fields: (t) => ({ joinedOn: t.string() .validate(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)) .validate(z.string().transform((str) => new Date(str))), }), }); ``` ## Choosing a validation library [#choosing-a-validation-library] Any Standard Schema library works; pick on ergonomics and bundle size, not compatibility: * **[Zod](https://zod.dev)**: TypeScript-first, the most feature-complete option. * **[Valibot](https://valibot.dev)**: modular and tree-shakeable, for bundle-size-sensitive builds. * **[ArkType](https://arktype.io)**: 1:1 TypeScript syntax, optimized editor-to-runtime. Because the plugin targets the Standard Schema interface rather than any one library, you can mix libraries in a single schema, or swap later without touching Pothos. ## Customizing validation errors [#customizing-validation-errors] By default a failed validation throws an `InputValidationError`, a `PothosValidationError` whose message lists each failing path and reason. Override the `validationError` option to reshape that into whatever your API and monitoring expect: ```typescript const builder = new SchemaBuilder({ plugins: [ValidationPlugin], validation: { validationError: (result, args, context) => { // result is the Standard Schema failure — result.issues[] carries path + message return new Error(result.issues.map((issue) => issue.message).join('; ')); }, }, }); ``` The handler runs with the raw failure `result`, the field's `args`, and the request `context`, enough to log, tag, or branch on the operation. It can: * **return an `Error`**, thrown as-is, * **return a `string`**, wrapped in a `PothosValidationError`, or * **throw** its own error directly. Whatever it returns follows [normal error handling](../patterns/handling-errors) from there: mask it in production, or surface it as a typed result with [`plugin-errors`](./errors). ## Execution order [#execution-order] When a field carries validation at several levels, Pothos runs them from the inside out so that each transform is applied before the next schema sees the value: 1. **Input fields** validate first. 2. **Input types** validate once their fields pass. 3. **Arguments** validate next. 4. **Field-level** `validate` / `t.validate` runs last, over the fully validated args. Schemas stacked on the *same* field or type run in sequence, so transforms chain. Schemas on *separate* fields or arguments run in parallel, and their failures merge into a single set of issues, so one response reports every problem at once rather than only the first. # With-Input plugin URL: /docs/plugins/with-input Define fields whose arguments live in a single generated input object with t.fieldWithInput. GraphQL convention is to give a mutation one argument (an `input` object) rather than a loose list of scalars. Writing that out by hand means declaring a separate input type for every field. The with-input plugin collapses the two steps: `t.fieldWithInput` takes the input fields inline, generates the input object type for you, and wires it up as the field's argument. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-with-input ``` ```bash pnpm add @pothos/plugin-with-input ``` ```bash yarn add @pothos/plugin-with-input ``` ```bash bun add @pothos/plugin-with-input ``` Add the plugin, then define input fields with the `t.input` builder. Pothos names and registers the input type on first use. ```typescript playground example="with-input-plugin" import SchemaBuilder from '@pothos/core'; import WithInputPlugin from '@pothos/plugin-with-input'; const builder = new SchemaBuilder({ plugins: [WithInputPlugin], }); const Team = builder.objectRef('Team').implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), wins: t.exposeInt('wins'), }), }); builder.queryType({ fields: (t) => ({ team: t.fieldWithInput({ type: Team, nullable: true, input: { id: t.input.id({ required: true }), }, resolve: (_root, args) => Teams.get(Number(args.input.id)) ?? null, }), }), }); ``` The `input` object maps field names to `t.input.*` definitions, the same scalar builders as `t.arg`, scoped to the generated input type. The resolver reads them off `args.input`. The generated schema: ```graphql type Query { team(input: QueryTeamInput!): Team } input QueryTeamInput { id: ID! } ``` ## Why a generated input [#why-a-generated-input] A single input argument is the standard shape for anything that mutates or takes structured arguments: new fields can be added without touching the call site, and large argument lists stay out of the field signature. `t.fieldWithInput` saves you from declaring a separate input type for each field, and keeps the input's definition next to the field it serves. ## Multiple input fields [#multiple-input-fields] Every entry under `input` becomes a field on the generated type, so a mutation with several arguments still needs exactly one type declaration: ```typescript playground example="with-input-plugin" builder.mutationType({ fields: (t) => ({ renameTeam: t.fieldWithInput({ type: Team, nullable: true, input: { id: t.input.id({ required: true }), name: t.input.string({ required: true }), }, resolve: (_root, args) => { const team = Teams.get(Number(args.input.id)); if (!team) { return null; } team.name = args.input.name; return team; }, }), }), }); ``` This generates `input MutationRenameTeamInput { id: ID!, name: String! }`. You can still declare ordinary arguments alongside the input by passing an `args` option; they sit next to `input` on the field rather than inside the generated type. ## Naming the input type and argument [#naming-the-input-type-and-argument] By default the input type name is `${ParentType}${FieldName}Input` (`QueryTeamInput`, `MutationRenameTeamInput`) and the argument is called `input`. Override either per field with `typeOptions.name` and `argOptions.name`. Renaming the argument also renames the key you read in the resolver: ```typescript playground example="with-input-plugin" builder.mutationType({ fields: (t) => ({ createTeam: t.fieldWithInput({ type: Team, typeOptions: { name: 'NewTeamInput' }, argOptions: { name: 'team' }, input: { name: t.input.string({ required: true }), }, resolve: (_root, args) => { const team: ITeam = { id: nextId++, name: args.team.name, wins: 0 }; Teams.set(team.id, team); return team; }, }), }), }); ``` Both option bags forward the rest of their keys to the underlying type and argument, so `typeOptions` accepts a `description`, `argOptions` accepts `deprecationReason`, and so on. To change the default naming scheme for the whole schema instead of one field, pass a `name` callback in `withInput.typeOptions`. It receives the parent type and field name and returns the input type name: ```typescript const builder = new SchemaBuilder({ plugins: [WithInputPlugin], withInput: { typeOptions: { name: ({ parentTypeName, fieldName }) => { const capitalized = `${fieldName[0].toUpperCase()}${fieldName.slice(1)}`; // Drop the Query/Mutation prefix from root fields. if (parentTypeName === 'Query' || parentTypeName === 'Mutation') { return `${capitalized}Input`; } return `${parentTypeName}${capitalized}Input`; }, }, }, }); ``` ## Optional inputs [#optional-inputs] The input argument is required by default. Set `argOptions.required: false` to make the whole argument optional, as for a search or filter field that can run with no input at all: ```typescript playground example="with-input-plugin" builder.queryType({ fields: (t) => ({ searchTeams: t.fieldWithInput({ type: [Team], argOptions: { required: false }, input: { namePrefix: t.input.string({ required: true }), }, resolve: (_root, args) => { const prefix = args.input?.namePrefix; const teams = [...Teams.values()]; return prefix ? teams.filter((team) => team.name.startsWith(prefix)) : teams; }, }), }), }); ``` When the argument is optional, `args.input` is nullable; read it with optional chaining (`args.input?.namePrefix`). The individual `t.input.*` fields keep their own `required` flags independently of the argument. To flip the default for the whole schema, set `withInput.argOptions.required` on the builder and declare the matching `WithInputArgRequired` in your `SchemaTypes` so the resolver argument types line up: ```typescript const builder = new SchemaBuilder<{ WithInputArgRequired: false }>({ plugins: [WithInputPlugin], withInput: { argOptions: { required: false, }, }, }); ``` ## Schema-wide defaults [#schema-wide-defaults] `withInput.typeOptions` and `withInput.argOptions` on the builder set defaults for every generated input type and argument. Per-field `typeOptions`/`argOptions` merge over them. This is the place for cross-cutting choices: a default description on generated inputs, or the `required` default above: ```typescript const builder = new SchemaBuilder({ plugins: [WithInputPlugin], withInput: { typeOptions: { // Applied to every input type this plugin generates. }, argOptions: { // Applied to every generated input argument. }, }, }); ``` ## Prisma integration [#prisma-integration] With the [Prisma plugin](./prisma) installed, `t.prismaFieldWithInput` combines the generated input with a Prisma-backed field, so the resolver receives the `query` selection alongside `args`: ```typescript builder.queryField('user', (t) => t.prismaFieldWithInput({ type: 'User', nullable: true, input: { id: t.input.id({ required: true }), }, resolve: (query, _root, args) => prisma.user.findUnique({ where: { id: Number.parseInt(args.input.id, 10) }, ...query, }), }), ); ``` ## Options reference [#options-reference] `t.fieldWithInput` takes every option a normal field takes, plus: | Option | Purpose | | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `input` | Map of field names to `t.input.*` definitions. Becomes the generated input type. | | `typeOptions` | Options for the generated input type. `name` overrides the type name; other keys (`description`, …) forward to the input object. | | `argOptions` | Options for the input argument. `name` renames the argument (and the resolver key); `required` toggles nullability. | | `args` | Ordinary arguments declared alongside `input`, outside the generated type. | Builder-level `withInput`: | Option | Purpose | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `withInput.typeOptions` | Defaults for every generated input type. `name` accepts a `({ parentTypeName, fieldName }) => string` callback to customize the naming scheme. | | `withInput.argOptions` | Defaults for every generated input argument, including `required`. | | `WithInputArgRequired` | `SchemaTypes` flag that sets whether input arguments are required by default. Pair it with `withInput.argOptions.required`. | # Writing plugins URL: /docs/plugins/writing-plugins Build a Pothos plugin that extends the type system, adds builder options and methods, and hooks into the schema lifecycle. A Pothos plugin is a class that hooks into schema construction. It can add options to the builder, add methods to the builder and field builders, wrap resolvers, and rewrite types and fields as the schema is built. Everything a plugin exposes to users lives in the same type system Pothos uses for its own API, so the work splits in two: extend the *types* so the new API is visible and checked, then implement the *behavior* in a plugin class. You don't need to understand all of Pothos' types to write a plugin. Start from the [example plugin](https://github.com/hayes/pothos/tree/main/packages/plugin-example), a working skeleton of every pattern below; copy the pieces you need, and lean on the types to tell you what fits. ## The type system [#the-type-system] Two structures carry type information through Pothos, and plugins extend both. **`PothosSchemaTypes`** is a global namespace of interfaces for every options object in the API: object options, field options, builder options, and more. A plugin adds a new option by declaring the matching interface inside this namespace; TypeScript merges your declaration with the core one. Each interface receives the generics relevant to where it is used, so a field-options interface is handed the parent shape, the return type, and the arguments, and your new options can depend on them. **`SchemaTypes`** is the per-builder bundle of types derived from the generic argument to `SchemaBuilder`, merged with Pothos' defaults. It carries the scalars, the backing models for string-referenced object and interface types, the context and root types, the default nullability setting, and any custom types plugins contribute. Almost every interface in `PothosSchemaTypes` receives it (look for `Types extends SchemaTypes` in the generics), which is how a plugin reaches user-provided types when building its options. ## Plugin structure [#plugin-structure] A plugin is three files, matching the example plugin: * **`global-types.ts`**: additions to Pothos' built-in `PothosSchemaTypes` namespace. * **`index.ts`**: the plugin implementation. * **`types.ts`**: any types that do *not* belong in the global namespace, imported into `global-types.ts` as needed. Build the types first. Declare your options in `global-types.ts` and write a test schema that uses them. That gets the user-facing API type-checking before any runtime code exists (you can confirm new options appear and constraints are enforced), and it means the config properties your implementation reads will already exist by the time you write `index.ts`. ### `global-types.ts` [#global-typests] This file declares the `PothosSchemaTypes` namespace and registers the plugin on the `Plugins` interface, mapping the plugin's name to its class: ```typescript import type { SchemaTypes } from '@pothos/core'; import type { PothosExamplePlugin } from '.'; declare global { export namespace PothosSchemaTypes { export interface Plugins { example: PothosExamplePlugin; } } } ``` Keep anything that is not part of `PothosSchemaTypes` out of this file; put those in `types.ts` and import them here. To add properties to a config object, find the interface that defines it in `@pothos/core`. Four files hold the interfaces that make up the namespace: * [`type-options.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/type-options.ts): options for each type (Object, Interface, Enum, and so on). * [`field-options.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/field-options.ts): options for creating fields. * [`schema-types.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/schema-types.ts): `SchemaBuilder` options, `SchemaTypes`, `toSchema` options, and other utility interfaces. * [`classes.ts`](https://github.com/hayes/pothos/blob/main/packages/core/src/types/global/classes.ts): the classes Pothos uses, including `SchemaBuilder` and the field builders. Copy the interface you want into your namespace, delete its existing properties, and keep every generic exactly as declared (importing the generic types from `@pothos/core`). Add your own properties, making them optional (`newProp?: TypeOfProp`) unless the plugin genuinely requires them. ### `index.ts` [#indexts] The implementation file needs five things: a bare import of the global types, the plugin name typed as a string literal, a default export of that name, a class extending `BasePlugin`, and a registration call. ```typescript import './global-types'; import SchemaBuilder, { BasePlugin, type SchemaTypes } from '@pothos/core'; export * from './types'; const pluginName = 'example'; export default pluginName; export class PothosExamplePlugin extends BasePlugin {} SchemaBuilder.registerPlugin(pluginName, PothosExamplePlugin); ``` `BasePlugin`, `SchemaTypes`, and `SchemaBuilder` all come from `@pothos/core`. To adapt the example plugin, change the name in these three places: `pluginName` here, the class name, and the `Plugins` key in `global-types.ts`. ### Lifecycle hooks [#lifecycle-hooks] `SchemaBuilder` instantiates every plugin fresh each time `toSchema` is called, then invokes each hook the plugin defines as the schema is built. Define only the ones you need: | Hook | Called | | --------------------- | ---------------------------------------------------------------------------------------------- | | `onTypeConfig` | Once per type, with the config used to build the underlying GraphQL type. | | `onOutputFieldConfig` | Once per Object or Interface field. | | `onInputFieldConfig` | Once per Input Object field or field argument. | | `onEnumValueConfig` | Once per enum value. | | `beforeBuild` | Before the schema is built; the last chance to add new types or fields. | | `afterBuild` | With the fully built `GraphQLSchema`. | | `wrapResolve` | When creating the resolver for each field. | | `wrapSubscribe` | For each field on the `Subscription` type. | | `wrapArgMappers` | Around resolve/subscribe, outside argument mapping, so argument-mapping errors can be handled. | | `wrapResolveType` | For each Union and Interface. | | `wrapIsTypeOf` | For each Object type's `isTypeOf`. | Every hook except `beforeBuild` must return a value matching its first argument: a config object, or the resolve/subscribe/resolveType function. If the plugin does not modify the value, return it unchanged. When you do change a config, return a *copy* with your changes rather than mutating the argument: spread it (`{ ...config, newProp: value }`) or use `Object.assign`. Each config carries the GraphQL properties needed to build the type or field (some, like `resolve`, are added later) plus Pothos-specific ones: `graphqlKind` for the underlying GraphQL type, and `pothosOptions` holding the options passed to the builder for that type or field. Add new types or fields in `beforeBuild`; anything added later may not be wired up correctly. Because a new plugin instance is created per schema, guard additions so they run once per schema even across instances. The `runUnique` helper on `BasePlugin` does this: pass it a key and a callback and the callback runs at most once per schema for that key. ## Extending the API [#extending-the-api] Below are the common ways a plugin extends Pothos, each a simplified sketch. Most plugins combine several. Whenever a step says "extend an interface," that interface goes in the `PothosSchemaTypes` namespace in `global-types.ts`. ### Options on the builder constructor [#options-on-the-builder-constructor] Users never construct a plugin directly, so they can't pass it options. Configure a plugin instead by adding properties to the `SchemaBuilder` options, extending `SchemaBuilderOptions`: ```typescript export interface SchemaBuilderOptions { optionInRootOfConfig?: boolean; nestedOptionsObject?: ExamplePluginOptions; // imported from types.ts } ``` Read them, fully typed, through `this.builder.options`: ```typescript export class PothosExamplePlugin extends BasePlugin { override onTypeConfig(typeConfig: PothosTypeConfig) { console.log(this.builder.options.nestedOptionsObject?.exampleOption); return typeConfig; } } ``` ### Options at build time [#options-at-build-time] Some plugins build the same builder in different modes: the [mocks plugin](./mocks) rebuilds with different mock sets, the [sub-graph plugin](./sub-graph) builds separate subgraphs. For those, extend the `toSchema` options via `BuildSchemaOptions`: ```typescript export interface BuildSchemaOptions { customBuildTimeOptions?: boolean; } ``` These are available on `this.options`: ```typescript override onTypeConfig(typeConfig: PothosTypeConfig) { console.log(this.options.customBuildTimeOptions); return typeConfig; } ``` ### Options on types [#options-on-types] Each GraphQL type has its own options interface. To add an option to object types, extend `ObjectTypeOptions`: ```typescript export interface ObjectTypeOptions { optionOnObject?: boolean; } ``` Read it from the type config, narrowing on `kind` first so TypeScript knows the config is for an object: ```typescript override onTypeConfig(typeConfig: PothosTypeConfig) { if (typeConfig.kind === 'Object') { console.log(typeConfig.pothosOptions.optionOnObject); } return typeConfig; } ``` `typeConfig.kind` follows how Pothos splits its config objects: `Query`, `Mutation`, and `Subscription` each have their own `kind` even though GraphQL calls them all objects. Use `typeConfig.graphqlKind` when you want the underlying GraphQL kind instead. ### Options on fields [#options-on-fields] Fields work the same way, across several interfaces for the different field types. To add an option to mutation fields, extend `MutationFieldOptions`: ```typescript export interface MutationFieldOptions< Types extends SchemaTypes, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, > { customMutationFieldOption?: boolean; } ``` Field interfaces carry more generics, which let your options depend on the exact field being defined. Copy every generic from `@pothos/core` even if you don't use them all; if the generics don't line up, TypeScript won't merge your declaration. You do *not* need the interface's `extends` clause when it extends another interface such as `FieldOptions`. Read the option after checking the field's `kind`: ```typescript override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) { if (fieldConfig.kind === 'Mutation') { console.log(fieldConfig.pothosOptions.customMutationFieldOption); } return fieldConfig; } ``` ### New builder methods [#new-builder-methods] Adding a method to `SchemaBuilder` or a field builder is two steps: declare it on the interface so TypeScript knows it exists, then attach the implementation to the prototype. ```typescript export interface SchemaBuilder { buildCustomObject: () => ObjectRef<{ custom: 'shape' }>; } ``` ```typescript const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder; schemaBuilderProto.buildCustomObject = function buildCustomObject() { return this.objectRef<{ custom: 'shape' }>('CustomObject').implement({ fields: () => ({}), }); }; ``` Use a `function` expression, not an arrow, so `this` resolves to the `SchemaBuilder` instance. ### Wrapping resolvers [#wrapping-resolvers] Runtime behavior goes in the wrap hooks: `wrapResolve`, `wrapSubscribe`, and `wrapResolveType`. Each receives the function it wraps plus the field or type config, and returns either the original function or a replacement with the same signature: ```typescript override wrapResolve( resolver: GraphQLFieldResolver, fieldConfig: PothosOutputFieldConfig, ): GraphQLFieldResolver { return (parent, args, context, info) => { console.log(`Resolving ${info.parentType}.${info.fieldName}`); return resolver(parent, args, context, info); }; } ``` Resolvers return values in many shapes: plain values, promises, even `Promise<(Promise | T)[]>`. Be careful introspecting a return value, and wrap resolvers only when you truly need to; wrapping every field adds overhead to every request. ### Wrapping arguments and inputs [#wrapping-arguments-and-inputs] There is no dedicated hook for wrapping input fields. Instead, modify the `args` object inside `wrapResolve` or `wrapSubscribe` before passing it to the original function. Two utilities from `@pothos/core` make this efficient, especially for recursive inputs, by doing the analysis at build time so runtime work stays minimal: * `mapInputFields`: selects the affected input fields and extracts a per-field value. * `createInputValueMapper`: turns a `mapInputFields` result into a function that rewrites those inputs in an args object. A plugin can use them to decode `globalID` inputs, selecting the global-ID arguments once, then mapping them on each call. The sketch below wraps `wrapResolve` to make the utilities easy to follow; the [relay plugin](./relay) does the same work from `onOutputFieldConfig`, appending to the field's `argMappers` array so the mapping runs inside the `wrapArgMappers` hook: ```typescript export class PothosRelayPlugin extends BasePlugin { // Optional cache so mappings can be reused across fields. // Only provide one if your mappings are not specific to the current field. private mappingCache = new Map>(); override wrapResolve( resolver: GraphQLFieldResolver, fieldConfig: PothosOutputFieldConfig, ): GraphQLFieldResolver { // Select the args that are globalIds. const argMappings = mapInputFields( fieldConfig.args, this.buildCache, (inputField) => (inputField.extensions?.isRelayGlobalID ? true : null), this.mappingCache, ); // If nothing reachable through args needs mapping, don't wrap at all. if (!argMappings) { return resolver; } const argMapper = createInputValueMapper(argMappings, (globalID) => internalDecodeGlobalID(this.builder, String(globalID)), ); return (parent, args, context, info) => resolver(parent, argMapper(args), context, info); } } ``` Returning `null` from the mapper means that input field gets no mapping. `mapInputFields` returns a map keyed by field/argument name, with values of roughly this shape: ```typescript interface InputFieldMapping { kind: 'Enum' | 'Scalar' | 'InputObject'; isList: boolean; listDepth: number; // how many list wrappers surround the input config: PothosInputFieldConfig; value: T; // the mapper's return value, when not null } ``` The real type is a discriminated union on `kind`; only the `InputObject` variant carries a `value` that may be `null` and the nested `fields` property below. When `kind` is `InputObject`, the mapping also has a `fields` property describing nested mappings: ```typescript interface InputTypeFieldsMapping { configs: Record>; map: Map> | null; } ``` Both the root map and each nested `fields.map` contain entries only for fields where the mapper returned non-null. If the mapper returned `null` for everything, `mapInputFields` returns `null`, the signal that no wrapping is needed. `createInputValueMapper` covers most cases; for the rest, write a custom mapping function over the same `mapInputFields` result. ### Removing fields and enum values [#removing-fields-and-enum-values] Return `null` from the matching `on*Config` hook to drop a field or enum value: ```typescript override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig) { return fieldConfig.name === 'removeMe' ? null : fieldConfig; } override onInputFieldConfig(fieldConfig: PothosInputFieldConfig) { return fieldConfig.name === 'removeMe' ? null : fieldConfig; } override onEnumValueConfig(valueConfig: PothosEnumValueConfig) { return valueConfig.value === 'removeMe' ? null : valueConfig; } ``` Removing a whole type is a schema transform; see below. ### Transforming the whole schema [#transforming-the-whole-schema] When the config hooks aren't powerful enough (removing types, for instance, as the [sub-graph plugin](./sub-graph) does), use `afterBuild`. It receives the built schema and returns either that schema or a new one, so you can run it through libraries like `graphql-tools` to transform it however you need: ```typescript override afterBuild(schema: GraphQLSchema): GraphQLSchema { return transformSchema(schema); } ``` ## Sharing types across the schema [#sharing-types-across-the-schema] ### Using SchemaTypes [#using-schematypes] The `Types extends SchemaTypes` generic on nearly every `@pothos/core` interface is what lets Pothos and its plugins share type information and fold in user-defined types. It combines Pothos' defaults with the generic passed to the `SchemaBuilder` constructor, and holds the scalars, the backing models used by string-referenced object and interface types, the context and root types, the default nullability setting, and any plugin-contributed types. The most common use is reaching the context type so a plugin callback can be typed against it: ```typescript export interface SchemaBuilderOptions { exampleSetupFn?: (context: Types['Context']) => ExamplePluginSetupConfig; } ``` ### Contributing user-defined types [#contributing-user-defined-types] A plugin can add its own user-definable entries to `SchemaTypes`; the [directives](./directives) and [scope-auth](./scope-auth) plugins both do. It takes two interfaces: `UserSchemaTypes`, describing what the user provides, and `ExtendDefaultTypes`, supplying a default when they don't: ```typescript export interface UserSchemaTypes { NewExampleTypes: Record; } export interface ExtendDefaultTypes> { NewExampleTypes: PartialTypes['NewExampleTypes'] & {}; } ``` The value is then reachable as `Types['NewExampleTypes']` in any interface or type that receives `SchemaTypes`. ### Per-request data [#per-request-data] A plugin that wraps resolvers can store data scoped to the current request. Define `createRequestData` to build it and call `requestData` to read it back; the data shape is the second generic on `BasePlugin`: ```typescript export class PothosExamplePlugin extends BasePlugin< Types, { resolveCount: number } > { override createRequestData(): { resolveCount: number } { return { resolveCount: 0 }; } override wrapResolve( resolver: GraphQLFieldResolver, fieldConfig: PothosOutputFieldConfig, ): GraphQLFieldResolver { return (parent, args, context, info) => { const requestData = this.requestData(context); requestData.resolveCount += 1; console.log(`request has resolved ${requestData.resolveCount} fields`); return resolver(parent, args, context, info); }; } } ``` `requestData` takes the context object as its only argument and uses it to identify the request, so the same data object comes back throughout one request. ## Useful methods [#useful-methods] A few `@pothos/core` internals help when a hook needs a config that isn't available yet: * `builder.configStore.onTypeConfig`: takes a type ref and a callback, and invokes the callback with that type's config once it's available. * `fieldRef.onFirstUse`: takes a callback invoked once the field's config is available. * `buildCache.getTypeConfig`: returns a type's config after all plugin modifications have been applied. This guide covers the common cases, not the full API; exploring the types and the [example plugin](https://github.com/hayes/pothos/tree/main/packages/plugin-example) source will surface the rest. If you get stuck, open a GitHub issue. # Zod validation plugin URL: /docs/plugins/zod Validate field arguments and input fields with a validate option that maps onto zod constraints. The [validation plugin](./validation) is now the recommended way to validate a schema. It supports zod alongside several other validation libraries. The zod plugin keeps working; use it only on schemas already built around it. The zod plugin validates field arguments and input fields with [zod](https://github.com/colinhacks/zod). You attach a `validate` option wherever you accept input (a single argument, a whole field's args, an input object, or one of its fields) and the plugin builds a zod validator that runs before your resolver. It does not re-export zod; instead `validate` takes a small options object whose keys map onto the zod methods you already know (`min`, `max`, `email`, `regex`, and so on), or an actual zod schema when you want the full API. ## Install [#install] npm pnpm yarn bun ```bash npm install --save zod @pothos/plugin-zod ``` ```bash pnpm add zod @pothos/plugin-zod ``` ```bash yarn add zod @pothos/plugin-zod ``` ```bash bun add zod @pothos/plugin-zod ``` ## Setup [#setup] Add the plugin, then optionally hand it a `validationError` callback to shape what clients see when validation fails. ```typescript import SchemaBuilder from '@pothos/core'; import ZodPlugin from '@pothos/plugin-zod'; const builder = new SchemaBuilder<{ Context: { userId: string } }>({ plugins: [ZodPlugin], zod: { // Runs when validation fails. Return a string or Error, or throw your own. // The default is to throw the raw ZodError. validationError: (zodError, _args, _context, _info) => zodError.issues[0].message, }, }); ``` `validationError` receives the `ZodError`, the field's `args`, the `context`, and the GraphQL `info`. Return a `string` (thrown as a `PothosValidationError`), return an `Error` instance (thrown as-is), or throw directly. Skip it to surface the raw zod error. ## Validating a single argument [#validating-a-single-argument] Add `validate` to any argument. The keys you pass are constraints for that argument's type; here an email string capped at 254 characters. ```typescript builder.queryType({ fields: (t) => ({ playerByEmail: t.boolean({ args: { email: t.arg.string({ validate: { email: true, maxLength: 254, }, }), }, resolve: () => true, }), }), }); ``` ## Validating all arguments together [#validating-all-arguments-together] Cross-field rules ("at least one of these," "start before end") belong on the field's own `validate`, which receives the whole args object. It can be a function, or a `[function, options]` pair when you want a message. ```typescript builder.mutationType({ fields: (t) => ({ inviteToTeam: t.boolean({ args: { email: t.arg.string({ validate: { email: true } }), phone: t.arg.string(), }, // Require at least one contact method across the two args. validate: [ (args) => !!args.email || !!args.phone, { message: 'Provide either an email address or a phone number' }, ], resolve: () => true, }), }), }); ``` ## Custom messages [#custom-messages] Every constraint accepts either a bare value or a `[value, { message }]` pair. The pair form is a `Constraint`; the options object is passed straight to the underlying zod method, so anything zod's method accepts (a `message`, and a `path` for object refinements) works. ```typescript t.arg.int({ validate: { min: [0, { message: 'jersey number cannot be negative' }], max: [99, { message: 'jersey number must be under 100' }], int: true, }, }); ``` ## Lists [#lists] List arguments validate the list and its items in one options object. Constraints like `minLength` / `maxLength` / `length` apply to the array; `items` carries the constraints for each element. ```typescript builder.mutationType({ fields: (t) => ({ setRoster: t.boolean({ args: { emails: t.arg.stringList({ validate: { maxLength: 12, items: { email: true, }, }, }), }, resolve: () => true, }), }), }); ``` ## Input objects [#input-objects] `validate` works the same on an input type and on its fields. Put per-field rules on each field, and cross-field rules on the input type itself, where the callback receives the whole object. ```typescript const RegisterTeamInput = builder.inputType('RegisterTeamInput', { fields: (t) => ({ name: t.string({ validate: { minLength: 3, maxLength: 40 } }), contactEmail: t.string({ validate: { email: true } }), backupEmail: t.string({ required: false, validate: { email: true } }), }), // Runs against the assembled input object. validate: [ (input) => input.contactEmail !== input.backupEmail, { message: 'backup email must differ from contact email' }, ], }); ``` ## Bring your own zod schema [#bring-your-own-zod-schema] When the built-in constraints run out (unions, branded types, `transform`, anything zod can express), pass a real schema with the `schema` key instead. It works on a single argument: ```typescript import { z } from 'zod'; t.arg.int({ validate: { schema: z.number().int().max(5), }, }); ``` ...or on the whole field, validating every argument at once: ```typescript builder.queryType({ fields: (t) => ({ signIn: t.boolean({ args: { email: t.arg.string(), password: t.arg.string(), }, validate: { schema: z.object({ email: z.string().email(), password: z.string().min(8), }), }, resolve: () => true, }), }), }); ``` You can combine `schema` with the constraint keys. The plugin pipes your schema into the generated validator, so both run. Validation runs as an async check just before your resolver; refinements may return a `Promise`, and the parsed value (including anything a `transform` rewrites) becomes the args your resolver receives. ## Constraint reference [#constraint-reference] `validate` accepts a bare refinement function, an array of them, or an options object. The options object always allows these keys: | Key | Type | Purpose | | -------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `type` | `'number' \| 'bigint' \| 'boolean' \| 'date' \| 'string' \| 'object' \| 'array'` | Pin the base zod type. See [How it works](#how-it-works) for why this matters. | | `refine` | function or `[function, { message?, path? }]`, or an array of either | A predicate handed to zod's `refine`. Receives the validated value; returns `boolean` or `Promise`. | | `schema` | `ZodType` | A zod schema piped into the generated validator. | The remaining keys depend on the field's type. Each value is a `Constraint`, either a bare value or `[value, { message }]`: | Type | Additional keys | | ------- | ------------------------------------------------------------------------------------------- | | Number | `min`, `max`, `int`, `positive`, `nonnegative`, `negative`, `nonpositive` | | String | `minLength`, `maxLength`, `length`, `email`, `url`, `uuid`, `regex` | | Array | `minLength`, `maxLength`, `length`, `items` (a nested `ValidationOptions` for each element) | | BigInt | *(base keys only)* | | Boolean | *(base keys only)* | | Date | *(base keys only)* | | Object | *(base keys only)* | ## How it works [#how-it-works] Each argument and input field builds its own zod validator from its `validate` options. At runtime the plugin has no access to the JavaScript type behind a GraphQL type, so when you pass plain constraints it builds a union of every base type that could satisfy them. A lone `maxLength`, for example, fits both strings and arrays: ```typescript z.union([z.string().max(5), z.array(z.unknown()).max(5)]); ``` An `email` constraint only fits strings, so the union collapses to one member. When the argument is not required, the whole validator is wrapped `.optional().nullable()` rather than folded into the union. Set `type` to skip the guessing and pin one base type: ```typescript // { validate: { type: 'string', maxLength: 5 } } builds: z.string().max(5); ``` Three cases sidestep the union entirely: * **Input object** args and fields always validate with `z.looseObject(...)`. * **List** args and fields always validate with `.array()`. * **Refinement-only** validators (a bare function, or an options object with just `refine` and/or `schema`) validate against `z.unknown()`, since no constraint narrows the type. A `schema` is piped into whatever the plugin generates (`yourSchema.pipe(generated)`), so your schema and the constraints both run. Older releases wrapped optional validators as `z.union([z.null(), z.undefined(), …])` and merged `schema` with `z.intersection`. On zod 4 the plugin uses `.optional().nullable()` and `.pipe()` instead; the shapes above reflect current behavior. ## Sharing schemas with client code [#sharing-schemas-with-client-code] To reuse a validator on the client, write it as an ordinary zod schema in a shared module, then attach it with `schema`: ```typescript // shared/validators.ts import { z } from 'zod'; export const jerseyNumber = z.number().int().min(0).max(99); ``` ```typescript // server import { jerseyNumber } from './shared/validators'; t.arg.int({ validate: { schema: jerseyNumber }, }); ``` ```typescript // client import { jerseyNumber } from './shared/validators'; jerseyNumber.parse(23); // pass jerseyNumber.parse(100); // throws ``` If you would rather share the constraint options object, the plugin exports `createZodSchema` to turn one into a zod schema on demand. Type the options with the exported `ValidationOptions`: ```typescript // shared/validators.ts import type { ValidationOptions } from '@pothos/plugin-zod'; export const jerseyNumberOptions: ValidationOptions = { min: 0, max: 99, int: true, }; ``` ```typescript // server import { jerseyNumberOptions } from './shared/validators'; t.arg.int({ validate: jerseyNumberOptions }); ``` ```typescript // client import { createZodSchema } from '@pothos/plugin-zod'; import { jerseyNumberOptions } from './shared/validators'; const validator = createZodSchema(jerseyNumberOptions); validator.parse(23); // pass validator.parse(100); // throws ``` # Connections URL: /docs/plugins/drizzle/connections Build Relay connections over Drizzle tables with cursor pagination, total counts, and page-size limits. Relay connections give you cursor-based pagination over a list. The Drizzle plugin builds them on top of the relational query builder and loads each page nested inside the same optimized query as the rest of the request. Use `t.relatedConnection` to paginate a relation of a [node](./relay), and `t.drizzleConnection` for a connection that's a root-field entry point. These examples assume the builder is set up with `DrizzlePlugin` and `RelayPlugin`, a `db` client in scope, and the league schema and relations from [Setup](./setup). ## A connection from a relation [#a-connection-from-a-relation] `t.relatedConnection` builds a connection from a relation of the current table, with no resolver needed since the relation names the data. It defines the `Connection` and `Edge` types for you and pairs naturally with a [node](./relay). ```typescript builder.drizzleNode('teams', { name: 'Team', id: { column: (team) => team.id }, fields: (t) => ({ name: t.exposeString('name'), // The simplest form: paginate the team's players. roster: t.relatedConnection('players'), }), }); ``` Unlike the Prisma plugin, there is no `cursor` option. The cursor is **derived from the connection's `orderBy`**, which defaults to the table's primary key. That derivation is also why the ordering format differs from Drizzle's own. To paginate backwards efficiently, the plugin runs some queries in reverse and inverts the ordering, so it needs to read `orderBy` as data, not as opaque SQL. Pass it as an object, `{ column: 'asc' | 'desc' }`, rather than Drizzle's `asc()` / `desc()` helpers. It can be a single column or an array for multi-column ordering, and the same columns are what the cursor is built from. ## Filtering and ordering the connection [#filtering-and-ordering-the-connection] Pass a `query` to filter and order the connection, and `args` to make it client-driven. `query` takes the same shape as [`t.relation`](./relations) minus `limit` and `offset` (the connection arguments own the window), with the object-form `orderBy` above: ```typescript builder.drizzleNode('teams', { name: 'Team', id: { column: (team) => team.id }, fields: (t) => ({ name: t.exposeString('name'), players: t.relatedConnection('players', { args: { sortByNumber: t.arg.boolean(), }, query: (args) => ({ orderBy: { number: args.sortByNumber ? 'asc' : 'desc', }, }), }), }), }); ``` | Option | Purpose | | ------------- | ------------------------------------------------------------------------------------------------------------------------ | | `query` | A static object, or a function of `(args, ctx)`, merged into the relation query (`where` and the object-form `orderBy`). | | `type` | Override the node type with a [variant](./variants) ref of the related table. | | `totalCount` | Set `true` to add a `totalCount` field to the connection. | | `defaultSize` | Page size when neither `first` nor `last` is given. | | `maxSize` | Maximum number of nodes returned. | `t.relatedConnection` takes optional Connection and Edge options as its third and fourth arguments, exactly like `t.connection` from the [Relay plugin](../relay). `first` and `last` can't be combined on the same connection; passing both throws, since there's no efficient query that honors both ends at once. Use one or the other. ## Total count [#total-count] Set `totalCount: true` to add a `totalCount` field. It's issued as a subquery inside the main query, and it only runs when the client actually selects `totalCount`: ```typescript builder.drizzleNode('teams', { name: 'Team', id: { column: (team) => team.id }, fields: (t) => ({ name: t.exposeString('name'), roster: t.relatedConnection('players', { totalCount: true, }), }), }); ``` ```graphql query { node(id: "...") { ... on Team { roster(first: 10) { totalCount edges { node { id name } } } } } } ``` ## Page-size limits [#page-size-limits] `defaultSize` sets the page size when the client passes neither `first` nor `last`; `maxSize` caps how many nodes a single page can return. Both accept a plain number or a function of `(args, ctx)`: ```typescript roster: t.relatedConnection('players', { defaultSize: 20, maxSize: 100, }); ``` To set them for every connection at once, use the `maxConnectionSize` and `defaultConnectionSize` options in the [plugin options](./setup). A per-field `defaultSize` or `maxSize` overrides the global default. ## A connection as an entry point [#a-connection-as-an-entry-point] `t.drizzleConnection` defines a connection field that's a way into your Drizzle data, the connection equivalent of [`t.drizzleField`](./objects). Its resolver receives a `query` function you call and pass to `findMany`; `query` merges the pagination window and nested selection with any `where` and `orderBy` you add. The `orderBy` uses the same object form as `t.relatedConnection`: ```typescript builder.queryFields((t) => ({ players: t.drizzleConnection({ type: 'players', resolve: (query, _root, _args, _ctx) => db.query.players.findMany( query({ orderBy: { number: 'asc', }, }), ), }), })); ``` Add a `totalCount` callback to include a total count. It receives the standard resolver arguments `(parent, args, context, info)`, so the count can depend on request context. The example uses `db.$count`, but any Drizzle count works: ```typescript import { players } from './db/schema'; builder.queryFields((t) => ({ players: t.drizzleConnection({ type: 'players', totalCount: () => db.$count(players), resolve: (query) => db.query.players.findMany( query({ orderBy: { number: 'asc', }, }), ), }), })); ``` When only `totalCount` is requested, without `edges` or `nodes`, the plugin skips the main query and runs only the count. # Drizzle plugin URL: /docs/plugins/drizzle Define GraphQL types from Drizzle tables and resolve relations with selection-aware, efficient queries. The Drizzle plugin builds GraphQL object types straight from your Drizzle tables and resolves their relations with queries it plans for you. You call `builder.drizzleObject` with a table name, expose the columns you want, and add relation fields with `t.relation`, and the plugin reads each nested GraphQL selection to build a single query scoped to it, so a column loads only when a client asks for it. The plugin builds on Drizzle's [relational query builder](https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v2), so you define your tables and relations in Drizzle first, then hand them to Pothos. You don't have to use the plugin to use Drizzle with Pothos, but it handles a lot of the wiring and query planning for you; see [Using Drizzle without a plugin](./without-a-plugin) for the manual approach. This package is new and depends on Drizzle's [RQB v2 API](https://rqbv2.drizzle-orm-fe.pages.dev/docs/rqb-v2). Some features are still missing and the API may change. It currently requires the `beta` tag for `drizzle-orm`. Upgrading from an older version? Read the [Drizzle relations migration guide](https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v1-v2) and check this package's changelog for Pothos-specific changes. ## What it does [#what-it-does] * Define GraphQL types from your Drizzle tables with full type-safety, without hand-writing object refs or importing table types. * Resolve relations automatically from the relations you declared with `defineRelations`. * Load exactly the data a query needs in as few round-trips as possible, folding nested relations into a single query where it can. * Keep GraphQL type and field names independent of your column names. * Integrate with the [Relay plugin](./relay) for nodes and connections that paginate efficiently. * Back multiple GraphQL types with the same table through [variants](./variants). * Add relation [count fields](./relations#relation-counts) and other [derived fields](./relations#derived-fields-with-relatedfield) from SQL. ## An example [#an-example] Here is a slice of an Ultimate League schema. It defines a `Team`, exposes columns, computes a field from a related table, loads a relation, and adds a Relay connection — all against the [canonical Drizzle schema](./setup): ```typescript // A GraphQL type backed by the `teams` table — no object ref, no table imports. builder.drizzleObject('teams', { name: 'Team', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // A list relation, with an argument that shapes the relation query. players: t.relation('players', { args: { byNumber: t.arg.boolean(), }, query: (args) => ({ orderBy: args.byNumber ? { number: 'asc' } : { name: 'asc' }, }), }), // A Relay connection from the same relation. playersConnection: t.relatedConnection('players'), }), }); // A Relay node backed by the `games` table. builder.drizzleNode('games', { name: 'Game', id: { column: (game) => game.id }, fields: (t) => ({ playedAt: t.exposeString('playedAt'), homeTeam: t.relation('homeTeam'), }), }); builder.queryType({ fields: (t) => ({ // An entry point that issues a single optimized query. myTeam: t.drizzleField({ type: 'teams', resolve: (query, _root, _args, ctx) => db.query.teams.findFirst( // Calling `query()` adds the selection the plugin computed for the // nested request, resolving as much as possible in one round-trip. query({ where: { id: ctx.teamId }, }), ), }), }), }); ``` ## How the query plan works [#how-the-query-plan-works] Given the schema above, a nested query resolves in a **single** Drizzle call: ```graphql query { myTeam { name players { name stats { goals } } } } ``` The `myTeam` resolver receives a `query` carrying the selection needed to load `players` and their `stats` in one go. The plugin reads the GraphQL selection, folds it into the relational query, and never loads a column or relation the client did not ask for. Fields that can't fold into the parent query, such as a second copy of a relation with different arguments or a derived field computed in SQL, get their own scoped query, and the plugin plans that for you too. # Indirect relations URL: /docs/plugins/drizzle/indirect-relations Reach through join tables with through relations and the drizzleConnectionHelpers builder. `t.relation` and `t.relatedConnection` cover direct edges. Some fields reach a table two hops away through a join table, or paginate nodes that live a level deeper than the relation they hang off. Drizzle gives you two tools for this: a `.through()` relation that hides the join entirely, and `drizzleConnectionHelpers` for the cases a single relation can't express. This page assumes the [schema, relations, and builder](./setup) are already wired up. In the Ultimate League schema, `players` and `games` connect through the `playerStats` join table. ## Many-to-many with a through relation [#many-to-many-with-a-through-relation] Drizzle's [relational query builder](https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v2) can model a many-to-many directly. Point a relation's `from` and `to` at the join table with `.through()`, and the join disappears from the graph, so `players.games` reads as a plain list of games: ```typescript export const relations = defineRelations(schema, (r) => ({ players: { stats: r.many.playerStats({ from: r.players.id, to: r.playerStats.playerId }), // A many-to-many through the join table. games: r.many.games({ from: r.players.id.through(r.playerStats.playerId), to: r.games.id.through(r.playerStats.gameId), }), }, // ...games and playerStats relations as in the setup schema })); ``` A `through` relation behaves like any other, so `t.relation` and `t.relatedConnection` resolve it with no extra work: ```typescript builder.drizzleNode('players', { name: 'Player', id: { column: (player) => player.id }, fields: (t) => ({ name: t.exposeString('name'), // The games this player featured in — the playerStats join is invisible. games: t.relatedConnection('games'), }), }); ``` Use the helpers below only when the join row itself matters: when you need data from `playerStats` on the edge, or the node sits somewhere a single relation can't name. ## Paginating through a join table [#paginating-through-a-join-table] `drizzleConnectionHelpers` builds a connection with the plain [`t.connection`](./connections) API instead of `t.relatedConnection`. The first argument after `builder` is the join **table** name; `select` pulls in the node, and `resolveNode` maps each join row to it. Here pagination runs over a player's `stats`, but each node resolves to the `game` nested one hop deeper: ```typescript import { drizzleConnectionHelpers } from '@pothos/plugin-drizzle'; const Game = builder.drizzleObject('games', { name: 'Game', fields: (t) => ({ id: t.exposeID('id'), playedAt: t.exposeString('playedAt'), }), }); const statsConnection = drizzleConnectionHelpers(builder, 'playerStats', { // Select the data needed for the nodes; nestedSelection builds the node's own selection. select: (nestedSelection) => ({ with: { game: nestedSelection(), }, }), // Resolve the node from each returned join row. resolveNode: (stat) => stat.game, }); builder.drizzleObjectField('players', 'gamesConnection', (t) => t.connection({ type: Game, // Not t.relatedConnection, so include the selection manually. select: (args, ctx, nestedSelection) => ({ with: { stats: statsConnection.getQuery(args, ctx, nestedSelection), }, }), // Format the loaded join rows for the connection. resolve: (player, args, ctx) => statsConnection.resolve(player.stats, args, ctx, player), }), ); ``` Pagination args apply to the relation to the join table (`stats`); the nodes are the `game` nested inside each `playerStats` row. When the edge and node are the *same* table, with pagination happening directly on a relation to the node type, call the helper with no options and use its `ref` as the connection type: ```typescript const statHelpers = drizzleConnectionHelpers(builder, 'playerStats'); builder.drizzleObject('games', { name: 'Game', fields: (t) => ({ playedAt: t.exposeString('playedAt'), stats: t.connection({ type: statHelpers.ref, select: (args, ctx, nestedSelection) => ({ with: { stats: statHelpers.getQuery(args, ctx, nestedSelection), }, }), resolve: (game, args, ctx) => statHelpers.resolve(game.stats, args, ctx), }), }), }); ``` ## Arguments, ordering, and filtering [#arguments-ordering-and-filtering] Define extra args, a default order, and a filter on the helper itself. Add the helper's args to the field with `getArgs`: ```typescript const statsConnection = drizzleConnectionHelpers(builder, 'playerStats', { args: (t) => ({ scoredOnly: t.boolean({ defaultValue: false }), }), query: (args) => ({ // Default order. orderBy: { gameId: 'asc' }, // Default filter, driven by an arg. where: args.scoredOnly ? { goals: { gt: 0 } } : {}, }), select: (nestedSelection) => ({ with: { game: nestedSelection(), }, }), resolveNode: (stat) => stat.game, }); builder.drizzleObjectField('players', 'gamesConnection', (t) => t.connection({ type: Game, // Pull the helper's args onto the field. args: statsConnection.getArgs(), select: (args, ctx, nestedSelection) => ({ with: { stats: statsConnection.getQuery(args, ctx, nestedSelection), }, }), resolve: (player, args, ctx) => statsConnection.resolve(player.stats, args, ctx, player), }), ); ``` ## Fields on the edge [#fields-on-the-edge] To expose data from the join row on the edge itself, pass edge options as the third argument to `t.connection`. The edge's parent is the join row, so a `playerStats` field like `goals` is available there: ```typescript builder.drizzleObjectFields('players', (t) => ({ gamesConnection: t.connection( { type: Game, select: (args, ctx, nestedSelection) => ({ with: { stats: statsConnection.getQuery(args, ctx, nestedSelection), }, }), resolve: (player, args, ctx) => statsConnection.resolve(player.stats, args, ctx, player), }, {}, // Options for the edge object. { fields: (edge) => ({ goals: edge.field({ type: 'Int', resolve: (stat) => stat.goals, }), }), }, ), })); ``` ## Non-relation connections [#non-relation-connections] `drizzleConnectionHelpers` also builds connections where there's no direct relation to lean on, such as an entry-point connection that runs its own query. Merge the `where` clause the helper generates with any additional filter you apply, so the two don't clobber each other: ```typescript builder.queryFields((t) => ({ gamesForPlayer: t.connection({ type: Game, args: { playerId: t.arg.int({ required: true }), }, nodeNullable: true, resolve: async (_, args, ctx, info) => { const query = statsConnection.getQuery(args, ctx, info); const stats = await db.query.playerStats.findMany({ ...query, where: { ...query.where, playerId: args.playerId, }, }); return statsConnection.resolve(stats, args, ctx); }, }), })); ``` # Interfaces URL: /docs/plugins/drizzle/interfaces Define GraphQL interfaces for a Drizzle table and share them across variants. `builder.drizzleInterface` works exactly like [`builder.drizzleObject`](./objects), but produces a GraphQL interface instead of an object type. Like `drizzleObject`, it can define either a table's primary type (with `name`) or a [variant](./variants) (with `variant` in place of `name`). Use it to give several variants of one table a shared set of fields: the interface holds what they have in common, and each variant implements it and adds its own. This page assumes the [schema, relations, and builder](./setup) are already wired up. The examples add two columns to the `players` table, an `isCaptain` discriminator and an optional `bio`: ```typescript export const players = sqliteTable('players', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), number: integer('number').notNull(), isCaptain: integer('is_captain', { mode: 'boolean' }).notNull().default(false), bio: text('bio'), teamId: integer('team_id').notNull().references(() => teams.id), }); ``` ## An interface with two variants [#an-interface-with-two-variants] The interface defines the fields every player shares. A `resolveType` picks the concrete variant for a given row. Return the type name as a **string** rather than an object ref, which avoids circular-reference problems between the interface and the variants that implement it. ```typescript const Player = builder.drizzleInterface('players', { name: 'Player', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), resolveType: (player) => (player.isCaptain ? 'Captain' : 'SquadPlayer'), }); builder.drizzleObject('players', { variant: 'Captain', interfaces: [Player], fields: (t) => ({ isCaptain: t.exposeBoolean('isCaptain'), }), }); builder.drizzleObject('players', { variant: 'SquadPlayer', interfaces: [Player], fields: (t) => ({ bio: t.exposeString('bio', { nullable: true }), }), }); ``` Both `Captain` and `SquadPlayer` are variants of the same `players` table, so they inherit its backing shape. Each adds the fields specific to it on top of the interface's `id` and `name`. Selections are **not** inherited. Under [`select` mode](./selections), add the columns you need to both the interface *and* every implementing object type. Otherwise the object falls back to the default selection of all columns, which may not be what you want. ## Fields on the interface [#fields-on-the-interface] `builder.drizzleInterfaceField` and `builder.drizzleInterfaceFields` attach selection-aware fields to an interface after it's defined, the interface counterparts of [`drizzleObjectField(s)`](./objects). This is how you break a circular reference between an interface and a variant it links to, or add a relation the interface should carry: ```typescript builder.drizzleInterfaceField(Player, 'stats', (t) => t.relatedConnection('stats'), ); ``` ## One table per interface [#one-table-per-interface] A drizzle interface only spans the table it was defined on. Trying to have an object for a *different* table implement it fails at build time: ```typescript // Error at build time: teams is a different table than players. builder.drizzleObject('teams', { interfaces: [Player], fields: (t) => ({ id: t.exposeID('id') }), }); ``` # Drizzle objects URL: /docs/plugins/drizzle/objects Define GraphQL object types from Drizzle tables with drizzleObject and resolve to them with drizzleField. `builder.drizzleObject` defines a GraphQL object type backed by a Drizzle table. The first argument is the table name from your [relations](./setup); the options mirror any other object type. The difference is that Pothos already knows the row shape from your schema, so exposing columns and adding relations is fully typed without object refs or table imports. ```typescript const TeamRef = builder.drizzleObject('teams', { name: 'Team', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.drizzleObject('players', { name: 'Player', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), number: t.exposeInt('number'), }), }); ``` `drizzleObject` returns an object ref (`TeamRef` here) that behaves like any other [object ref](../../fundamentals/objects): use it as a field `type`, pass it to `t.variant`, or hand it to `t.drizzleField`. GraphQL field names are independent of column names; `t.exposeString('name')` maps a column to a field, and you can call the field whatever you like. ## Naming the type [#naming-the-type] `name` sets the GraphQL type name. Pass `variant` instead of `name` when one table backs [more than one GraphQL type](./variants), a public `Player` and a private `Viewer`, say: ```typescript builder.drizzleObject('players', { // The GraphQL type is `Viewer`, still backed by the players table. variant: 'Viewer', fields: (t) => ({ id: t.exposeID('id'), }), }); ``` ## Computed fields [#computed-fields] Fields often don't map to a single column. Add a resolver like you would on any Pothos object type; the row is the `parent`: ```typescript builder.drizzleObject('players', { name: 'Player', fields: (t) => ({ name: t.exposeString('name'), // The whole row is available on `parent` by default. label: t.string({ resolve: (player) => `#${player.number} ${player.name}`, }), }), }); ``` By default every column of the table is available on `parent`. To load only the columns a field needs, or to reach into a related table or a raw SQL expression, see [Selections](./selections). ## Resolving to a Drizzle type [#resolving-to-a-drizzle-type] `t.drizzleField` adds a field whose type is a Drizzle table, most often on `Query` or `Mutation`. Its resolver receives a `query` function that you **must** call and pass to a Drizzle `findFirst` or `findMany`: ```typescript builder.queryType({ fields: (t) => ({ team: t.drizzleField({ type: 'teams', args: { id: t.arg.id({ required: true }), }, resolve: (query, _root, args, _ctx) => db.query.teams.findFirst( query({ where: { id: Number(args.id) }, }), ), }), teams: t.drizzleField({ type: ['teams'], resolve: (query, _root, _args, _ctx) => db.query.teams.findMany(query()), }), }), }); ``` `t.drizzleField` differs from `t.field` in two ways: 1. **`type`** is a table name (`'teams'` for one record, `['teams']` for a list) or an object ref returned by `drizzleObject`. 2. **`resolve`** gets an extra first argument, `query`. Call it (optionally with your own `where`, `orderBy`, `limit`) and pass the result to the Drizzle query. It carries the selection the plugin computed for the nested part of the request, so relations and columns load in the same round-trip. Unlike the Prisma plugin's `query`, which you spread, Drizzle's `query` is a function. Call `query(options)` and hand the result to `findFirst`/`findMany`; spreading it, or forgetting to call it, drops the nested selection and under-fetches. You are not required to use `t.drizzleField` (a `drizzleObject` ref works with a plain `t.field` too), but only `t.drizzleField` (and `t.drizzleConnection`) gives you the `query` function that folds the nested selection into one query. ## Extending a Drizzle object [#extending-a-drizzle-object] The usual `builder.objectField` and `builder.objectFields` work on Drizzle objects, but they can't use [selections](./selections) or reach columns outside the default selection. To add a field that pulls in extra columns, a relation, or a connection, use `builder.drizzleObjectField` or `builder.drizzleObjectFields`, which hand you the selection-aware field builder: ```typescript builder.drizzleObjectField(TeamRef, 'playerCount', (t) => t.relatedCount('players'), ); ``` The first argument is the object ref (or the table name); the field builder `t` is the same one `drizzleObject`'s `fields` function receives, so [`t.relation`](./relations), [`t.relatedCount`](./relations#relation-counts), and field-level [`select`](./selections) are all available. # Relations URL: /docs/plugins/drizzle/relations Add relation fields with t.relation, shape them with query and args, and add relation counts and derived fields. `t.relation` adds a field for a relation you declared with [`defineRelations`](./setup). The plugin reads the relation from your schema, gives the field the correct type automatically, and folds it into the `query` of whichever [`t.drizzleField`](./objects#resolving-to-a-drizzle-type) started the request, so a chain of relations resolves without a query per level. ```typescript builder.drizzleObject('teams', { name: 'Team', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // A `many` relation becomes a list field. players: t.relation('players'), }), }); builder.drizzleObject('players', { name: 'Player', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // A `one` relation becomes a single field. team: t.relation('team'), }), }); ``` The plugin knows from your relations whether `players` is a `many` (a list field) or `team` is a `one` (a single field), and types the field accordingly. Nested relations fold into a single query: a request for a team, its players, and each player's stats reaches the database once. ## Filtering, ordering, and arguments [#filtering-ordering-and-arguments] `t.drizzleField` takes arguments and you write its query yourself. `t.relation` is different: the planner writes the query, so you shape the relation with a `query` option. It's either a query object or a function of the field's arguments, the request context, and the query path: ```typescript builder.drizzleObject('teams', { name: 'Team', fields: (t) => ({ id: t.exposeID('id'), // A relation with client-driven paging and a fixed order. players: t.relation('players', { args: { limit: t.arg.int(), offset: t.arg.int(), }, query: (args) => ({ limit: args.limit ?? 10, offset: args.offset ?? 0, orderBy: { number: 'asc' }, }), }), // The same relation, shaped a second way. captains: t.relation('players', { query: { where: { number: 1 }, }, }), }), }); ``` The object maps straight onto Drizzle's relational query builder, so it accepts the usual keys: `where`, `orderBy`, `limit`, `offset`. The callback receives the field arguments, the request context, and a `pathInfo` object carrying the GraphQL query `path` and `segments`. It does **not** receive the parent row: the relation is pre-loaded before the parent exists, which is exactly what keeps a list of parents from triggering a query each. See the [relational query docs](https://rqbv2.drizzle-orm-fe.pages.dev/docs/rqb-v2) for the full set of `query` keys. ### Pointing a relation at a variant [#pointing-a-relation-at-a-variant] When a table backs [more than one GraphQL type](./variants), pass `type` to resolve a relation to a specific variant's ref instead of the table's default type: ```typescript players: t.relation('players', { type: Viewer, }); ``` ## Relation counts [#relation-counts] Counting related records is common enough to have a dedicated `t.relatedCount`. With no options it counts every related row; a `where` narrows it: ```typescript import { gt } from 'drizzle-orm'; builder.drizzleObject('players', { name: 'Player', fields: (t) => ({ name: t.exposeString('name'), // Count of every related stat line. appearances: t.relatedCount('stats'), // Count with a static filter. scoringGames: t.relatedCount('stats', { where: gt(playerStats.goals, 0), }), }), }); ``` `where` accepts a static SQL filter or a function of the field's args and context, so the count can respond to input: ```typescript import { and, eq, gt } from 'drizzle-orm'; scoringGames: t.relatedCount('stats', { args: { inGame: t.arg.int(), }, where: (args, _ctx) => args.inGame ? and(gt(playerStats.goals, 0), eq(playerStats.gameId, args.inGame)) : gt(playerStats.goals, 0), }); ``` Under the hood `t.relatedCount` issues a `db.$count` scoped to the related rows, run as a subquery within the main query. ## Derived fields with relatedField [#derived-fields-with-relatedfield] `t.relatedCount` is a shorthand for the more general `t.relatedField`, which defines a field from a relation using a custom selection, useful for any aggregate or derived value you'd rather compute in SQL than by loading the full related rows. The `select` callback receives a `buildFilter` helper that produces the `WHERE` clause matching the relation, so you can scope a query to exactly the related records: ```typescript import { sql } from 'drizzle-orm'; builder.drizzleObject('players', { name: 'Player', fields: (t) => ({ name: t.exposeString('name'), // Total goals across every related stat line, computed in SQL. totalGoals: t.relatedField('stats', { type: 'Int', select: (buildFilter) => ({ extras: { // buildFilter(parent) is the WHERE that matches this player's stats. totalGoals: (parent) => sql`(select coalesce(sum(${playerStats.goals}), 0) from ${playerStats} where ${buildFilter(parent)})`, }, }), resolve: (player) => player.totalGoals, }), }), }); ``` `select` also receives the field's `args`, the context, and a `nestedQuery` helper as later arguments. The value it computes lands on `parent` under the key you gave it in `extras`, and `resolve` reads it back. This is `t.field` under the hood; `buildFilter` just makes it easy to scope the query to the related records. # Relay nodes URL: /docs/plugins/drizzle/relay Turn a Drizzle table into a Relay node with global IDs, composite keys, and efficient node(id) lookups. The Drizzle plugin wires into the [Relay plugin](../relay) so a Drizzle table becomes a Relay node with a global ID and an efficient `node(id: ID!)` lookup. `builder.drizzleNode` takes the place of `builder.drizzleObject`: it defines the same object type and, on top of it, implements the `Node` interface and the `id` field for you. These examples assume the builder is set up with both `DrizzlePlugin` and `RelayPlugin`, a `db` client in scope, and the league schema and relations from [Setup](./setup). `drizzleNode` throws at build time unless [`@pothos/plugin-relay`](../relay) is registered on the builder. Add `RelayPlugin` to `plugins` before you use it. ## Defining a node [#defining-a-node] `drizzleNode` takes the same options as [`drizzleObject`](./objects) plus one required `id` option. Its `column` names the database column that backs the node's global ID, and Pothos derives both the encoded ID and the `node(id:)` lookup from it. The `column` is a function of the table's columns: ```typescript builder.drizzleNode('players', { name: 'Player', id: { // Which column backs the node's global id. column: (player) => player.id, }, // fields work exactly like builder.drizzleObject. fields: (t) => ({ name: t.exposeString('name'), number: t.exposeInt('number'), team: t.relation('team'), }), }); ``` With the `id` set, the `node(id:)` query loads the record keyed on that column through the plugin's selection-aware loader, with no resolver to write, and the load joins into the same query as the rest of the request. ## Composite primary keys [#composite-primary-keys] When a table's primary key spans more than one column, common for join tables keyed on the two rows they connect, pass an array of columns. Pothos packs all of them into the global ID and unpacks them on lookup: ```typescript builder.drizzleNode('playerStats', { name: 'PlayerStat', id: { // A composite key over the two foreign keys. column: (stat) => [stat.playerId, stat.gameId], }, fields: (t) => ({ goals: t.exposeInt('goals'), assists: t.exposeInt('assists'), }), }); ``` ## Customizing the id field [#customizing-the-id-field] The rest of the `id` option is passed straight to the generated global-ID field, so you can set anything a normal field takes, such as a `description`. The plugin fixes `column`, `type`, `nullable`, and the field's `args`, so those aren't yours to override: ```typescript builder.drizzleNode('players', { name: 'Player', id: { column: (player) => player.id, description: 'The global Relay ID for this player.', }, fields: (t) => ({ name: t.exposeString('name'), }), }); ``` ## Variants carry over [#variants-carry-over] Every `drizzleObject` option works on `drizzleNode`, including `name` and `variant` for exposing [multiple GraphQL types from one table](./variants). A node can be the private `Viewer` view of a row just as easily as the public one; set `variant` in place of `name`. # Selections URL: /docs/plugins/drizzle/selections Control which columns, relations, and SQL expressions Drizzle loads with select and field-level selections. By default a `drizzleObject` loads every column of its table and pre-loads only the relations a query touches. A `select` option tunes that: narrow a wide table to the columns you expose, pre-load a relation every time, or add a raw SQL expression to the row. `select` has three parts: `columns`, related tables via `with`, and computed SQL via `extras`. ```typescript builder.drizzleObject('players', { name: 'Player', select: { columns: { name: true, number: true, }, // Pre-load a relation so every resolver on this type can read it. with: { team: true, }, // A raw SQL column, available on every row of this type. extras: { lowercaseName: (players, { sql }) => sql`lower(${players.name})`, }, }, fields: (t) => ({ number: t.exposeInt('number'), label: t.string({ resolve: (player) => `#${player.number} ${player.name}`, }), teamName: t.string({ resolve: (player) => player.team.name, }), slug: t.string({ resolve: (player) => player.lowercaseName.replace(/\s+/g, '-'), }), }), }); ``` Anything selected on the type is available in every resolver on that type. `extras` is the piece with no Prisma equivalent: a map of names to `(table, { sql }) => sql` builders that add computed columns straight from SQL. ## Default selection [#default-selection] The `select` option changes what loads by default: * **Omit `select`**: every column loads. Convenient, and fine for narrow tables. * **`select: {}`**: nothing loads by default. Each field adds only what it needs, so the database returns the minimum for a given request. * **`select: { columns: { ... } }`**: exactly the listed columns load on every request. Whichever you pick, `t.expose*` and `t.relation` still work: the plugin adds a column or relation to the query **when its field is queried**, on top of the default selection. So a `select: {}` type stays lean, and exposing a column you didn't select just pulls it in when a client asks for it. ## Per-field selections [#per-field-selections] A type-level `select` loads its columns for every request. To load a column, relation, or SQL expression only when a specific field is queried, put `select` on the field instead. This keeps each field's cost tied to whether the client asks for it: ```typescript builder.drizzleObject('players', { name: 'Player', select: {}, fields: (t) => ({ name: t.exposeString('name'), // name + number load only when `label` is queried. label: t.string({ select: { columns: { name: true, number: true }, }, resolve: (player) => `#${player.number} ${player.name}`, }), // The team relation loads only when `teamName` is queried. teamName: t.string({ select: { with: { team: true }, }, resolve: (player) => player.team.name, }), // The SQL expression is computed only when `slug` is queried. slug: t.string({ select: { extras: { lowercaseName: (players, { sql }) => sql`lower(${players.name})`, }, }, resolve: (player) => player.lowercaseName.replace(/\s+/g, '-'), }), }), }); ``` ## Selections from arguments or context [#selections-from-arguments-or-context] A field-level `select` can be a function of the field's arguments and context, so a selection responds to input. This field takes a game id and pre-loads only the stats recorded in that game: ```typescript builder.drizzleObject('players', { name: 'Player', select: {}, fields: (t) => ({ name: t.exposeString('name'), goalsInGame: t.int({ args: { gameId: t.arg.int({ required: true }), }, select: (args) => ({ with: { stats: { where: { gameId: args.gameId }, }, }, }), resolve: (player) => player.stats.reduce((sum, s) => sum + s.goals, 0), }), }), }); ``` # Setup URL: /docs/plugins/drizzle/setup Define your Drizzle schema and relations, then wire the client and DrizzleRelations into the builder. The Drizzle plugin reads your Drizzle relational schema to understand your tables. You define the tables, describe their relations with `defineRelations`, then hand the client, a dialect-specific `getTableConfig`, and the relations type to the builder. Everything else on these pages assumes this wiring is in place. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-drizzle drizzle-orm@beta ``` ```bash pnpm add @pothos/plugin-drizzle drizzle-orm@beta ``` ```bash yarn add @pothos/plugin-drizzle drizzle-orm@beta ``` ```bash bun add @pothos/plugin-drizzle drizzle-orm@beta ``` The plugin depends on Drizzle's [RQB v2 API](https://rqbv2.drizzle-orm-fe.pages.dev/docs/rqb-v2), which currently ships under the `beta` tag for `drizzle-orm`. If you are moving from an earlier version, the relations format changed, so read the [relations v1 to v2 migration guide](https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v1-v2) before wiring up the builder. ## Define the schema [#define-the-schema] This is the Ultimate League schema that runs through every Drizzle page. It uses SQLite, matching the dialect the plugin's own tests exercise: ```typescript // db/schema.ts import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; export const teams = sqliteTable('teams', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull().unique(), }); export const players = sqliteTable('players', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), number: integer('number').notNull(), teamId: integer('team_id') .notNull() .references(() => teams.id), }); export const games = sqliteTable('games', { id: integer('id').primaryKey({ autoIncrement: true }), playedAt: text('played_at').notNull(), homeTeamId: integer('home_team_id') .notNull() .references(() => teams.id), awayTeamId: integer('away_team_id') .notNull() .references(() => teams.id), }); export const playerStats = sqliteTable('player_stats', { id: integer('id').primaryKey({ autoIncrement: true }), goals: integer('goals').notNull(), assists: integer('assists').notNull(), playerId: integer('player_id') .notNull() .references(() => players.id), gameId: integer('game_id') .notNull() .references(() => games.id), }); ``` ## Describe the relations [#describe-the-relations] The plugin resolves `t.relation` fields from relations you declare with `defineRelations`; there is no code generation step. Each relation names its `from` and `to` columns. A `.through()` clause turns the `player_stats` join table into a direct many-to-many between players and games, which the [indirect relations](./indirect-relations) page builds on: ```typescript // db/relations.ts import { defineRelations } from 'drizzle-orm'; import * as schema from './schema'; export const relations = defineRelations(schema, (r) => ({ teams: { players: r.many.players({ from: r.teams.id, to: r.players.teamId }), homeGames: r.many.games({ from: r.teams.id, to: r.games.homeTeamId }), }, players: { team: r.one.teams({ from: r.players.teamId, to: r.teams.id }), stats: r.many.playerStats({ from: r.players.id, to: r.playerStats.playerId }), games: r.many.games({ from: r.players.id.through(r.playerStats.playerId), to: r.games.id.through(r.playerStats.gameId), }), }, games: { homeTeam: r.one.teams({ from: r.games.homeTeamId, to: r.teams.id }), stats: r.many.playerStats({ from: r.games.id, to: r.playerStats.gameId }), }, playerStats: { player: r.one.players({ from: r.playerStats.playerId, to: r.players.id }), game: r.one.games({ from: r.playerStats.gameId, to: r.games.id }), }, })); ``` ## Wire up the builder [#wire-up-the-builder] Create the Drizzle client with these `relations`, register the plugin, expose the relations type through `DrizzleRelations` so Pothos can infer table shapes, and pass a `drizzle` config with the `client`, the dialect's `getTableConfig`, and the `relations`: ```typescript import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; // Import getTableConfig from the core package for your dialect. import { getTableConfig } from 'drizzle-orm/sqlite-core'; import SchemaBuilder from '@pothos/core'; import DrizzlePlugin from '@pothos/plugin-drizzle'; import { relations } from './db/relations'; export const db = drizzle({ client: new Database('league.db'), relations }); const builder = new SchemaBuilder<{ // Gives the builder full type information about your Drizzle schema. DrizzleRelations: typeof relations; }>({ plugins: [DrizzlePlugin], drizzle: { client: db, getTableConfig, relations, }, }); ``` The rest of the Drizzle pages assume this `builder` and this module-level `db` client already exist. Import `getTableConfig` from the package that matches your database: `drizzle-orm/sqlite-core`, `drizzle-orm/pg-core`, or `drizzle-orm/mysql-core`. The plugin uses it to read primary keys and column metadata, so the wrong dialect's import produces type errors on the config. ### Drizzle config options [#drizzle-config-options] The `drizzle` object accepts: | Option | Purpose | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `client` | The Drizzle client, or a function `(ctx) => client` to pick a client per request. | | `getTableConfig` | The dialect-specific helper (`sqlite-core`, `pg-core`, or `mysql-core`). Required so the plugin can read table metadata. | | `relations` | The object returned by `defineRelations`. Required when `client` is a function; otherwise inferred from the client. | | `maxConnectionSize` | Upper bound on the `first`/`last` a [Relay connection](./connections) will honor. | | `defaultConnectionSize` | Page size a connection uses when the client passes no `first`/`last`. | | `skipDeferredFragments` | Skip `@defer` fragments when planning the query, so deferred fields aren't fetched eagerly. Defaults to `true`. | ### A client per request [#a-client-per-request] Pass a function for `client` to choose a client from context, useful for per-request transactions, connection scoping, or a read-only replica for some users: ```typescript const builder = new SchemaBuilder<{ Context: { user: { isAdmin: boolean } }; DrizzleRelations: typeof relations; }>({ plugins: [DrizzlePlugin], drizzle: { client: (ctx) => (ctx.user.isAdmin ? db : readOnlyDb), getTableConfig, relations, }, }); ``` When `client` is a function, `relations` is required in the config, since the plugin can no longer read it off a fixed client. ## Working with Relay and With-Input [#working-with-relay-and-with-input] The Drizzle plugin composes with other Pothos plugins. [Relay](./relay) is required for [nodes](./relay) and [connections](./connections): `builder.drizzleNode`, `t.relatedConnection`, and `t.drizzleConnection` throw without it. [With-input](../with-input) is required for `t.drizzleFieldWithInput`. Register them before the Drizzle plugin: ```typescript import RelayPlugin from '@pothos/plugin-relay'; import WithInputPlugin from '@pothos/plugin-with-input'; const builder = new SchemaBuilder<{ DrizzleRelations: typeof relations; }>({ plugins: [RelayPlugin, WithInputPlugin, DrizzlePlugin], drizzle: { client: db, getTableConfig, relations, }, }); ``` # Type variants URL: /docs/plugins/drizzle/variants Expose one Drizzle table as several GraphQL types with the variant option. One Drizzle table often needs to appear in the schema as more than one GraphQL type: a public view and a private one, a full record and a lightweight card. Pothos calls these **variants**. Every table has one primary type (defined with a `name`, as on the [objects page](./objects)); each additional variant is defined with a `variant` option in its place. This page assumes the [schema, relations, and builder](./setup) are already wired up. The examples add an optional `email` column to the `players` table so a private variant has something to guard: ```typescript export const players = sqliteTable('players', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), number: integer('number').notNull(), email: text('email'), teamId: integer('team_id').notNull().references(() => teams.id), }); ``` ## Defining a variant [#defining-a-variant] Give `variant` a type name instead of `name`. Here `PlayerPrivateInfo` is a second GraphQL type over the same `players` table, exposing a field the public type shouldn't: ```typescript const PlayerPrivateInfo = builder.drizzleObject('players', { variant: 'PlayerPrivateInfo', select: {}, fields: (t) => ({ id: t.exposeID('id'), email: t.exposeString('email', { nullable: true }), }), }); ``` ## Linking variants together [#linking-variants-together] `t.variant` adds a field that returns another variant of the same row. Reference the **primary** variant by its table name as a string; reference any **other** variant by the object ref it returned. An `isNull` callback can hide the variant when it shouldn't be visible. Here the private info resolves to `null` unless the parent player is the current viewer: ```typescript const PlayerPrivateInfo = builder.drizzleObject('players', { variant: 'PlayerPrivateInfo', select: {}, fields: (t) => ({ email: t.exposeString('email', { nullable: true }), // The table name references the primary variant. player: t.variant('players'), }), }); const Player = builder.drizzleNode('players', { name: 'Player', id: { column: (player) => player.id }, fields: (t) => ({ name: t.exposeString('name'), // Reference another variant by its ref, not the table name. privateInfo: t.variant(PlayerPrivateInfo, { // Hide private info unless the parent player is the current viewer. isNull: (player, args, ctx) => player.id !== ctx.currentPlayerId, }), }), }); ``` `builder.drizzleNode` needs the [relay plugin](./relay) and takes an `id` option: the column (or columns) that back the node's global id. A variant that doesn't need to be a Relay node can use `builder.drizzleObject` instead. ## Variants on relations [#variants-on-relations] A relation field can return a variant rather than the related table's primary type. Pass the variant ref as the relation's `type`, and use `query` to scope which rows it loads. Here a team's `schedule` returns games through a `CompletedGame` variant, filtered to games already played: ```typescript const CompletedGame = builder.drizzleNode('games', { variant: 'CompletedGame', id: { column: (game) => game.id }, fields: (t) => ({ playedAt: t.exposeString('playedAt'), homeTeam: t.relation('homeTeam'), }), }); const TeamSchedule = builder.drizzleObject('teams', { variant: 'TeamSchedule', fields: (t) => ({ id: t.exposeID('id'), schedule: t.relation('homeGames', { // Use the CompletedGame variant for this relation instead of the default Game. type: CompletedGame, query: { where: { playedAt: { lt: new Date().toISOString() } } }, }), }), }); ``` ## Breaking circular references [#breaking-circular-references] Two drizzle object refs that reference each other in their `fields` functions can trip TypeScript into a circular-type error. Split one side out with `builder.drizzleObjectField`, which attaches a single field after both refs exist. It takes the ref (or type name), the field name, and a field function: ```typescript const PlayerPrivateInfo = builder.drizzleObject('players', { variant: 'PlayerPrivateInfo', select: {}, fields: (t) => ({ email: t.exposeString('email', { nullable: true }), }), }); const Player = builder.drizzleNode('players', { name: 'Player', id: { column: (player) => player.id }, fields: (t) => ({ name: t.exposeString('name'), }), }); // Attach the back-reference after both refs exist, breaking the cycle. builder.drizzleObjectField(PlayerPrivateInfo, 'player', (t) => t.variant(Player)); ``` The same workaround applies to relations that use variants: move the offending relation field into a `drizzleObjectField` call. # Drizzle without a plugin URL: /docs/plugins/drizzle/without-a-plugin Back GraphQL objects with Drizzle rows using plain objectRef, and tame the resulting N+1 queries. You don't need the [Drizzle plugin](./setup) to put Drizzle rows behind a GraphQL schema. `builder.objectRef` takes any TypeScript shape as its backing model, and a Drizzle row is just a shape; `InferSelectModel` gives you the type of a `select` from a table. Point a ref at it and resolve relations with ordinary query-builder calls. You give up the plugin's automatic query-planning, but the code stays plain Pothos. ```typescript import { InferSelectModel } from 'drizzle-orm'; import { players, teams } from './db/schema'; type TeamRow = InferSelectModel; type PlayerRow = InferSelectModel; const TeamObject = builder.objectRef('Team'); const PlayerObject = builder.objectRef('Player'); TeamObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), players: t.field({ type: [PlayerObject], resolve: (team) => db.query.players.findMany({ where: { teamId: team.id } }), }), }), }); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, resolve: async (player) => { const team = await db.query.teams.findFirst({ where: { id: player.teamId } }); if (!team) throw new Error(`Team ${player.teamId} not found`); return team; }, }), }), }); builder.queryType({ fields: (t) => ({ myTeam: t.field({ type: TeamObject, resolve: async (_root, _args, ctx) => { const team = await db.query.teams.findFirst({ where: { id: ctx.teamId } }); if (!team) throw new Error('Team not found'); return team; }, }), }), }); ``` This defines `Team` and `Player` objects with a relation each, plus a `myTeam` query for the viewer's team. Three details make it work: * **Split the ref from `implement`.** Declaring `TeamObject`/`PlayerObject` up front and calling `implement` afterwards, rather than `builder.objectRef(...).implement(...)` in one expression, keeps TypeScript from choking on the circular reference between teams and players. * **Throw for non-null fields.** `team` and `myTeam` are non-nullable, so they must never resolve to `null`. Drizzle's `findFirst` returns `undefined` when nothing matches, so throw explicitly. Mark the field `nullable` instead if a missing row is a valid result. * **Ref names vs. type names.** The refs are `TeamObject`/`PlayerObject` because `TeamRow`/`PlayerRow` name the backing shapes. Give the refs the GraphQL type names directly if you'd rather. ## Cutting down N+1 queries [#cutting-down-n1-queries] The schema above issues one query per relation edge. Fetch a team, then its players, then each player's team, and the round-trips multiply. You can shape the backing model to avoid the round-trip entirely. If you almost always load a player's team alongside the player, fold the team into the backing shape and have the parent resolver load it with `with`: ```typescript const TeamObject = builder.objectRef('Team'); // Widen the backing model so a Player always carries its Team. const PlayerObject = builder.objectRef('Player'); TeamObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), players: t.field({ type: [PlayerObject], resolve: (team) => db.query.players.findMany({ // Load the team so the child resolver has it already. with: { team: true }, where: { teamId: team.id }, }), }), }), }); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, // No query — the team came along with the player. resolve: (player) => player.team, }), }), }); ``` Requiring `team` on every `Player` is a strong claim: every resolver that produces a player now owes you the `with`. When only some paths can supply it, make the field optional and fall back to a query: ```typescript const PlayerObject = builder.objectRef('Player'); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, resolve: async (player) => { if (player.team) return player.team; const team = await db.query.teams.findFirst({ where: { id: player.teamId } }); if (!team) throw new Error(`Team ${player.teamId} not found`); return team; }, }), }), }); ``` Now a parent resolver *may* pre-load the team, and the field still resolves correctly when it doesn't. A [dataloader](../dataloader) is another lever for N+1, batching the per-player team lookups into one query. The [Drizzle plugin](./setup) is a third option: `t.relation` and `t.drizzleField` read the GraphQL selection set and build a single nested query, so you don't shape the backing model by hand. # Connections URL: /docs/plugins/prisma/connections Build Relay connections over Prisma models with cursor pagination, total counts, and shared connection objects. Relay connections give you cursor-based pagination over a list. The Prisma plugin implements them on top of Prisma's own cursor pagination, and pre-loads the data nested inside each page in the same optimized query as the rest of the request. Use `t.prismaConnection` on a root field, `t.relatedConnection` for a relation of a [node](./relay), and `prismaConnectionHelpers` when the list lives behind a join table. These examples assume the builder is set up with `PrismaPlugin` and `RelayPlugin` and a `prisma` client in scope (see [Setup](./setup)). ## A connection on a root field [#a-connection-on-a-root-field] `t.prismaConnection` defines a Relay `connection` field and preloads everything the page needs. The resolver receives a `query` object as its first argument. Spread it into your Prisma call; it carries the correct `take`, `skip`, and `cursor` derived from the connection arguments, plus the `include`/`select` for nested selections. ```typescript builder.queryType({ fields: (t) => ({ players: t.prismaConnection( { type: 'Player', cursor: 'id', resolve: (query, _parent, _args, _ctx, _info) => prisma.player.findMany({ ...query }), }, {}, // optional options for the Connection type {}, // optional options for the Edge type ), }), }); ``` The three arguments are the field options, then optional options for the generated Connection type, then optional options for the Edge type. | Option | Purpose | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Name of the Prisma model being connected to. | | `cursor` | A `@unique` column (or unique index) of that model, passed to Prisma as the `cursor`. | | `resolve` | Like the [`prismaField`](./objects) resolver: spread the first `query` argument into your Prisma query and return the array of nodes. | | `defaultSize` | Page size when neither `first` nor `last` is given. Default `20`. | | `maxSize` | Maximum number of nodes returned. Default `100`. | | `totalCount` | A function `(parent, args, context, info)` that loads the total count and adds a `totalCount` field to the connection. Its `parent` is the connection field's parent, like the [`prismaField`](./objects) resolver. Does not apply with a shared connection object (see [Total count on shared objects](#total-count-on-shared-connection-objects)). | `defaultSize` and `maxSize` accept a plain number or a function of `(args, context)`. You can also set them for every connection at once with the `maxConnectionSize` and `defaultConnectionSize` options in the [`prisma` plugin options](./setup). Prisma-backed connections support only the argument combinations that map to an efficient cursor query: `first`, `last`, or `before` on their own; `first` with `before`; and `last` with `after`. Other combinations would require loading every record between two cursors (or between a cursor and the end of the set), which is complex and inefficient, so they throw an error indicating the combination is unsupported. ## A connection from a relation [#a-connection-from-a-relation] `t.relatedConnection` builds a connection from a relation of the current model, with no resolver needed since the relation names the data. It works on any Prisma object, and pairs naturally with a [node](./relay). ```typescript builder.prismaNode('Team', { id: { field: 'id' }, fields: (t) => ({ name: t.exposeString('name'), // The simplest form: just a cursor. roster: t.relatedConnection('players', { cursor: 'id', }), // Or add arguments and a custom query merged into the relation. players: t.relatedConnection( 'players', { cursor: 'id', args: { sortByNumber: t.arg.boolean(), }, query: (args, _context) => ({ orderBy: { number: args.sortByNumber ? 'asc' : 'desc', }, }), }, {}, // optional options for the Connection type {}, // optional options for the Edge type ), }), }); ``` | Option | Purpose | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `cursor` | A `@unique` column of the related model, passed to Prisma as the `cursor`. | | `defaultSize` | Page size when neither `first` nor `last` is given. Default `20`. | | `maxSize` | Maximum number of nodes returned. Default `100`. | | `query` | A function of `(args, context)` returning filtering and sorting merged into the query for the relation. | | `totalCount` | Set `true` to add a `totalCount` field to the connection, like [`relationCount`](./relations). Does not apply when using a shared connection object. | ## Indirect relations as connections [#indirect-relations-as-connections] When the list you want to paginate lives behind a join table, `t.relatedConnection` doesn't fit, because you paginate the join rows but return the nodes nested one level deeper. `prismaConnectionHelpers` handles this with a plain `t.connection` field. In the league schema, `PlayerStat` joins `Player` to `Game`: paginating a player's games means paginating that player's `stats` and resolving each edge to its `game`. ```typescript // The node type of the connection. const Game = builder.prismaObject('Game', { select: { id: true, }, fields: (t) => ({ playedAt: t.expose('playedAt', { type: 'DateTime' }), }), }); // Connection helpers for the join table let you use a plain t.connection. const gameConnectionHelpers = prismaConnectionHelpers( builder, 'PlayerStat', // the join table { cursor: 'id', select: (nodeSelection) => ({ // Select the relation to the node using nodeSelection. game: nodeSelection({ // Fields to select by default for the node. select: { id: true, }, }), }), // Resolve the node from the edge (join) row. resolveNode: (playerStat) => playerStat.game, // Optional size limits, like the connection fields above. maxSize: 100, defaultSize: 20, }, ); builder.prismaObjectField('Player', 'gamesConnection', (t) => t.connection({ type: Game, // Not using t.relatedConnection, so include the selection manually. select: (args, ctx, nestedSelection) => ({ stats: gameConnectionHelpers.getQuery(args, ctx, nestedSelection), }), resolve: (player, args, ctx) => // Format the loaded join rows into the connection. gameConnectionHelpers.resolve(player.stats, args, ctx), }), ); ``` The helper exposes `getQuery` (build the Prisma query for the relation), `resolve` (format a list of loaded rows into a connection), `ref` (the connection's node ref), and `getArgs` (below). `prismaConnectionHelpers` also covers the case where the edge and connection share the same model and pagination happens directly on a relation to the nodes (even a nested one). Pass the helper's `ref` as the connection `type`: ```typescript const playerConnectionHelpers = prismaConnectionHelpers(builder, 'Player', { cursor: 'id', }); const SelectTeam = builder.prismaObject('Team', { fields: (t) => ({ name: t.exposeString('name'), players: t.connection({ type: playerConnectionHelpers.ref, select: (args, ctx, nestedSelection) => ({ players: playerConnectionHelpers.getQuery(args, ctx, nestedSelection), }), resolve: (parent, args, ctx) => playerConnectionHelpers.resolve(parent.players, args, ctx), }), }), }); ``` ### Adding arguments [#adding-arguments] To add arguments to a helper-based connection, the easiest place is the connection field itself. Defining them there lets one helper be shared across fields that don't share the same arguments: ```typescript const gameConnectionHelpers = prismaConnectionHelpers(builder, 'PlayerStat', { cursor: 'id', select: (nodeSelection) => ({ game: nodeSelection({}), }), resolveNode: (playerStat) => playerStat.game, }); builder.prismaObjectField('Player', 'gamesConnection', (t) => t.connection({ type: Game, args: { recentFirst: t.arg.boolean(), }, select: (args, ctx, nestedSelection) => ({ stats: { ...gameConnectionHelpers.getQuery(args, ctx, nestedSelection), orderBy: { game: { playedAt: args.recentFirst ? 'desc' : 'asc', }, }, }, }), resolve: (player, args, ctx) => gameConnectionHelpers.resolve(player.stats, args, ctx), }), ); ``` Arguments, ordering, and filtering can also live on the helper itself. Args defined there are available as the second argument of `select`, and `getArgs()` adds them to the field: ```typescript const gameConnectionHelpers = prismaConnectionHelpers(builder, 'PlayerStat', { cursor: 'id', // Arguments for the helper, available as the second argument of `select`. args: (t) => ({ recentFirst: t.arg.boolean(), }), select: (nodeSelection, _args) => ({ game: nodeSelection({}), }), query: (args) => ({ // Custom filtering with a where clause. where: { game: { stats: { some: {} }, }, }, // Custom ordering using the args. orderBy: { game: { playedAt: args.recentFirst ? 'desc' : 'asc', }, }, }), resolveNode: (playerStat) => playerStat.game, }); builder.prismaObjectField('Player', 'gamesConnection', (t) => t.connection({ type: Game, // Pull the helper's args onto the field. args: gameConnectionHelpers.getArgs(), select: (args, ctx, nestedSelection) => ({ stats: gameConnectionHelpers.getQuery(args, ctx, nestedSelection), }), resolve: (player, args, ctx) => gameConnectionHelpers.resolve(player.stats, args, ctx), }), ); ``` ## Sharing connection objects [#sharing-connection-objects] By default each connection field generates its own Connection and Edge types. To reuse one across fields, build it up front with `builder.connectionObject` and pass the resulting ref where a connection field would take its Connection options. It works with `t.prismaConnection`, `t.relatedConnection`, and `t.connection`. Shared edges follow the same pattern with `builder.edgeObject`. ```typescript const PlayerConnection = builder.connectionObject({ // Either a prisma object ref… type: Player, // …or a connection helper's ref: // type: playerConnectionHelpers.ref, name: 'PlayerConnection', }); builder.prismaNode('Team', { id: { field: 'id' }, fields: (t) => ({ name: t.exposeString('name'), playersConnection: t.relatedConnection( 'players', { cursor: 'id' }, // Pass the shared connection ref in place of the Connection options. PlayerConnection, ), }), }); ``` ## Extending connection edges [#extending-connection-edges] To expose data from a join table on the *edge* rather than the node, select the extra join columns in the helper and define the edge fields in the third argument of `t.connection`. The parent shape for edge fields is inferred from the connection's `resolve`. ```typescript const gameConnectionHelpers = prismaConnectionHelpers(builder, 'PlayerStat', { cursor: 'id', select: (nodeSelection) => ({ game: nodeSelection({}), // Extra fields from the join table, for the edge. goals: true, }), resolveNode: (playerStat) => playerStat.game, }); builder.prismaObjectFields('Player', (t) => ({ gamesConnection: t.connection( { type: Game, select: (args, ctx, nestedSelection) => ({ stats: gameConnectionHelpers.getQuery(args, ctx, nestedSelection), select: { stats: nestedSelection({}, ['edges', 'node']), }, }), resolve: (player, args, ctx) => gameConnectionHelpers.resolve(player.stats, args, ctx), }, {}, // Options for the edge object. { fields: (edge) => ({ goals: edge.field({ type: 'Int', // Edge parent is the join row, so goals is available here. resolve: (stat) => stat.goals, }), }), }, ), })); ``` ## Total count on shared connection objects [#total-count-on-shared-connection-objects] Setting `totalCount: true` on a `prismaConnection` or `relatedConnection` normally adds the `totalCount` field for you. With a *shared* connection object it can't, so add the field yourself. The connection's parent carries a `totalCount` property that is either the number or a function returning it (possibly async): ```typescript const PlayerConnection = builder.connectionObject({ type: Player, name: 'PlayerConnection', fields: (t) => ({ totalCount: t.int({ resolve: (connection) => { const { totalCount } = connection as { totalCount?: number | (() => number | Promise); }; return typeof totalCount === 'function' ? totalCount() : totalCount; }, }), }), }); ``` To add `totalCount` to *every* connection, register the field globally with `builder.globalConnectionField` and declare the `Connection` shape on the builder so the parent is typed: ```typescript export const builder = new SchemaBuilder<{ PrismaTypes: PrismaTypes; Connection: { totalCount: number | (() => number | Promise); }; }>({ plugins: [PrismaPlugin, RelayPlugin], relayOptions: {}, prisma: { client: prisma, dmmf: getDatamodel(), }, }); builder.globalConnectionField('totalCount', (t) => t.int({ nullable: false, resolve: (parent) => typeof parent.totalCount === 'function' ? parent.totalCount() : parent.totalCount, }), ); ``` ## Parsing and formatting cursors [#parsing-and-formatting-cursors] `parsePrismaCursor` and `formatPrismaCursor` build and read cursors compatible with Prisma connections by hand. Parsing a cursor returns the value from the cursor column, often the `id`, or an array or object when a compound index backs the cursor. Formatting takes the column value(s) that make up the cursor and produces the opaque cursor string. ```typescript import { parsePrismaCursor, formatPrismaCursor } from '@pothos/plugin-prisma'; ``` # Prisma plugin URL: /docs/plugins/prisma Define GraphQL types from Prisma models and resolve relations with automatically optimized queries. The Prisma plugin builds GraphQL object types straight from your Prisma models and resolves their relations with queries it plans for you. You call `builder.prismaObject` with a model name, expose the columns you want, and add relation fields with `t.relation` — the plugin turns a nested GraphQL selection into as few Prisma queries as it can, which is where the classic N+1 problem usually creeps in. The plugin is not required to use Prisma with Pothos, but it removes a lot of manual wiring and query planning. If you would rather keep Prisma at arm's length, see [Using Prisma without a plugin](./without-a-plugin). ## What it does [#what-it-does] * Define GraphQL types from your Prisma models with full type-safety, without hand-writing object refs or importing generated client types. * Resolve relations automatically from the relationships already declared in your database. * Load exactly the data a query needs in as few round-trips as possible, folding nested relations into a single Prisma query where it can. * Keep GraphQL type and field names independent of your column names and types. * Integrate with the [Relay plugin](./relay) for nodes and connections that load efficiently. * Back multiple GraphQL types with the same database model through [variants](./variants). * Add relation [count fields](./relations#relation-counts) to objects and connections. ## An example [#an-example] Here is a slice of an Ultimate League schema, built against the [canonical Prisma schema](./setup#add-the-pothos-generator). It defines a `Team`, exposes columns, computes a field from a related table, loads relations, and adds a Relay connection: ```typescript // A GraphQL type backed by the Team model — no object ref, no client imports. builder.prismaObject('Team', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), // Load a related column on demand and compute a field from it. starPlayer: t.string({ select: { players: { orderBy: { number: 'asc' }, take: 1 }, }, resolve: (team) => team.players[0]?.name ?? 'TBD', }), // A list relation, with an argument that shapes the relation query. players: t.relation('players', { args: { byNumber: t.arg.boolean(), }, query: (args) => ({ orderBy: args.byNumber ? { number: 'asc' } : { name: 'asc' }, }), }), // A Relay connection using Prisma's cursor-based pagination. gamesConnection: t.relatedConnection('homeGames', { cursor: 'id', }), }), }); // A Relay node backed by the Game model. builder.prismaNode('Game', { id: { field: 'id' }, fields: (t) => ({ playedAt: t.string({ resolve: (game) => game.playedAt.toISOString() }), homeTeam: t.relation('homeTeam'), }), }); builder.queryType({ fields: (t) => ({ // A field that issues a single optimized Prisma query. myTeam: t.prismaField({ type: 'Team', resolve: async (query, _root, _args, ctx) => prisma.team.findUniqueOrThrow({ // Spreading `query` adds the include/select the plugin computed // for the nested selection, resolving as much as possible at once. ...query, where: { id: ctx.teamId }, }), }), }), }); ``` ## How the query plan works [#how-the-query-plan-works] Given the schema above, a nested query resolves in a **single** Prisma call (which Prisma turns into a handful of optimized SQL statements): ```graphql query { myTeam { name players { name stats { goals } } } } ``` The `myTeam` resolver receives a `query` with the `include`/`select` needed to load `players` and their `stats` in one go. Add a second, differently-argumented copy of the same relation, though, and one query is no longer enough: ```graphql query { myTeam { name players { name } byNumber: players(byNumber: true) { name } } } ``` This runs **two** Prisma queries: one for everything except `byNumber`, and a second for the aliased relation. Prisma can resolve a given relation only once per query, so a second copy of `players` with different arguments needs its own query. The plugin detects this and splits the work into the fewest queries possible. [Relations](./relations#fallback-queries) covers the edge cases that trigger a split. # Indirect relations URL: /docs/plugins/prisma/indirect-relations Pre-load data through wrapper types and join tables with the nestedSelection helper. `t.relation` pre-loads a direct relation, but some fields don't map to one edge. A field might wrap a Prisma object in a non-Prisma type, or reach a model through a join table two hops away. For those, the `select` function on a field gives you `nestedSelection`, a helper that reads the GraphQL selection set at a path you choose and returns the matching Prisma `select`, so you can still load everything in one query. This page assumes the [generated types and builder](./setup) are already wired up. ## Selecting through a wrapper type [#selecting-through-a-wrapper-type] By default `nestedSelection` returns selections for the current field's own type. Pass it a path and it looks *deeper*, into a field nested inside the returned type. That's what you need when a field returns a plain `objectRef` whose inner field is the actual `prismaObject`. Here a `Player` exposes `statCards`, a list of a plain wrapper type. Each `StatCard` carries a computed `summary` plus the full `PlayerStat` behind its `stat` field. `nestedSelection` reaches into `statCards.stat` to figure out what to load: ```typescript import { PlayerStat } from '@prisma/client'; const PlayerStatRef = builder.prismaObject('PlayerStat', { fields: (t) => ({ goals: t.exposeInt('goals'), assists: t.exposeInt('assists'), game: t.relation('game'), }), }); const StatCard = builder.objectRef('StatCard').implement({ fields: (t) => ({ stat: t.field({ type: PlayerStatRef, resolve: (stat) => stat, }), summary: t.string({ resolve: (stat) => `${stat.goals}G ${stat.assists}A`, }), }), }); builder.prismaObject('Player', { fields: (t) => ({ id: t.exposeID('id'), statCards: t.field({ select: (args, ctx, nestedSelection) => ({ stats: nestedSelection( // Default query for the stats relation — cap how many we load. { take: 2 }, // Look at selections under statCards.stat to decide what to select. ['stat'], // Optional: if the field returned a union or interface, name the // concrete type whose selections you want. 'PlayerStat', ), }), type: [StatCard], resolve: (player) => player.stats, }), }), }); ``` The third argument is only needed when the nested field returns a union or interface; pass the object type name whose selections you want. For a plain object type it can be omitted. ## Reaching through a join table [#reaching-through-a-join-table] Many-to-many relations modeled with an explicit join table are the common case for the two-hop reach. In the Ultimate League schema, `Player` and `Game` connect through `PlayerStat`: ```prisma model Game { id Int @id @default(autoincrement()) playedAt DateTime stats PlayerStat[] } model Player { id Int @id @default(autoincrement()) name String stats PlayerStat[] } model PlayerStat { id Int @id @default(autoincrement()) goals Int assists Int player Player @relation(fields: [playerId], references: [id]) playerId Int game Game @relation(fields: [gameId], references: [id]) gameId Int } ``` To expose the `Player`s who featured in a `Game` as a flat list, hiding the `PlayerStat` join entirely, nest `nestedSelection` inside the join relation's `select`. It reads what the query asks of `Player` and pre-loads exactly those columns and relations: ```typescript const Game = builder.prismaObject('Game', { fields: (t) => ({ id: t.exposeID('id'), players: t.field({ select: (args, ctx, nestedSelection) => ({ stats: { select: { // Inspects the fields queried on Player and selects them — // automatically pulling in a relation like `team` if requested. player: nestedSelection( // Default query for the player relation; could also be // something like `{ select: { id: true } }`. true, ), }, }, }), type: [Player], resolve: (game) => game.stats.map((stat) => stat.player), }), }), }); const Player = builder.prismaObject('Player', { select: { id: true, }, fields: (t) => ({ name: t.exposeString('name'), team: t.relation('team'), }), }); ``` The `resolve` maps the join rows back to their `Player`, so clients see `game.players` with no sign of `PlayerStat`, while the query still loads a requested `team` relation in the same round-trip. # Interfaces URL: /docs/plugins/prisma/interfaces Define GraphQL interfaces for a Prisma model and share them across variants. `builder.prismaInterface` works exactly like [`builder.prismaObject`](./objects), but produces a GraphQL interface instead of an object type. Like `prismaObject`, it can define either a model's primary type (with `name`) or a [variant](./variants) (with `variant` in place of `name`). Use it to give several variants of one Prisma model a shared set of fields: the interface holds what they have in common, and each variant implements it and adds its own. This page assumes the [generated types and builder](./setup) are already wired up. The examples add two columns to the base `Player` model, an `isCaptain` discriminator and an optional `bio`: ```prisma model Player { id Int @id @default(autoincrement()) name String number Int isCaptain Boolean @default(false) bio String? // ...team and stats relations as in the base schema } ``` ## An interface with two variants [#an-interface-with-two-variants] The interface defines the fields every player shares. A `resolveType` picks the concrete variant for a given row. Return the type name as a **string** rather than an object ref, which avoids circular-reference problems between the interface and the variants that implement it. ```typescript const Player = builder.prismaInterface('Player', { name: 'Player', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), resolveType: (player) => (player.isCaptain ? 'Captain' : 'SquadPlayer'), }); builder.prismaObject('Player', { variant: 'Captain', interfaces: [Player], fields: (t) => ({ isCaptain: t.exposeBoolean('isCaptain'), }), }); builder.prismaObject('Player', { variant: 'SquadPlayer', interfaces: [Player], fields: (t) => ({ bio: t.exposeString('bio', { nullable: true }), }), }); ``` Both `Captain` and `SquadPlayer` are variants of the same `Player` model, so they inherit its backing shape. Each adds the fields specific to it on top of the interface's `id` and `name`. Selections are **not** inherited. Under [`select` mode](./selections), add the columns you need to both the interface *and* every implementing object type. Otherwise the object falls back to the default selection of all scalar columns, which may not be what you want. An interface only spans one model. Trying to have an object for a *different* Prisma model implement it fails at build time: ```typescript // Error at build time: Team is a different model than Player. builder.prismaObject('Team', { interfaces: [Player], fields: (t) => ({ id: t.exposeID('id') }), }); ``` # Prisma objects URL: /docs/plugins/prisma/objects Define GraphQL object types from Prisma models with prismaObject and resolve to them with prismaField. `builder.prismaObject` defines a GraphQL object type backed by a Prisma model. You pass the model name and a set of options that mirror any other object type. The difference is that Pothos already knows the shape from your [generated `PrismaTypes`](./setup), so exposing columns and adding relations is fully typed without object refs or imports from the client. ```typescript builder.prismaObject('Team', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); builder.prismaObject('Player', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), number: t.exposeInt('number'), }), }); ``` These behave like any Pothos object type. `prismaObject` returns an object ref you can use anywhere a ref is expected. Unlike a plain `objectRef`, the type information comes from your schema, and relation fields on it can be query-planned. ## Naming the type [#naming-the-type] The GraphQL type takes the model's name by default. Pass `name` to call it something else, which is handy when one model backs [more than one GraphQL type](./variants): ```typescript builder.prismaObject('Team', { // The GraphQL type is `Roster`, still backed by the Team model. name: 'Roster', fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` ## Resolving to a Prisma type [#resolving-to-a-prisma-type] Use `t.prismaField` to add a field whose type is a Prisma model, most often on `Query` or `Mutation`: ```typescript builder.queryType({ fields: (t) => ({ myTeam: t.prismaField({ type: 'Team', resolve: async (query, _root, _args, ctx) => prisma.team.findUniqueOrThrow({ ...query, where: { id: ctx.teamId }, }), }), }), }); ``` `t.prismaField` works like `t.field` with two differences: 1. **`type`** is a Prisma model name: `'Team'`, or `['Team']` for a list field. 2. **`resolve`** gets an extra first argument, `query`. Spread it into your Prisma call. It carries the `include`/`select` the plugin computed for the nested part of the request, so relations and selected columns load in the same query. Which fields end up in it depends on what the client selected and how you defined the fields and types involved. You are not required to use `t.prismaField` (a `prismaObject` ref works with a plain `t.field` too), but only `t.prismaField` (and `t.prismaConnection`) gives you the `query` argument that makes the loading efficient. ## Extending a Prisma object [#extending-a-prisma-object] The usual `builder.objectField` and `builder.objectFields` work on Prisma objects, but they can't use [selections](./selections) or expose fields outside the default selection. To add a field that pulls in extra columns or relations, use `builder.prismaObjectField` or `builder.prismaObjectFields` instead: ```typescript builder.prismaObjectField('Team', 'playerCount', (t) => t.relationCount('players'), ); ``` # Prisma utils URL: /docs/plugins/prisma/prisma-utils Build Prisma-compatible filter, order-by, create, and update input types from your schema. Writing input types for filtering, ordering, and mutating Prisma models by hand is repetitive and easy to get out of sync with your schema. The prisma-utils plugin adds builder helpers that produce input types shaped to match Prisma's own `where`, `orderBy`, `create`, and `update` arguments, so a filter you define type-checks against the query you'll pass it to. It layers on top of the [Prisma plugin](./setup) but is otherwise independent; you can adopt it a few input types at a time. This package is highly experimental and not recommended for production use. The helpers are building blocks that may change with breaking releases as they mature. ## Setup [#setup] Enable the `prismaUtils` feature on the Pothos generator in your `schema.prisma`: ```prisma generator client { provider = "prisma-client" output = "../lib/prisma" } generator pothos { provider = "prisma-pothos-types" clientOutput = "./prisma" // relative path from the pothos output to the prisma client output = "../lib/pothos-prisma-types.ts" prismaUtils = true // enable the prisma-utils feature } ``` Then add the plugin alongside the Prisma plugin when you build. The utils lean on your scalar mappings, so register any custom scalars (like `DateTime`) the input types will reference: ```typescript import SchemaBuilder from '@pothos/core'; import { PrismaClient } from '@prisma/client'; import PrismaPlugin from '@pothos/plugin-prisma'; import PrismaUtils from '@pothos/plugin-prisma-utils'; import type PrismaTypes from '../lib/pothos-prisma-types'; import { getDatamodel } from '../lib/pothos-prisma-types'; export const prisma = new PrismaClient({}); export default new SchemaBuilder<{ Scalars: { DateTime: { Input: Date; Output: Date }; }; PrismaTypes: PrismaTypes; }>({ plugins: [PrismaPlugin, PrismaUtils], prisma: { client: prisma, dmmf: getDatamodel(), }, }); ``` ## What the plugin is for [#what-the-plugin-is-for] The goal is not to generate every input type automatically; there are too many design trade-offs in filtering and mutation inputs for one scheme to fit every schema. Instead the plugin gives you composable building blocks, so writing your own helpers or a [code generator](#generators) becomes far easier. Each helper below produces one Prisma-compatible input type you assemble into `where`, `orderBy`, `create`, and `update` arguments. ## Filters [#filters] ### Scalar and enum filters [#scalar-and-enum-filters] `builder.prismaFilter` builds a filter input for a scalar or enum, exposing the operators you list in `ops`: ```typescript const StringFilter = builder.prismaFilter('String', { ops: ['contains', 'equals', 'startsWith', 'not'], }); export const IntFilter = builder.prismaFilter('Int', { ops: ['equals', 'not'], }); builder.enumType(Position, { name: 'Position' }); const PositionFilter = builder.prismaFilter(Position, { ops: ['not', 'equals'], }); ``` ### Object (`where`) filters [#object-where-filters] `builder.prismaWhere` builds a filter matching a model's `where` clause. Its `fields` can be a static object or a function; each field takes a filter, a scalar type name (for equality-only), or a `t.field` for extra options. Relations are filtered by referencing another `where` filter: ```typescript const PlayerWhere = builder.prismaWhere('Player', { fields: { id: IntFilter, }, }); const GameWhere = builder.prismaWhere('Game', { fields: (t) => ({ // Use a filter for rich operators... id: IntFilter, // ...or a scalar type name for equality only. playedAt: 'DateTime', // Relations reference another where filter. homeTeam: TeamWhere, // t.field adds options like a description. homeTeamId: t.field({ type: IntFilter, description: 'filter by home team id' }), }), }); ``` ### Scalar list filters [#scalar-list-filters] `builder.prismaScalarListFilter` builds a filter for a scalar-array column: ```typescript export const StringListFilter = builder.prismaScalarListFilter('String', { name: 'StringListFilter', ops: ['has', 'hasSome', 'hasEvery', 'isEmpty', 'equals'], }); ``` ### Object list filters [#object-list-filters] `builder.prismaListFilter` wraps a `where` filter with list operators, for filtering a to-many relation: ```typescript const PlayerListFilter = builder.prismaListFilter(PlayerWhere, { ops: ['every', 'some', 'none'], }); ``` ## Order-by inputs [#order-by-inputs] `builder.prismaOrderBy` builds an `orderBy` input. Set a scalar field to `true` to make it sortable; reference another order-by input to sort by a relation: ```typescript const TeamOrderBy = builder.prismaOrderBy('Team', { fields: { name: true, }, }); export const PlayerOrderBy = builder.prismaOrderBy('Player', { fields: () => ({ id: true, name: true, number: true, team: TeamOrderBy, }), }); ``` ## Create inputs [#create-inputs] `builder.prismaCreate` builds an input for a create mutation. For types with circular references, add an explicit `InputObjectRef` annotation so the types resolve; simple types without cycles can omit it. The first type argument is the builder's `SchemaTypes`, which you recover once with a helper alias. ```typescript import { InputObjectRef } from '@pothos/core'; import { Prisma } from '@prisma/client'; // Recover the builder's SchemaTypes for the input-ref annotations below. type Types = typeof builder extends PothosSchemaTypes.SchemaBuilder ? T : never; export const PlayerCreate: InputObjectRef = builder.prismaCreate('Player', { name: 'PlayerCreate', fields: () => ({ // scalars id: 'Int', name: 'String', number: 'Int', // relation inputs are defined separately, below team: PlayerCreateTeam, // list relations are declared the same way — Pothos makes the input a list stats: PlayerCreateStats, }), }); ``` `builder.prismaCreateRelation` defines the nested input for one relation. `create` points at a `prismaCreate` input; `connect` points at a `prismaWhere`/`prismaWhereUnique` filter: ```typescript export const PlayerCreateTeam = builder.prismaCreateRelation('Player', 'team', { fields: () => ({ // built with builder.prismaCreate create: TeamCreateWithoutPlayers, // built with builder.prismaWhereUnique connect: TeamUniqueFilter, }), }); export const PlayerCreateStats = builder.prismaCreateRelation('Player', 'stats', { fields: () => ({ create: PlayerStatCreateWithoutPlayer, connect: PlayerStatUniqueFilter, }), }); ``` ## Update inputs [#update-inputs] `builder.prismaUpdate` mirrors `prismaCreate` for update mutations, with the same annotation guidance for circular references: ```typescript export const PlayerUpdate: InputObjectRef = builder.prismaUpdate('Player', { name: 'PlayerUpdate', fields: () => ({ id: 'Int', name: 'String', number: 'Int', team: PlayerUpdateTeam, stats: PlayerUpdateStats, }), }); ``` `builder.prismaUpdateRelation` exposes the full set of Prisma nested-write operations. Define only the ones a given relation needs: ```typescript export const PlayerUpdateTeam = builder.prismaUpdateRelation('Player', 'team', { fields: () => ({ create: TeamCreateWithoutPlayers, // builder.prismaCreate update: TeamUpdateWithoutPlayers, // builder.prismaUpdate connect: TeamUniqueFilter, // builder.prismaWhereUnique }), }); export const PlayerUpdateStats = builder.prismaUpdateRelation('Player', 'stats', { fields: () => ({ create: PlayerStatCreateWithoutPlayer, // builder.prismaCreate createMany: { // builder.prismaCreateMany skipDuplicates: 'Boolean', data: PlayerStatCreateManyWithoutPlayer, }, set: PlayerStatUniqueFilter, // builder.prismaWhereUnique disconnect: PlayerStatUniqueFilter, delete: PlayerStatUniqueFilter, connect: PlayerStatUniqueFilter, update: { where: PlayerStatUniqueFilter, // builder.prismaWhereUnique data: PlayerStatUpdateWithoutPlayer, // builder.prismaUpdate }, updateMany: { where: PlayerStatWithoutPlayerFilter, // builder.prismaWhere data: PlayerStatUpdateWithoutPlayer, // builder.prismaUpdate }, deleteMany: PlayerStatWithoutPlayerFilter, // builder.prismaWhere }), }); ``` ### Atomic number updates [#atomic-number-updates] `builder.prismaIntAtomicUpdate` builds an input for Prisma's atomic integer operations, so a mutation can `increment` or `decrement` a column instead of overwriting it: ```typescript const IntUpdate = builder.prismaIntAtomicUpdate(); // or with options const IntUpdateWithOps = builder.prismaIntAtomicUpdate({ name: 'IntUpdate', ops: ['increment', 'decrement'], }); export const PlayerStatUpdate = builder.prismaUpdate('PlayerStat', { name: 'PlayerStatUpdate', fields: () => ({ assists: 'Int', goals: IntUpdate, }), }); ``` ## Generators [#generators] Hand-writing every input type for a large schema is exactly the repetition these helpers exist to remove. Pothos does not ship an official generator, but two example generators show how to wire the building blocks into one. They're deliberately limited and not built for reuse, and they'll change with breaking updates, so copy and adapt them rather than importing them. * **Static generation** writes the input types to a TypeScript file you import into your schema. See the [example static generator](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/generator.ts), the [file it produces](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/schema/prisma-inputs.ts), and [how it's consumed](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/schema/index.ts). * **Dynamic generation** creates the input types at runtime through helpers imported into your app. See the [example dynamic generator](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/crud/generator.ts) and [how it's used](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/crud/schema/index.ts#L9-L20). # Relations URL: /docs/plugins/prisma/relations Add relation fields with t.relation, shape them with query and args, and add relation counts. `t.relation` adds a field for a relation declared in your Prisma schema. The plugin pre-loads it through the `query` of whichever [`t.prismaField`](./objects#resolving-to-a-prisma-type) started the request, so a chain of relations resolves without a query per level. ```typescript builder.queryType({ fields: (t) => ({ myTeam: t.prismaField({ type: 'Team', resolve: async (query, _root, _args, ctx) => prisma.team.findUniqueOrThrow({ ...query, where: { id: ctx.teamId }, }), }), }), }); builder.prismaObject('Team', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), players: t.relation('players'), }), }); builder.prismaObject('Player', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.relation('team'), }), }); ``` Each `t.relation` contributes an `include` (`{ include: { players: true } }`) to the `query` argument of the `prismaField` that resolves the parent. When a relation field's parent is itself a relation, the includes nest, and the whole chain flows back to the `prismaField` at the root. This query: ```graphql query { myTeam { players { team { id } } } } ``` hands the `myTeam` resolver a `query` shaped like: ```typescript { include: { players: { include: { team: true, }, }, }, } ``` That single include resolves the whole tree. A few cases make one query impossible; when they arise, Pothos loads the missing pieces itself. ## Fallback queries [#fallback-queries] When some data can't be pre-loaded, Pothos issues a `findUnique` for the parent of the un-loaded fields and selects just the missing relations. These queries are efficient: Pothos batches the requirements of several fields into one, and Prisma batches the resulting per-parent queries (the N+1 shape) down to a single SQL statement. A fallback query kicks in when: * The parent object wasn't loaded through a `t.prismaField` or `t.relation`. * The root `t.prismaField` didn't spread its `query` argument into the Prisma call. * The query selects the same relation more than once with different filters, sorting, or limits. * The query aliases the same relation field with different arguments that produce different relation query options. * A relation field's `query` is incompatible with the parent object's default includes. These are uncommon in normal use, and the plugin handles them automatically when they occur. ## Filters, sorting, and arguments [#filters-sorting-and-arguments] `t.prismaField` takes arguments like any field and you pass them into your own Prisma call. `t.relation` is different. You aren't writing the Prisma query, the planner is, so you shape the relation with a `query` option. It's either a query object or a function of the field's arguments and the request context: ```typescript builder.prismaObject('Team', { fields: (t) => ({ id: t.exposeID('id'), players: t.relation('players', { // Arguments are declared like any other field. args: { byNumber: t.arg.boolean(), }, // Build the relation query from those arguments. query: (args, _context) => ({ orderBy: args.byNumber ? { number: 'asc' } : { name: 'asc' }, }), }), }), }); ``` The object `query` returns is merged into the `include` for this relation, so it accepts the usual relation query keys: `where`, `skip`, `take`, `orderBy`. The function receives the field arguments and the request context. It does **not** receive the parent object: the relation is pre-loaded before the parent exists, which is exactly what avoids the N+1 query. ## Relation counts [#relation-counts] Prisma can return [relation counts](https://www.prisma.io/docs/orm/prisma-client/queries/aggregation-grouping-summarizing#count-relations) alongside other includes. `t.relationCount` exposes one as an `Int` field: ```typescript builder.prismaObject('Team', { fields: (t) => ({ id: t.exposeID('id'), playerCount: t.relationCount('players', { where: { number: { gte: 1 }, }, }), }), }); ``` Filtering a relation count needs Prisma 4.2.0 or newer, under the `filteredRelationCount` preview feature. Before 4.2.0, `t.relationCount` still works but can only return a total, so drop the `where` option. # Relay nodes URL: /docs/plugins/prisma/relay Turn a Prisma model into a Relay node with global IDs and efficient node(id) lookups. The Prisma plugin wires into the [Relay plugin](../relay) so a Prisma model becomes a Relay node with a global ID and an efficient `node(id: ID!)` lookup. `builder.prismaNode` takes the place of `builder.prismaObject`: it defines the same object type and, on top of it, implements the `Node` interface and the `id` field for you. These examples assume the builder is set up with both `PrismaPlugin` and `RelayPlugin` and a `prisma` client in scope (see [Setup](./setup) for the generator, `PrismaTypes`, and builder wiring). ## Defining a node [#defining-a-node] `prismaNode` takes the same options as [`prismaObject`](./objects) plus one required `id` option that mirrors the `id` option of the Relay plugin's `node` method. The simplest form points `id.field` at a unique column or index, and Pothos derives both the global ID and the lookup from it. ```typescript builder.prismaNode('Player', { // Which database column backs the node's global id. id: { field: 'id' }, // fields work exactly like builder.prismaObject. fields: (t) => ({ name: t.exposeString('name'), number: t.exposeInt('number'), team: t.relation('team'), }), }); ``` With `id.field` set, the `node(id:)` query loads the record with a `prisma.player.findUnique` keyed on that column, with no resolver to write. ## Customizing the id [#customizing-the-id] To format the global ID yourself, replace `id.field` with an `id.resolve` function that returns a string from a node instance. Pair it with `findUnique`, whose return value is passed as the `where` of a `prisma.player.findUnique` to load the node back from that formatted ID. This is for cases where the raw column value isn't the shape you want to expose. ```typescript builder.prismaNode('Player', { id: { resolve: (player) => String(player.id) }, // The return value becomes the `where` of a prisma.player.findUnique. findUnique: (id) => ({ id: Number.parseInt(id, 10) }), fields: (t) => ({ name: t.exposeString('name'), number: t.exposeInt('number'), team: t.relation('team'), }), }); ``` ## Missing records [#missing-records] When `node(id:)` resolves to a global ID that no longer maps to a row, the default behavior is to throw. Some clients would rather receive `null` for a deleted or never-existent node than surface an error. Set `nullable: true` to load with `findUnique` instead of `findUniqueOrThrow` and return `null` on a miss. ```typescript builder.prismaNode('Player', { id: { resolve: (player) => String(player.id) }, nullable: true, fields: (t) => ({ name: t.exposeString('name'), number: t.exposeInt('number'), team: t.relation('team'), }), }); ``` Every other `prismaObject` option carries over unchanged, including `name` and `variant` for exposing [multiple GraphQL types from one model](./variants). # Selections URL: /docs/plugins/prisma/selections Control which columns and relations Prisma loads with include, select, and field-level selections. By default the plugin loads a model's full row and pre-loads only the relations a query touches. `include` and `select` on a `prismaObject` let you tune that: pre-load a relation every time, or narrow a wide table down to the columns you actually expose. ## Always include a relation [#always-include-a-relation] Add `include` to a `prismaObject` to pre-load a relation whenever the type is loaded. This lets a field read from a related table without declaring the relation in GraphQL. Deeply nested relations can be included the same way: ```typescript builder.prismaObject('Player', { // stats are always loaded with a Player. include: { stats: true, }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), totalGoals: t.int({ // player is now typed with `stats`, so the field can read from it. resolve: (player) => player.stats.reduce((sum, s) => sum + s.goals, 0), }), }), }); ``` ## Select instead of include [#select-instead-of-include] By default the plugin uses `include`, which loads every column of a table. That's usually fine, but for tables with many columns or a few heavy payloads you may want to load only what you expose. Add a `select` to the `prismaObject` to switch that type into select mode: ```typescript builder.prismaObject('Player', { select: { id: true, }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); ``` In select mode, `t.expose*` and `t.relation` automatically add their columns and relations to the selection **when the field is queried**, so only the requested columns leave the database. Other fields can add their own selections with a `select` option: ```typescript builder.prismaObject('Player', { select: { id: true, }, fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), totalGoals: t.int({ // stats are selected only when totalGoals is queried. select: { stats: { select: { goals: true }, }, }, resolve: (player) => player.stats.reduce((sum, s) => sum + s.goals, 0), }), }), }); ``` ## Selections from arguments or context [#selections-from-arguments-or-context] `select` can be a function of the field's arguments and context, so a selection can respond to input. This field takes a date and selects only the stats recorded since then: ```typescript builder.prismaObject('Player', { fields: (t) => ({ name: t.exposeString('name'), recentGoals: t.int({ args: { since: t.arg({ type: 'Date', required: true }), }, select: (args) => ({ stats: { where: { game: { playedAt: { gt: args.since } }, }, }, }), resolve: (player) => player.stats.reduce((sum, s) => sum + s.goals, 0), }), }), }); ``` ## Optimized queries without `t.prismaField` [#optimized-queries-without-tprismafield] Sometimes you need the plugin's computed query for a field that can't be a `t.prismaField`, because it combines with another plugin, or the field doesn't return a Prisma object directly. `queryFromInfo` builds that query from the resolver's `info`. A common case is a mutation that wraps a Prisma object in a result type: ```typescript import type { Player } from '@prisma/client'; const PlayerType = builder.prismaObject('Player', { fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), }), }); const SignPlayerResult = builder .objectRef<{ success: boolean; player?: Player }>('SignPlayerResult') .implement({ fields: (t) => ({ success: t.exposeBoolean('success'), player: t.field({ type: PlayerType, nullable: true, resolve: (result) => result.player, }), }), }); builder.mutationField('signPlayer', (t) => t.field({ type: SignPlayerResult, args: { name: t.arg.string({ required: true }), teamId: t.arg.id({ required: true }), }, resolve: async (_root, args, context, info) => { if (!args.name) { return { success: false }; } const player = await prisma.player.create({ ...queryFromInfo({ context, info, // Nested path where the selections for the Player type live. path: ['player'], // Optional initial selection, in case the field at `path` selects nothing. select: { stats: true }, }), data: { name: args.name, number: 0, teamId: Number(args.teamId), }, }); return { success: true, player }; }, }), ); ``` `path` points `queryFromInfo` at where in the selection the Prisma object appears (here the `player` field of the result). `select` (or `include`) seeds an initial selection, useful when the field at `path` may not be selected at all, leaving the selection set empty. # Setup URL: /docs/plugins/prisma/setup Add the Pothos generator to your Prisma schema and wire PrismaTypes into the builder. The Prisma plugin reads a small set of generated types to understand your schema. You add a generator to `schema.prisma`, run `prisma generate`, then hand the generated `PrismaTypes` and a datamodel to the builder. Everything else on these pages assumes this wiring is in place. npm pnpm yarn bun ```bash npm install --save @pothos/plugin-prisma ``` ```bash pnpm add @pothos/plugin-prisma ``` ```bash yarn add @pothos/plugin-prisma ``` ```bash bun add @pothos/plugin-prisma ``` Earlier versions inferred every type from the Prisma client itself. That made editors slow on large schemas and left some advanced cases impossible to type. The generator exists so the plugin can read a compact, purpose-built set of types instead. ## Add the Pothos generator [#add-the-pothos-generator] Add the `pothos` generator alongside your Prisma client generator. This example uses the Ultimate League schema that runs through every Prisma page: ```prisma generator pothos { provider = "prisma-pothos-types" } model Team { id Int @id @default(autoincrement()) name String @unique players Player[] homeGames Game[] @relation("HomeTeam") awayGames Game[] @relation("AwayTeam") } model Player { id Int @id @default(autoincrement()) name String number Int team Team @relation(fields: [teamId], references: [id]) teamId Int stats PlayerStat[] } model Game { id Int @id @default(autoincrement()) playedAt DateTime homeTeam Team @relation("HomeTeam", fields: [homeTeamId], references: [id]) homeTeamId Int awayTeam Team @relation("AwayTeam", fields: [awayTeamId], references: [id]) awayTeamId Int stats PlayerStat[] } model PlayerStat { id Int @id @default(autoincrement()) goals Int assists Int player Player @relation(fields: [playerId], references: [id]) playerId Int game Game @relation(fields: [gameId], references: [id]) gameId Int } ``` Pothos types regenerate whenever you regenerate the client: ```sh npx prisma generate ``` Two generator options control where the types land and what they import from: * **`output`**: where to write the generated types file. Defaults next to the Prisma client. * **`clientOutput`**: the import path the generated file uses to reach `PrismaClient`. It defaults to the absolute path of wherever the client is generated. If you check the generated file into source control, set this to a relative path so the import survives on other machines. ```prisma generator client { provider = "prisma-client" output = "../lib/prisma" } generator pothos { provider = "prisma-pothos-types" clientOutput = "./prisma" // relative path from the pothos output to the client output = "../lib/pothos-prisma-types.ts" } ``` If auto-completion for Prisma types and relations is not working, check that the generated types file imports the client from the right location; a stale `clientOutput` is the usual cause. ## Wire up the builder [#wire-up-the-builder] Register the plugin, give the builder your `PrismaTypes`, and pass a `prisma` config with the client and datamodel: ```typescript import SchemaBuilder from '@pothos/core'; import { PrismaClient } from '@prisma/client'; import PrismaPlugin from '@pothos/plugin-prisma'; import type PrismaTypes from '../lib/pothos-prisma-types'; import { getDatamodel } from '../lib/pothos-prisma-types'; const prisma = new PrismaClient({}); const builder = new SchemaBuilder<{ // Gives the builder full type information about your Prisma schema. PrismaTypes: PrismaTypes; }>({ plugins: [PrismaPlugin], prisma: { client: prisma, // Describes tables, relations, and indexes so Pothos can plan optimal queries at runtime. dmmf: getDatamodel(), }, }); ``` The rest of the Prisma pages assume this `builder` and this module-level `prisma` client already exist. The datamodel used to ride along on the Prisma client, but most runtimes now strip it to shrink bundle size, so `dmmf: getDatamodel()` passes it explicitly. `getDatamodel` is exported from the generated types file. ### Prisma config options [#prisma-config-options] The `prisma` object accepts: | Option | Purpose | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client` | The `PrismaClient` instance, or a function `(ctx) => client` to pick a client per request. | | `dmmf` | The datamodel from `getDatamodel()`. Required so the plugin can plan queries. | | `exposeDescriptions` | Use `///` comments from the Prisma schema as descriptions for models, relations, and exposed fields. Defaults to `false`. Pass `{ models, fields }` to enable them selectively; set a field's `description` to `false` to opt one out. | | `filterConnectionTotalCount` | Apply a related connection's `where` clause to its `totalCount`. Defaults to `true`. | | `onUnusedQuery` | Warn or throw when a resolver forgets to use its `query` argument. See [below](#detecting-unused-query-arguments). | | `maxConnectionSize` / `defaultConnectionSize` | Bounds for [Relay connections](./connections). | | `skipDeferredFragments` | Skip `@defer` fragments when planning the Prisma query, so deferred fields aren't fetched eagerly. Defaults to `true`. | ### A client per request [#a-client-per-request] Pass a function for `client` to choose a client based on context, useful for periodically recycled clients or read-only replicas for some users: ```typescript const prisma = new PrismaClient({}); const readOnlyPrisma = new PrismaClient({ datasources: { db: { url: process.env.READ_ONLY_REPLICA_URL }, }, }); const builder = new SchemaBuilder<{ Context: { user: { isAdmin: boolean } }; PrismaTypes: PrismaTypes; }>({ plugins: [PrismaPlugin], prisma: { client: (ctx) => (ctx.user.isAdmin ? prisma : readOnlyPrisma), dmmf: getDatamodel(), }, }); ``` Keep the Prisma client out of your `Context` type. The client's types are large, and threading them through `Context` slows type-checking and makes editors laggy (see [this TypeScript issue](https://github.com/microsoft/TypeScript/issues/45405)). Reference a module-level `prisma` singleton instead, as above. ## Detecting unused query arguments [#detecting-unused-query-arguments] `t.prismaField` and `t.prismaConnection` hand your resolver a `query` argument to spread into the Prisma call. Forgetting to spread it produces inefficient queries, or missing data. Set `onUnusedQuery` to catch the mistake: * `'warn'` logs a warning when the resolver returns without using `query`. * `'error'` throws instead. * A function receives the `info` object so you can log or throw your own error. ```typescript prisma: { client: prisma, dmmf: getDatamodel(), onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn', } ``` The check is deliberately naive: it wraps the `query` object's properties in getters that flip a flag when read. If nothing on the object is accessed before the resolver returns, the `onUnusedQuery` condition fires. Enable it in development to surface these issues quickly. # Type variants URL: /docs/plugins/prisma/variants Expose one Prisma model as several GraphQL types with the variant option. One Prisma model often needs to appear in the schema as more than one GraphQL type: a public view and a private one, a full record and a lightweight card. Pothos calls these **variants**. Every model has one primary type (defined with a `name`, as on the [objects page](./objects)); each additional variant is defined with a `variant` option in its place. This page assumes the [generated types and builder](./setup) are already wired up. The examples add an optional `email` column to the base `Player` model so a private variant has something to guard: ```prisma model Player { id Int @id @default(autoincrement()) name String email String? // ...number, team, and stats as in the base schema } ``` ## Defining a variant [#defining-a-variant] Give `variant` a type name instead of `name`. Here `PlayerPrivateInfo` is a second GraphQL type over the same `Player` model, exposing a field the public type shouldn't: ```typescript const PlayerPrivateInfo = builder.prismaObject('Player', { variant: 'PlayerPrivateInfo', fields: (t) => ({ id: t.exposeID('id'), email: t.exposeString('email', { nullable: true }), }), }); ``` ## Linking variants together [#linking-variants-together] `t.variant` adds a field that returns another variant of the same row. Reference the **primary** variant by its model name as a string; reference any **other** variant by the object ref it returned. An `isNull` callback can hide the variant when it shouldn't be visible. Here the private info resolves to `null` unless the parent player is the current viewer: ```typescript const PlayerPrivateInfo = builder.prismaObject('Player', { variant: 'PlayerPrivateInfo', fields: (t) => ({ email: t.exposeString('email', { nullable: true }), // The model name references the primary variant. player: t.variant('Player'), }), }); const Player = builder.prismaNode('Player', { id: { resolve: (player) => String(player.id) }, fields: (t) => ({ name: t.exposeString('name'), // Reference another variant by its ref, not the model name. privateInfo: t.variant(PlayerPrivateInfo, { // Hide private info unless the parent player is the current viewer. isNull: (player, args, ctx) => player.id !== ctx.currentPlayerId, }), }), }); ``` `builder.prismaNode` needs the [relay plugin](./relay) and takes an `id` option: either an `id: { resolve }` that computes the node id, or `id: { field: 'id' }` to point at a database column. A variant that doesn't need to be a Relay node can use `builder.prismaObject` instead. ## Variants on relations [#variants-on-relations] A relation field can return a variant rather than the related model's primary type. Pass the variant ref as the relation's `type`, and use `query` to scope which rows it loads. Here a team's `schedule` returns games through a `CompletedGame` variant, filtered to games already played: ```typescript const CompletedGame = builder.prismaNode('Game', { variant: 'CompletedGame', // Which database column backs the node id. id: { field: 'id' }, fields: (t) => ({ playedAt: t.field({ type: 'DateTime', resolve: (game) => game.playedAt }), homeTeam: t.relation('homeTeam'), }), }); const Team = builder.prismaObject('Team', { variant: 'TeamSchedule', fields: (t) => ({ id: t.exposeID('id'), schedule: t.relation('homeGames', { // Use the CompletedGame variant for this relation instead of the default Game. type: CompletedGame, query: { where: { playedAt: { lt: new Date() } } }, }), }), }); ``` ## Breaking circular references [#breaking-circular-references] Two prisma object refs that reference each other in their `fields` functions can trip TypeScript into a circular-type error. Split one side out with `builder.prismaObjectField`, which attaches a single field after both refs exist. It takes the ref, the field name, and a field function: ```typescript const PlayerPrivateInfo = builder.prismaObject('Player', { variant: 'PlayerPrivateInfo', fields: (t) => ({ email: t.exposeString('email', { nullable: true }), }), }); const Player = builder.prismaNode('Player', { id: { resolve: (player) => String(player.id) }, fields: (t) => ({ name: t.exposeString('name'), }), }); // Attach the back-reference after both refs exist, breaking the cycle. builder.prismaObjectField(PlayerPrivateInfo, 'player', (t) => t.variant(Player)); ``` The same workaround applies to relations that use variants: move the offending relation field into a `prismaObjectField` call. # Prisma without a plugin URL: /docs/plugins/prisma/without-a-plugin Back GraphQL objects with Prisma models using plain objectRef, and tame the resulting N+1 queries. You don't need the [Prisma plugin](./setup) to put Prisma models behind a GraphQL schema. `builder.objectRef` takes any TypeScript shape as its backing model, and a Prisma row is just a shape, so you can point a ref at `Team` or `Player` from `@prisma/client` and resolve relations with ordinary client calls. You give up the plugin's automatic query-planning, but the code stays plain Pothos. ```typescript import { Player, PrismaClient, Team } from '@prisma/client'; const db = new PrismaClient(); const TeamObject = builder.objectRef('Team'); const PlayerObject = builder.objectRef('Player'); TeamObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), players: t.field({ type: [PlayerObject], resolve: (team) => db.player.findMany({ where: { teamId: team.id } }), }), }), }); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, resolve: (player) => db.team.findUniqueOrThrow({ where: { id: player.teamId } }), }), }), }); builder.queryType({ fields: (t) => ({ myTeam: t.field({ type: TeamObject, resolve: (_root, _args, ctx) => db.team.findUniqueOrThrow({ where: { id: ctx.teamId } }), }), }), }); ``` This defines `Team` and `Player` objects with a relation each, plus a `myTeam` query for the viewer's team. Three details make it work: * **Split the ref from `implement`.** Declaring `TeamObject`/`PlayerObject` up front and calling `implement` afterwards, rather than `builder.objectRef(...).implement(...)` in one expression, keeps TypeScript from choking on the circular reference between teams and players. * **`findUniqueOrThrow` for non-null fields.** `team` and `myTeam` are non-nullable, so they must never resolve to `null`. `findUnique` returns `null` when nothing matches; `findUniqueOrThrow` throws instead. Use `findUnique` only when the field is marked `nullable`. * **Ref names vs. type names.** The refs are `TeamObject`/`PlayerObject` because `Team` and `Player` are already taken by the imports from `@prisma/client`. Alias the imports instead (`import { Team as TeamModel }`) if you'd rather name the refs after the GraphQL types. ## Cutting down N+1 queries [#cutting-down-n1-queries] The schema above issues one query per relation edge. Fetch a team, then its players, then each player's team, and the round-trips multiply. Prisma batches some of this for you, but you can also shape the backing model to avoid the round-trip entirely. If you almost always load a player's team alongside the player, fold the team into the backing shape and have the parent resolver `include` it: ```typescript const TeamObject = builder.objectRef('Team'); // Widen the backing model so a Player always carries its Team. const PlayerObject = builder.objectRef('Player'); TeamObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), players: t.field({ type: [PlayerObject], resolve: (team) => db.player.findMany({ // Include the team so the child resolver has it already. include: { team: true }, where: { teamId: team.id }, }), }), }), }); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, // No query — the team came along with the player. resolve: (player) => player.team, }), }), }); ``` Requiring `team` on every `Player` is a strong claim: every resolver that produces a player now owes you the include. When only some paths can supply it, make the field optional and fall back to a query: ```typescript const PlayerObject = builder.objectRef('Player'); PlayerObject.implement({ fields: (t) => ({ id: t.exposeID('id'), name: t.exposeString('name'), team: t.field({ type: TeamObject, resolve: (player) => player.team ?? db.team.findUniqueOrThrow({ where: { id: player.teamId } }), }), }), }); ``` Now a parent resolver *may* pre-load the team, and the field still resolves correctly when it doesn't. A [dataloader](../dataloader) is the other lever for N+1: batch the per-player team lookups into one query. Or let the [Prisma plugin](./setup) plan these selections for you: `t.relation` and `t.prismaField` read the GraphQL selection set and build a single nested query.