Gathering detailed insights and metrics for @pothos/plugin-relay
Gathering detailed insights and metrics for @pothos/plugin-relay
Gathering detailed insights and metrics for @pothos/plugin-relay
Gathering detailed insights and metrics for @pothos/plugin-relay
Pothos GraphQL is library for creating GraphQL schemas in typescript using a strongly typed code first approach
npm install @pothos/plugin-relay
Typescript
Module System
Node Version
NPM Version
@pothos/plugin-relay@4.6.1
Updated on Jul 11, 2025
@pothos/core@4.7.2
Updated on Jul 11, 2025
@pothos/core@4.7.1
Updated on Jul 10, 2025
@pothos/plugin-relay@4.6.0
Updated on Jul 10, 2025
@pothos/plugin-prisma@4.10.0
Updated on Jul 10, 2025
@pothos/plugin-directives@4.2.4
Updated on Jul 10, 2025
TypeScript (83.15%)
MDX (16.2%)
JavaScript (0.54%)
CSS (0.09%)
Shell (0.02%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
ISC License
2,487 Stars
2,907 Commits
172 Forks
13 Watchers
36 Branches
120 Contributors
Updated on Jul 11, 2025
Latest Version
4.6.1
Package Id
@pothos/plugin-relay@4.6.1
Unpacked Size
472.93 kB
Size
73.53 kB
File Count
122
NPM Version
10.9.2
Node Version
22.17.0
Published on
Jul 11, 2025
Cumulative downloads
Total Downloads
2
The Relay plugin adds a number of builder methods and helper functions to simplify building a relay compatible schema.
1npm install --save @pothos/plugin-relay
1import RelayPlugin from '@pothos/plugin-relay'; 2const builder = new SchemaBuilder({ 3 plugins: [RelayPlugin], 4 relay: {}, 5});
The relay
options object passed to builder can contain the following properties:
idFieldName
: The name of the field that contains the global id for the node. Defaults to id
.idFieldOptions
: Options to pass to the id field.clientMutationId
: omit
(default) | required
| optional
. Determines if clientMutationId
fields are created on relayMutationFields
, and if they are required.cursorType
: String
| ID
. Determines type used for cursor fields. Defaults to String
nodeQueryOptions
: Options for the node
field on the query object, set to false to omit the
fieldnodesQueryOptions
: Options for the nodes
field on the query object, set to false to omit the
fieldnodeTypeOptions
: Options for the Node
interface typepageInfoTypeOptions
: Options for the TypeInfo
object typeclientMutationIdFieldOptions
: Options for the clientMutationId
field on connection objectsclientMutationIdInputOptions
: Options for the clientMutationId
input field on connections
fieldsmutationInputArgOptions
: Options for the Input object created for each connection fieldcursorFieldOptions
: Options for the cursor
field on an edge object.nodeFieldOptions
: Options for the node
field on an edge object.edgesFieldOptions
: Options for the edges
field on a connection object.pageInfoFieldOptions
: Options for the pageInfo
field on a connection object.hasNextPageFieldOptions
: Options for the hasNextPage
field on the PageInfo
object.hasPreviousPageFieldOptions
: Options for the hasPreviousPage
field on the PageInfo
object.startCursorFieldOptions
: Options for the startCursor
field on the PageInfo
object.endCursorFieldOptions
: Options for the endCursor
field on the PageInfo
object.beforeArgOptions
: Options for the before
arg on a connection field.afterArgOptions
: Options for the after
arg on a connection field.firstArgOptions
: Options for the first
arg on a connection field.lastArgOptions
: Options for the last
arg on a connection field.defaultConnectionTypeOptions
: Default options for the Connection
Object types.defaultEdgeTypeOptions
: Default options for the Edge
Object types.defaultPayloadTypeOptions
: Default options for the Payload
Object types.defaultMutationInputTypeOptions
: default options for the mutation Input
types.nodesOnConnection
: If true, the nodes
field will be added to the Connection
object types.defaultConnectionFieldOptions
: Default options for connection fields defined with t.connectionbrandLoadedObjects
: Defaults to true
. This will add a hidden symbol to objects returned from
the load
methods of Nodes that allows the default resolveType
implementation to identify the
type of the node. When this is enabled, you will not need to implement an isTypeOf
check for
most common patterns.To create objects that extend the Node
interface, you can use the new builder.node
method.
1// Using object refs 2const User = builder.objectRef<UserType>('User'); 3// Or using a class 4class User { 5 id: string; 6 name: string; 7} 8 9builder.node(User, { 10 // define an id field 11 id: { 12 resolve: (user) => user.id, 13 // other options for id field can be added here 14 }, 15 16 // Define only one of the following methods for loading nodes by id 17 loadOne: (id) => loadUserByID(id), 18 loadMany: (ids) => loadUsers(ids), 19 loadWithoutCache: (id) => loadUserByID(id), 20 loadManyWithoutCache: (ids) => loadUsers(ids), 21 22 // if using a class instaed of a ref, you will need to provide a name 23 name: 'User', 24 fields: (t) => ({ 25 name: t.exposeString('name'), 26 }), 27});
builder.node
will create an object type that implements the Node
interface. It will also create
the Node
interface the first time it is used. The resolve
function for id
should return a
number or string, which will be converted to a globalID. The relay plugin adds to new query fields
node
and nodes
which can be used to directly fetch nodes using global IDs by calling the
provided loadOne
or loadMany
method. Each node will only be loaded once by id, and cached if the
same node is loaded multiple times inn the same request. You can provide loadWithoutCache
or
loadManyWithoutCache
instead if caching is not desired, or you are already using a caching
datasource like a dataloader.
Nodes may also implement an isTypeOf
method which can be used to resolve the correct type for
lists of generic nodes. When using a class as the type parameter, the isTypeOf
method defaults to
using an instanceof
check, and falls back to checking the constructor property on the prototype.
The means that for many cases if you are using classes in your type parameters, and all your values
are instances of those classes, you won't need to implement an isTypeOf
method, but it is usually
better to explicitly define that behavior.
By default (unless brandLoadedObjects
is set to false
) any nodes loaded through one of the
load*
methods will be branded so that the default resolveType
method can identify the GraphQL
type for the loaded object. This means isTypeOf
is only required for union
and interface
fields that return node objects that are manually loaded, where the union or interface does not have
a custom resolveType
method that knows how to resolve the node type.
By default all node ids are parsed as string. This behavior can be customized by providing a custom parse function for your node's ID field:
1const User = builder.objectRef<UserType>('User') 2builder.node(User, { 3 // define an id field 4 id: { 5 resolve: (user) => user.id, 6 parse: (id) => Number.parseInt(id, 10), 7 }, 8 // the ID is now a number 9 loadOne: (id) => loadUserByID(id), 10 ... 11});
To make it easier to create globally unique ids the relay plugin adds new methods for creating globalID fields.
1import { encodeGlobalID } from '@pothos/plugin-relay'; 2 3builder.queryFields((t) => ({ 4 singleID: t.globalID({ 5 resolve: (parent, args, context) => { 6 return { id: 123, type: 'SomeType' }; 7 }, 8 }), 9 listOfIDs: t.globalIDList({ 10 resolve: (parent, args, context) => { 11 return [{ id: 123, type: 'SomeType' }]; 12 }, 13 }), 14}));
The returned IDs can either be a string (which is expected to already be a globalID), or an object
with the an id
and a type
, The type can be either the name of a name as a string, or any object
that can be used in a type parameter.
There are also new methods for adding globalIDs in arguments or fields of input types:
1builder.queryType({ 2 fields: (t) => ({ 3 fieldThatAcceptsGlobalID: t.boolean({ 4 args: { 5 id: t.arg.globalID({ 6 required: true, 7 }), 8 idList: t.arg.globalIDList(), 9 }, 10 resolve(parent, args) { 11 console.log(`Get request for type ${args.id.typename} with id ${args.id.id}`); 12 return true; 13 }, 14 }), 15 }), 16});
globalIDs used in arguments expect the client to send a globalID string, but will automatically be
converted to an object with 2 properties (id
and typename
) before they are passed to your
resolver in the arguments object.
globalID
input's can be configured to validate the type of the globalID. This is useful if you
only want to accept IDs for specific node types.
1builder.queryType({ 2 fields: (t) => ({ 3 fieldThatAcceptsGlobalID: t.boolean({ 4 args: { 5 id: t.arg.globalID({ 6 for: SomeType, 7 // or allow multiple types 8 for: [TypeOne, TypeTwo], 9 required: true, 10 }), 11 }, 12 }), 13 }), 14});
The t.connection
field builder method can be used to define connections. This method will
automatically create the Connection
and Edge
objects used by the connection, and add before
,
after
, first
, and last
arguments. The first time this method is used, it will also create the
PageInfo
type.
1builder.queryFields((t) => ({ 2 numbers: t.connection( 3 { 4 type: NumberThing, 5 resolve: (parent, { first, last, before, after }) => { 6 return { 7 pageInfo: { 8 hasNextPage: false, 9 hasPreviousPage: false, 10 startCursor: 'abc', 11 endCursor: 'def', 12 }, 13 edges: [ 14 { 15 cursor: 'abc', 16 node: new NumberThing(123), 17 }, 18 { 19 cursor: 'def', 20 node: new NumberThing(123), 21 }, 22 ], 23 }; 24 }, 25 }, 26 { 27 name: 'NameOfConnectionType', // optional, will use ParentObject + capitalize(FieldName) + "Connection" as the default 28 fields: (tc) => ({ 29 // define extra fields on Connection 30 // We need to use a new variable for the connection field builder (eg tc) to get the correct types 31 }), 32 edgesField: {}, // optional, allows customizing the edges field on the Connection Object 33 // Other options for connection object can be added here 34 }, 35 { 36 // Same as above, but for the Edge Object 37 name: 'NameOfEdgeType', // optional, will use Connection name + "Edge" as the default 38 fields: (te) => ({ 39 // define extra fields on Edge 40 // We need to use a new variable for the connection field builder (eg te) to get the correct types 41 }), 42 nodeField: {}, // optional, allows customizing the node field on the Edge Object 43 }, 44 ), 45}));
Manually implementing connections can be cumbersome, so there are a couple of helper methods that can make resolving connections a little easier.
For limit/offset based apis:
1import { resolveOffsetConnection } from '@pothos/plugin-relay'; 2 3builder.queryFields((t) => ({ 4 things: t.connection({ 5 type: SomeThing, 6 resolve: (parent, args) => { 7 return resolveOffsetConnection({ args }, ({ limit, offset }) => { 8 return getThings(offset, limit); 9 }); 10 }, 11 }), 12}));
resolveOffsetConnection
has a few default limits to prevent unintentionally allowing too many
records to be fetched at once. These limits can be configure using the following options:
1{ 2 args: ConnectionArguments; 3 defaultSize?: number; // defaults to 20 4 maxSize?: number; // defaults to 100 5 totalCount?: number // required to support using `last` without `before` 6}
For APIs where you have the full array available you can use resolveArrayConnection
, which works
just like resolveOffsetConnection
and accepts the same options.
1import { resolveArrayConnection } from '@pothos/plugin-relay'; 2 3builder.queryFields((t) => ({ 4 things: t.connection({ 5 type: SomeThings, 6 resolve: (parent, args) => { 7 return resolveArrayConnection({ args }, getAllTheThingsAsArray()); 8 }, 9 }), 10}));
Cursor based pagination can be implemented using the resolveCursorConnection
method. The following
example uses prisma, but a similar solution should work with any data store that supports limits,
ordering, and filtering.
1import { resolveCursorConnection, ResolveCursorConnectionArgs } from '@pothos/plugin-relay'; 2 3builder.queryField('posts', (t) => 4 t.connection({ 5 type: Post, 6 resolve: (_, args) => 7 resolveCursorConnection( 8 { 9 args, 10 toCursor: (post) => post.createdAt.toISOString(), 11 }, 12 // Manually defining the arg type here is required 13 // so that typescript can correctly infer the return value 14 ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => 15 prisma.post.findMany({ 16 take: limit, 17 where: { 18 createdAt: { 19 lt: before, 20 gt: after, 21 }, 22 }, 23 orderBy: { 24 createdAt: inverted ? 'desc' : 'asc', 25 }, 26 }), 27 ), 28 }), 29);
You can use the relayMutationField
method to define relay compliant mutation fields. This method
will generate a mutation field, an input object with a clientMutationId
field, and an output
object with the corresponding clientMutationId
.
Example ussage:
1builder.relayMutationField( 2 'deleteItem', 3 { 4 inputFields: (t) => ({ 5 id: t.id({ 6 required: true, 7 }), 8 }), 9 }, 10 { 11 resolve: async (root, args, ctx) => { 12 if (ctx.items.has(args.input.id)) { 13 ctx.items.delete(args.input.id); 14 15 return { success: true }; 16 } 17 18 return { success: false }; 19 }, 20 }, 21 { 22 outputFields: (t) => ({ 23 success: t.boolean({ 24 resolve: (result) => result.success, 25 }), 26 }), 27 }, 28);
Which produces the following graphql types:
1input DeleteItemInput { 2 clientMutationId: ID! 3 id: ID! 4} 5 6type DeleteItemPayload { 7 clientMutationId: ID! 8 itWorked: Boolean! 9} 10 11type Mutation { 12 deleteItem(input: DeleteItemInput!): DeleteItemPayload! 13}
The relayMutationField
has 4 arguments:
name
: Name of the mutation fieldinputOptions
: Options for the input
object or a ref to an existing input objectfieldOptions
: Options for the mutation fieldpayloadOptions
: Options for the Payload objectThe inputOptions
has a couple of non-standard options:
name
which can be used to set the name of the input objectargName
which can be used to overwrite the default arguments name (input
).The payloadOptions
object also accepts a name
property for setting the name of the payload
object.
You can also access refs for the created input and payload objects so you can re-use them in other fields:
1// Using aliases when destructuring lets you name your refs rather than using the generic `inputType` and `payloadType` 2const { inputType: DeleteItemInput, payloadType: DeleteItemPayload } = builder.relayMutationField( 3 'deleteItem', 4 ... 5);
In some cases you may want to create a connection object type that is shared by multiple fields. To do this, you will need to create the connection object separately and then create a fields using a ref to your connection object:
1import { resolveOffsetConnection } from '@pothos/plugin-relay'; 2 3const ThingsConnection = builder.connectionObject( 4 { 5 // connection options 6 type: SomeThing, 7 name: 'ThingsConnection', 8 }, 9 { 10 // Edge options (optional) 11 name: 'ThingsEdge', // defaults to Appending `Edge` to the Connection name 12 }, 13); 14 15// You can use connection object with normal fields 16builder.queryFields((t) => ({ 17 things: t.field({ 18 type: ThingsConnection, 19 args: { 20 ...t.arg.connectionArgs(), 21 }, 22 resolve: (parent, args) => { 23 return resolveOffsetConnection({ args }, ({ limit, offset }) => { 24 return getThings(offset, limit); 25 }); 26 }, 27 }), 28})); 29 30// Or by providing the connection type when creating a connection field 31builder.queryFields((t) => ({ 32 things: t.connection({ 33 resolve: (parent, args) => { 34 return resolveOffsetConnection({ args }, ({ limit, offset }) => { 35 return getThings(offset, limit); 36 }); 37 }, 38 }), 39 ThingsConnection, 40}));
builder.connectionObject
creates the connect object type and the associated Edge type.
t.arg.connectionArgs()
will create the default connection args.
Similarly you can directly create and re-use edge objects
1import { resolveOffsetConnection } from '@pothos/plugin-relay'; 2 3const ThingsEdge = builder.edgeObject( 4 { 5 name: 'ThingsEdge', 6 type: SomeThing, 7 }, 8); 9 10// The edge object can be used when creating a connection object 11const ThingsConnection = builder.connectionObject( 12 { 13 type: SomeThing, 14 name: 'ThingsConnection', 15 }, 16 ThingsEdge, 17); 18 19// Or when creating a connection field 20builder.queryFields((t) => ({ 21 things: t.connection({ 22 resolve: (parent, args) => { 23 return resolveOffsetConnection({ args }, ({ limit, offset }) => { 24 return getThings(offset, limit); 25 }); 26 }, 27 }), 28 { 29 // connection options 30 }, 31 ThingsEdge, 32})); 33 34
builder.connectionObject
creates the connect object type and the associated Edge type.
t.arg.connectionArgs()
will create the default connection args.
The t.node
and t.nodes
methods can be used to add additional node fields. the expected return
values of id
and ids
fields is the same as the resolve value of t.globalID
, and can either be
a globalID or an object with and an id
and a type
.
Loading nodes by id
uses a request cache, so the same node will only be loaded once per request,
even if it is used multiple times across the schema.
1builder.queryFields((t) => ({ 2 extraNode: t.node({ 3 id: () => 'TnVtYmVyOjI=', 4 }), 5 moreNodes: t.nodeList({ 6 ids: () => ['TnVtYmVyOjI=', { id: 10, type: 'SomeType' }], 7 }), 8}));
The relay plugin exports decodeGlobalID
and encodeGlobalID
as helper methods for interacting
with global IDs directly. If you accept a global ID as an argument you can use the decodeGlobalID
function to decode it:
1builder.mutationFields((t) => ({ 2 updateThing: t.field({ 3 type: Thing, 4 args: { 5 id: t.args.id({ required: true }), 6 update: t.args.string({ required: true }), 7 }, 8 resolve(parent, args) { 9 const { type, id } = decodeGlobalID(args.id); 10 11 const thing = Thing.findById(id); 12 13 thing.update(args.update); 14 15 return thing; 16 }, 17 }), 18}));
In some cases you may want to encode global ids differently than the build in ID encoding. To do this, you can pass a custom encoding and decoding function into the relay options of the builder:
1import RelayPlugin from '@pothos/plugin-relay'; 2const builder = new SchemaBuilder({ 3 plugins: [RelayPlugin], 4 relayOptions: { 5 encodeGlobalID: (typename: string, id: string | number | bigint) => `${typename}:${id}`, 6 decodeGlobalID: (globalID: string) => { 7 const [typename, id] = globalID.split(':'); 8 9 return { typename, id }; 10 }, 11 }, 12});
If you need to customize how nodes are loaded for the node
and or nodes
fields you can provide
custom resolve functions in the builder options for these fields:
1import RelayPlugin from '@pothos/plugin-relay'; 2 3function customUserLoader({ id, typename }: { id: string; typename: string }) { 4 // load user 5} 6 7const builder = new SchemaBuilder({ 8 plugins: [RelayPlugin], 9 relayOptions: { 10 nodeQueryOptions: { 11 resolve: (root, { id }, context, info, resolveNode) => { 12 // use custom loading for User nodes 13 if (id.typename === 'User') { 14 return customUserLoader(id); 15 } 16 17 // fallback to normal loading for everything else 18 return resolveNode(id); 19 }, 20 }, 21 nodesQueryOptions: { 22 resolve: (root, { ids }, context, info, resolveNodes) => { 23 return ids.map((id) => { 24 if (id.typename === 'User') { 25 return customNodeLoader(id); 26 } 27 28 // it would be more efficient to load all the nodes at once 29 // but it is important to ensure the resolver returns nodes in the right order 30 // we are resolving nodes one at a time here for simplicity 31 return resolveNodes([id]); 32 }); 33 }, 34 }, 35 }, 36});
There are 2 builder methods for adding fields to all connection objects: t.globalConnectionField
and t.globalConnectionFields
. These methods work like many of the other methods on the builder for
adding fields to objects or interfaces.
1builder.globalConnectionField('totalCount', (t) => 2 t.int({ 3 nullable: false, 4 resolve: (parent) => 123, 5 }), 6); 7// Or 8builder.globalConnectionFields((t) => ({ 9 totalCount: t.int({ 10 nullable: false, 11 resolve: (parent) => 123, 12 }), 13}));
In the above example, we are just returning a static number for our totalCount
field. To make this
more useful, we need to have our resolvers for each connection actually return an object that
contains a totalCount for us. To guarantee that resolvers correctly implement this behavior, we can
define custom properties that must be returned from connection resolvers when we set up our builder:
1import RelayPlugin from '@pothos/plugin-relay'; 2const builder = new SchemaBuilder<{ 3 Connection: { 4 totalCount: number; 5 }; 6}>({ 7 plugins: [RelayPlugin], 8 relayOptions: {}, 9});
Now typescript will ensure that objects returned from each connection resolver include a totalCount property, which we can use in our connection fields:
1builder.globalConnectionField('totalCount', (t) => 2 t.int({ 3 nullable: false, 4 resolve: (parent) => parent.totalCount, 5 }), 6);
Note that adding additional required properties will make it harder to use the provided connection helpers since they will not automatically return your custom properties. You will need to manually add in any custom props after getting the result from the helpers:
1builder.queryFields((t) => ({ 2 posts: t.connection({ 3 type: Post, 4 resolve: (parent, args, context) => { 5 const postsArray = context.Posts.getAll(); 6 const result = resolveArrayConnection({ args }, postsArray); 7 8 return result && { totalCount: postsArray.length, ...result }; 9 }, 10 }), 11}));
If you want to change the nullability of the edges
field on a Connection
or the node
field on
an Edge
you can configure this in 2 ways:
1import RelayPlugin from '@pothos/plugin-relay'; 2const builder = new SchemaBuilder<{ 3 DefaultEdgesNullability: false; 4 DefaultNodeNullability: true; 5}>({ 6 plugins: [RelayPlugin], 7 relayOptions: { 8 edgesFieldOptions: { 9 nullable: false, 10 }, 11 nodeFieldOptions: { 12 nullable: true, 13 }, 14 }, 15});
The types provided for DefaultEdgesNullability
and DefaultNodeNullability
must match the values
provided in the nullable option of edgesFieldOptions
and nodeFieldOptions
respectively. This
will set the default nullability for all connections created by your builder.
nullability for edges
fields defaults to { list: options.defaultFieldNullability, items: true }
and the nullability of node
fields is the same as options.defaultFieldNullability
(which
defaults to true
).
1builder.queryFields((t) => ({ 2 things: t.connection({ 3 type: SomeThings, 4 edgesNullable: { 5 items: true, 6 list: false, 7 }, 8 nodeNullable: false, 9 resolve: (parent, args) => { 10 return resolveOffsetConnection({ args }, ({ limit, offset }) => { 11 return getThings(offset, limit); 12 }); 13 }, 14 }), 15})); 16// Or 17 18const ThingsConnection = builder.connectionObject({ 19 type: SomeThing, 20 name: 'ThingsConnection', 21 edgesNullable: { 22 items: true, 23 list: false, 24 }, 25 nodeNullable: false, 26});
Node
interfaceUse the nodeInterfaceRef
method of your Builder.
For example, to add a new derived field on the interface:
1builder.interfaceField(builder.nodeInterfaceRef(), 'extra', (t) => 2 t.string({ 3 resolve: () => 'it works', 4 }), 5);
No vulnerabilities found.
No security vulnerabilities found.
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year