Skip to content

Commit ae3f7d0

Browse files
authored
Merge branch '16.x.x' into handle-unhandled-exception
2 parents 20263e7 + 3f5858b commit ae3f7d0

File tree

9 files changed

+331
-39
lines changed

9 files changed

+331
-39
lines changed

website/pages/docs/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const meta = {
1717
title: 'Advanced Guides',
1818
},
1919
'constructing-types': '',
20+
'abstract-types': '',
2021
'oneof-input-objects': '',
2122
'defer-stream': '',
2223
'cursor-based-pagination': '',
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
---
2+
title: Abstract types in GraphQL.js
3+
---
4+
5+
GraphQL includes two kinds of abstract types: interfaces and unions. These types let a single
6+
field return values of different object types, while keeping your schema type-safe.
7+
8+
This guide covers how to define and resolve abstract types using GraphQL.js. It focuses on
9+
constructing types in JavaScript using the GraphQL.js type system, not the schema definition
10+
language (SDL).
11+
12+
## What are abstract types?
13+
14+
Most GraphQL types are concrete. They represent a specific kind of object, for example, a
15+
`Book` or an `Author`. Abstract types let a field return different types of objects depending
16+
on the data.
17+
18+
This is useful when the return type can vary but comes from a known set. For example, a `search`
19+
field might return a book, an author, or a publisher. Abstract types let you model this kind of
20+
flexibility while preserving validation, introspection, and tool support.
21+
22+
GraphQL provides two kinds of abstract types:
23+
24+
- Interfaces define a set of fields that multiple object types must implement.
25+
- Use case: A `ContentItem` interface with fields like `id`, `title`, and `publishedAt`,
26+
implemented by types such as `Article` and `PodcastEpisode`.
27+
- Unions group together unrelated types that don't share any fields.
28+
- Use case: A `SearchResult` union that includes `Book`, `Author`, and `Publisher` types.
29+
30+
## Defining interfaces
31+
32+
To define an interface in GraphQL.js, use the `GraphQLInterfaceType` constructor. An interface
33+
must include a `name`, a `fields` function, and a `resolveType` function, which tells GraphQL which
34+
concrete type a given value corresponds to.
35+
36+
The following example defines a `ContentItem` interface for a publishing platform:
37+
38+
```js
39+
const { GraphQLInterfaceType, GraphQLString, GraphQLNonNull } = require('graphql');
40+
41+
const ContentItemInterface = new GraphQLInterfaceType({
42+
name: 'ContentItem',
43+
fields: {
44+
id: { type: new GraphQLNonNull(GraphQLString) },
45+
title: { type: GraphQLString },
46+
publishedAt: { type: GraphQLString },
47+
},
48+
resolveType(value) {
49+
if (value.audioUrl) {
50+
return 'PodcastEpisode';
51+
}
52+
if (value.bodyText) {
53+
return 'Article';
54+
}
55+
return null;
56+
},
57+
});
58+
```
59+
60+
You can return either the type name as a string or the corresponding `GraphQLObjectType` instance.
61+
Returning the instance is recommended when possible for better type safety and tooling support.
62+
63+
## Implementing interfaces with object types
64+
65+
To implement an interface, define a `GraphQLObjectType` and include the interface in its
66+
`interfaces` array. The object type must implement all fields defined by the interface.
67+
68+
The following example implements the `Article` and `PodcastEpisode` types that
69+
conform to the `ContentItem` interface:
70+
71+
```js
72+
const { GraphQLObjectType, GraphQLString, GraphQLNonNull } = require('graphql');
73+
74+
const ArticleType = new GraphQLObjectType({
75+
name: 'Article',
76+
interfaces: [ContentItemInterface],
77+
fields: {
78+
id: { type: new GraphQLNonNull(GraphQLString) },
79+
title: { type: GraphQLString },
80+
publishedAt: { type: GraphQLString },
81+
bodyText: { type: GraphQLString },
82+
},
83+
isTypeOf: (value) => value.bodyText !== undefined,
84+
});
85+
86+
const PodcastEpisodeType = new GraphQLObjectType({
87+
name: 'PodcastEpisode',
88+
interfaces: [ContentItemInterface],
89+
fields: {
90+
id: { type: new GraphQLNonNull(GraphQLString) },
91+
title: { type: GraphQLString },
92+
publishedAt: { type: GraphQLString },
93+
audioUrl: { type: GraphQLString },
94+
},
95+
isTypeOf: (value) => value.audioUrl !== undefined,
96+
});
97+
```
98+
99+
The `isTypeOf` function is optional. It provides a fallback when `resolveType` isn't defined, or
100+
when runtime values could match multiple types. If both `resolveType` and `isTypeOf` are defined,
101+
GraphQL uses `resolveType`.
102+
103+
## Defining union types
104+
105+
Use the `GraphQLUnionType` constructor to define a union. A union allows a field to return one
106+
of several object types that don't need to share fields.
107+
108+
A union requires:
109+
110+
- A `name`
111+
- A list of object types (`types`)
112+
- A `resolveType` function
113+
114+
The following example defines a `SearchResult` union:
115+
116+
```js
117+
const { GraphQLUnionType } = require('graphql');
118+
119+
const SearchResultType = new GraphQLUnionType({
120+
name: 'SearchResult',
121+
types: [BookType, AuthorType, PublisherType],
122+
resolveType(value) {
123+
if (value.isbn) {
124+
return 'Book';
125+
}
126+
if (value.bio) {
127+
return 'Author';
128+
}
129+
if (value.catalogSize) {
130+
return 'Publisher';
131+
}
132+
return null;
133+
},
134+
});
135+
```
136+
137+
Unlike interfaces, unions don’t declare any fields of their own. Clients use inline fragments
138+
to query fields from the concrete types.
139+
140+
## Resolving abstract types at runtime
141+
142+
GraphQL resolves abstract types dynamically during execution using the `resolveType` function.
143+
144+
This function receives the following arguments:
145+
146+
```js
147+
resolveType(value, context, info)
148+
```
149+
150+
It can return:
151+
152+
- A `GraphQLObjectType` instance (recommended)
153+
- The name of a type as a string
154+
- A `Promise` resolving to either of the above
155+
156+
If `resolveType` isn't defined, GraphQL falls back to checking each possible type's `isTypeOf`
157+
function. This fallback is less efficient and makes type resolution harder to debug. For most cases,
158+
explicitly defining `resolveType` is recommended.
159+
160+
## Querying abstract types
161+
162+
To query a field that returns an abstract type, use inline fragments to select fields from the
163+
possible concrete types. GraphQL evaluates each fragment based on the runtime type of the result.
164+
165+
For example:
166+
167+
```graphql
168+
{
169+
search(term: "deep learning") {
170+
... on Book {
171+
title
172+
isbn
173+
}
174+
... on Author {
175+
name
176+
bio
177+
}
178+
... on Publisher {
179+
name
180+
catalogSize
181+
}
182+
}
183+
}
184+
```
185+
186+
GraphQL's introspection system lists all possible types for each interface and union, which
187+
enables code generation and editor tooling to provide type-aware completions.
188+
189+
## Best practices
190+
191+
- Always implement `resolveType` for interfaces and unions to handle runtime type resolution.
192+
- Return the `GraphQLObjectType` instance when possible for better clarity and static analysis.
193+
- Keep `resolveType` logic simple, using consistent field shapes or tags to distinguish
194+
types.
195+
- Test `resolveType` logic carefully. Errors in `resolveType` can cause runtime errors that can
196+
be hard to trace.
197+
- Use interfaces when types share fields and unions when types are structurally unrelated.
198+
199+
## Additional resources
200+
201+
- [Constructing Types](https://www.graphql-js.org/docs/constructing-types/)
202+
- GraphQL Specification:
203+
- [Interfaces](https://spec.graphql.org/October2021/#sec-Interfaces)
204+
- [Unions](https://spec.graphql.org/October2021/#sec-Unions)

website/pages/docs/advanced-custom-scalars.mdx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,36 @@ describe('DateTime scalar', () => {
100100
Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior.
101101

102102
```js
103-
const { graphql, buildSchema } = require('graphql');
103+
const { graphql, GraphQLSchema, GraphQLObjectType } = require('graphql');
104+
const { DateTimeResolver as DateTime } = require('graphql-scalars');
105+
106+
const Query = new GraphQLObjectType({
107+
name: 'Query',
108+
fields: {
109+
now: {
110+
type: DateTime,
111+
resolve() {
112+
return new Date();
113+
},
114+
},
115+
},
116+
});
104117

105-
const schema = buildSchema(`
118+
/*
106119
scalar DateTime
107120
108121
type Query {
109122
now: DateTime
110123
}
111-
`);
112-
113-
const rootValue = {
114-
now: () => new Date('2024-01-01T00:00:00Z'),
115-
};
124+
*/
125+
const schema = new GraphQLSchema({
126+
query: Query,
127+
});
116128

117129
async function testQuery() {
118130
const response = await graphql({
119131
schema,
120132
source: '{ now }',
121-
rootValue,
122133
});
123134
console.log(response);
124135
}
@@ -181,13 +192,22 @@ If you need domain-specific behavior, you can wrap an existing scalar with custo
181192
```js
182193
const { EmailAddressResolver } = require('graphql-scalars');
183194

184-
const StrictEmail = new GraphQLScalarType({
195+
const StrictEmailAddress = new GraphQLScalarType({
185196
...EmailAddressResolver,
197+
name: 'StrictEmailAddress',
186198
parseValue(value) {
187-
if (!value.endsWith('@example.com')) {
199+
const email = EmailAddressResolver.parseValue(value);
200+
if (!email.endsWith('@example.com')) {
201+
throw new TypeError('Only example.com emails are allowed.');
202+
}
203+
return email;
204+
},
205+
parseLiteral(literal, variables) {
206+
const email = EmailAddressResolver.parseLiteral(literal, variables);
207+
if (!email.endsWith('@example.com')) {
188208
throw new TypeError('Only example.com emails are allowed.');
189209
}
190-
return EmailAddressResolver.parseValue(value);
210+
return email;
191211
},
192212
});
193213
```

website/pages/docs/cursor-based-pagination.mdx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
title: Implementing Cursor-based Pagination
33
---
44

5+
import { Callout } from "nextra/components";
6+
57
When a GraphQL API returns a list of data, pagination helps avoid
68
fetching too much data at once. Cursor-based pagination fetches items
79
relative to a specific point in the list, rather than using numeric offsets.
@@ -18,15 +20,15 @@ that works well with clients.
1820

1921
Cursor-based pagination typically uses a structured format that separates
2022
pagination metadata from the actual data. The most widely adopted pattern follows the
21-
[Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While
23+
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While
2224
this format originated in Relay, many GraphQL APIs use it independently because of its
2325
clarity and flexibility.
2426

2527
This pattern wraps your list of items in a connection type, which includes the following fields:
2628

27-
- `edges`: A list of edge objects, each representing an item in the list.
28-
- `node`: The actual object you want to retrieve, such as user, post, or comment.
29-
- `cursor`: An opaque string that identifies the position of the item in the list.
29+
- `edges`: A list of edge objects, representing for each item in the list:
30+
- `node`: The actual object you want to retrieve, such as user, post, or comment.
31+
- `cursor`: An opaque string that identifies the position of the item in the list.
3032
- `pageInfo`: Metadata about the list, such as whether more items are available.
3133

3234
The following query and response show how this structure works:
@@ -192,7 +194,7 @@ const usersField = {
192194
let start = 0;
193195
if (args.after) {
194196
const index = decodeCursor(args.after);
195-
if (index != null) {
197+
if (Number.isFinite(index)) {
196198
start = index + 1;
197199
}
198200
}
@@ -243,7 +245,7 @@ async function resolveUsers(_, args) {
243245

244246
if (args.after) {
245247
const index = decodeCursor(args.after);
246-
if (index != null) {
248+
if (Number.isFinite(index)) {
247249
offset = index + 1;
248250
}
249251
}
@@ -279,6 +281,25 @@ an `OFFSET`. To paginate backward, you can reverse the sort order and slice the
279281
results accordingly, or use keyset pagination for improved performance on large
280282
datasets.
281283
284+
<Callout type='info'>
285+
286+
The above is just an example to aid understanding; in a production application,
287+
for most databases it is better to use `WHERE` clauses to implement cursor
288+
pagination rather than using `OFFSET`. Using `WHERE` can leverage indices
289+
(indexes) to jump directly to the relevant records, whereas `OFFSET` typically
290+
must scan over and discard that number of records. When paginating very large
291+
datasets, `OFFSET` can become more expensive as the value grows, whereas using
292+
`WHERE` tends to have fixed cost. Using `WHERE` can also typically handle the
293+
addition or removal of data more gracefully.
294+
295+
For example, if you were ordering a collection of users by username, you could
296+
use the username itself as the `cursor`, thus GraphQL's `allUsers(first: 10,
297+
after: $cursor)` could become SQL's `WHERE username > $1 LIMIT 10`. Even if
298+
that user was deleted, you could still continue to paginate from that position
299+
onwards.
300+
301+
</Callout>
302+
282303
## Handling edge cases
283304
284305
When implementing pagination, consider how your resolver should handle the following scenarios:
@@ -297,7 +318,7 @@ errors.
297318
298319
To learn more about cursor-based pagination patterns and best practices, see:
299320
300-
- [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
321+
- [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
301322
- [Pagination](https://graphql.org/learn/pagination/) guide on graphql.org
302323
- [`graphql-relay-js`](https://github.com/graphql/graphql-relay-js): Utility library for
303324
building Relay-compatible GraphQL servers using GraphQL.js

website/pages/docs/custom-scalars.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ providing a name, description, and three functions:
8080
- `serialize`: How the server sends internal values to clients.
8181
- `parseValue`: How the server parses incoming variable values.
8282
- `parseLiteral`: How the server parses inline values in queries.
83+
- `specifiedByURL` (optional): A URL specifying the behavior of your scalar;
84+
this can be used by clients and tooling to recognize and handle common scalars
85+
such as [date-time](https://scalars.graphql.org/andimarek/date-time.html)
86+
independent of their name.
8387

8488
The following example is a custom `DateTime` scalar that handles ISO-8601 encoded
8589
date strings:
@@ -90,6 +94,7 @@ const { GraphQLScalarType, Kind } = require('graphql');
9094
const DateTime = new GraphQLScalarType({
9195
name: 'DateTime',
9296
description: 'An ISO-8601 encoded UTC date string.',
97+
specifiedByURL: 'https://scalars.graphql.org/andimarek/date-time.html',
9398

9499
serialize(value) {
95100
if (!(value instanceof Date)) {

0 commit comments

Comments
 (0)