Skip to content

docs: editorial for recent documentation updates #4395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 16.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions website/pages/docs/advanced-custom-scalars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,36 @@ describe('DateTime scalar', () => {
Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior.

```js
const { graphql, buildSchema } = require('graphql');
const { graphql, GraphQLSchema, GraphQLObjectType } = require('graphql');
const { DateTimeResolver as DateTime } = require('graphql-scalars');

const Query = new GraphQLObjectType({
name: 'Query',
fields: {
now: {
type: DateTime,
resolve() {
return new Date();
},
},
},
});

const schema = buildSchema(`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildSchema approach didn't allow the custom DateTime scalar to be implemented, so I replaced it with the class-based approach.

/*
scalar DateTime

type Query {
now: DateTime
}
`);

const rootValue = {
now: () => new Date('2024-01-01T00:00:00Z'),
};
*/
const schema = new GraphQLSchema({
query: Query,
});

async function testQuery() {
const response = await graphql({
schema,
source: '{ now }',
rootValue,
});
console.log(response);
}
Expand Down Expand Up @@ -181,13 +192,22 @@ If you need domain-specific behavior, you can wrap an existing scalar with custo
```js
const { EmailAddressResolver } = require('graphql-scalars');

const StrictEmail = new GraphQLScalarType({
const StrictEmailAddress = new GraphQLScalarType({
...EmailAddressResolver,
name: 'StrictEmailAddress',
parseValue(value) {
if (!value.endsWith('@example.com')) {
const email = EmailAddressResolver.parseValue(value);
if (!email.endsWith('@example.com')) {
throw new TypeError('Only example.com emails are allowed.');
}
return email;
},
parseLiteral(literal, variables) {
const email = EmailAddressResolver.parseLiteral(literal, variables);
if (!email.endsWith('@example.com')) {
throw new TypeError('Only example.com emails are allowed.');
}
return EmailAddressResolver.parseValue(value);
return email;
},
Comment on lines +195 to 211
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We commented that it was essential to implement parseLiteral alongside parseValue so I figured we should do that! I also restructured so the underlying scalar does the hard work (especially for literal!) and then we just check the end result. Further, I showed renaming the scalar.

});
```
Expand Down
35 changes: 28 additions & 7 deletions website/pages/docs/cursor-based-pagination.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Implementing Cursor-based Pagination
---

import { Callout } from "nextra/components";

When a GraphQL API returns a list of data, pagination helps avoid
fetching too much data at once. Cursor-based pagination fetches items
relative to a specific point in the list, rather than using numeric offsets.
Expand All @@ -18,15 +20,15 @@ that works well with clients.

Cursor-based pagination typically uses a structured format that separates
pagination metadata from the actual data. The most widely adopted pattern follows the
[Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We renamed this spec to the GraphQL Cursor Connections Specification by agreement at the GraphQL WG a couple years back. Relay hosts it, but it's not really a "relay" spec any more - it's very widely used.

this format originated in Relay, many GraphQL APIs use it independently because of its
clarity and flexibility.

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

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

The following query and response show how this structure works:
Expand Down Expand Up @@ -192,7 +194,7 @@ const usersField = {
let start = 0;
if (args.after) {
const index = decodeCursor(args.after);
if (index != null) {
if (Number.isFinite(index)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is me being a bit pedantic on the security front, but it's possible that a user could pass something that when you parseInt it would become NaN (e.g. cursor:frog), so the isFinite check ensures that the number is reasonable. Really the hardening should take place in decodeCursor but this felt a cleaner edit.

start = index + 1;
}
}
Expand Down Expand Up @@ -243,7 +245,7 @@ async function resolveUsers(_, args) {

if (args.after) {
const index = decodeCursor(args.after);
if (index != null) {
if (Number.isFinite(index)) {
offset = index + 1;
}
}
Expand Down Expand Up @@ -279,6 +281,25 @@ an `OFFSET`. To paginate backward, you can reverse the sort order and slice the
results accordingly, or use keyset pagination for improved performance on large
datasets.

<Callout type='info'>

The above is just an example to aid understanding; in a production application,
for most databases it is better to use `WHERE` clauses to implement cursor
pagination rather than using `OFFSET`. Using `WHERE` can leverage indices
(indexes) to jump directly to the relevant records, whereas `OFFSET` typically
must scan over and discard that number of records. When paginating very large
datasets, `OFFSET` can become more expensive as the value grows, whereas using
`WHERE` tends to have fixed cost. Using `WHERE` can also typically handle the
addition or removal of data more gracefully.

For example, if you were ordering a collection of users by username, you could
use the username itself as the `cursor`, thus GraphQL's `allUsers(first: 10,
after: $cursor)` could become SQL's `WHERE username > $1 LIMIT 10`. Even if
that user was deleted, you could still continue to paginate from that position
onwards.

</Callout>

## Handling edge cases

When implementing pagination, consider how your resolver should handle the following scenarios:
Expand All @@ -297,7 +318,7 @@ errors.

To learn more about cursor-based pagination patterns and best practices, see:

- [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
- [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
- [Pagination](https://graphql.org/learn/pagination/) guide on graphql.org
- [`graphql-relay-js`](https://github.com/graphql/graphql-relay-js): Utility library for
building Relay-compatible GraphQL servers using GraphQL.js
5 changes: 5 additions & 0 deletions website/pages/docs/custom-scalars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ providing a name, description, and three functions:
- `serialize`: How the server sends internal values to clients.
- `parseValue`: How the server parses incoming variable values.
- `parseLiteral`: How the server parses inline values in queries.
- `specifiedByURL` (optional): A URL specifying the behavior of your scalar;
this can be used by clients and tooling to recognize and handle common scalars
such as [date-time](https://scalars.graphql.org/andimarek/date-time.html)
independent of their name.

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

serialize(value) {
if (!(value instanceof Date)) {
Expand Down
27 changes: 21 additions & 6 deletions website/pages/docs/graphql-errors.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: Understanding GraphQL.js Errors
---
import { Callout, GitHubNoteIcon } from "nextra/components";

# Understanding GraphQL.js Errors

Expand Down Expand Up @@ -37,13 +38,22 @@ For example:
Each error object can include the following fields:

- `message`: A human-readable description of the error.
- `locations` (optional): Where the error occurred in the operation.
- `locations` (optional): Where the error occurred in the operation document.
- `path` (optional): The path to the field that caused the error.
- `extensions` (optional): Additional error metadata, often used for error codes, HTTP status
codes or debugging information.

The GraphQL specification only requires the `message` field. All others are optional, but
recommended to help clients understand and react to errors.
<Callout type="info">

The GraphQL specification separates errors into two types: _request_ errors, and
_execution_ errors. Request errors indicate something went wrong that prevented
the GraphQL operation from executing, for example the document is invalid, and
only requires the `message` field. Execution errors indicate something went
wrong during execution, typically due to the result of calling a resolver, and
requires both the `message` and `path` fields to be present. All others fields
are optional, but recommended to help clients understand and react to errors.

</Callout>

## Creating and handling errors with `GraphQLError`

Expand Down Expand Up @@ -81,12 +91,16 @@ Each option helps tie the error to specific parts of the GraphQL execution:

When a resolver throws an error:

- If the thrown value is already a `GraphQLError`, GraphQL.js uses it as-is.
- If it is another type of error (such as a built-in `Error`), GraphQL.js wraps it into a
`GraphQLError`.
- If the thrown value is a `GraphQLError` and contains the required information
(`path`), GraphQL.js uses it as-is.
- Otherwise, GraphQL.js wraps it into a `GraphQLError`.

This ensures that all errors returned to the client follow a consistent structure.

You may throw any type of error that makes sense in your application; throwing
`Error` is fine, you do not need to throw `GraphQLError`. However, ensure that
your errors do not reveal security sensitive information.

## How errors propagate during execution

Errors in GraphQL don't necessarily abort the entire operation. How an error affects the response
Expand All @@ -96,6 +110,7 @@ depends on the nullability of the field where the error occurs.
the error and sets the field's value to `null` in the `data` payload.
- **Non-nullable fields**: If a resolver for a non-nullable field throws an error, GraphQL.js
records the error and then sets the nearest parent nullable field to `null`.
If no such nullable field exists, then the operation root will be set `null` (`"data": null`).

For example, consider the following schema:

Expand Down
23 changes: 12 additions & 11 deletions website/pages/docs/n1-dataloader.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Solving the N+1 Problem with `DataLoader`
---

When building a server with GraphQL.js, it's common to encounter
When building your first server with GraphQL.js, it's common to encounter
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to soften the wording here, don't want people saying "See! Even the GraphQL documentation says that it has major performance issues!" without bothering to read on. Wanted to make this clear this is a mistake you might make early on, but once you've built a few GraphQL schemas using batching will be your bread and butter.

performance issues related to the N+1 problem: a pattern that
results in many unnecessary database or service calls,
especially in nested query structures.
Expand Down Expand Up @@ -69,7 +69,8 @@ when setting up a GraphQL HTTP server such as [express-graphql](https://github.c
Suppose each `Post` has an `authorId`, and you have a `getUsersByIds(ids)`
function that can fetch multiple users in a single call:

```js
{/* prettier-ignore */}
```js {14-17,37}
import {
graphql,
GraphQLObjectType,
Expand All @@ -81,6 +82,15 @@ import {
import DataLoader from 'dataloader';
import { getPosts, getUsersByIds } from './db.js';

function createContext() {
return {
userLoader: new DataLoader(async (userIds) => {
const users = await getUsersByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
}),
};
}

const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
Expand Down Expand Up @@ -114,15 +124,6 @@ const QueryType = new GraphQLObjectType({
});

const schema = new GraphQLSchema({ query: QueryType });

function createContext() {
return {
userLoader: new DataLoader(async (userIds) => {
const users = await getUsersByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
}),
};
}
```

With this setup, all `.load(authorId)` calls are automatically collected and batched
Expand Down
9 changes: 5 additions & 4 deletions website/pages/docs/resolver-anatomy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ When GraphQL.js executes a resolver, it calls the resolver function
with four arguments:

```js
function resolver(source, args, context, info) { ... }
function resolve(source, args, context, info) { ... }
```

Each argument provides information that can help the resolver return the
correct value:

- `source`: The result from the parent field's resolver. In nested fields,
`source` contains the value returned by the parent object. For root fields,
it is often `undefined`.
`source` contains the value returned by the parent object (after resolving any
lists). For root fields, it is the `rootValue` passed to GraphQL, which is often
Comment on lines +43 to +44
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parenthesis might not be the best way of saying this, but source is essentially the list item inside a list if there is a list, and that's quite important for users to realise.

left `undefined`.
- `args`: An object containing the arguments passed to the field in the
query. For example, if a field is defined to accept an `id` argument, you can
access it as `args.id`.
Expand Down Expand Up @@ -85,7 +86,7 @@ A custom resolver is a function you define to control exactly how a field's
value is fetched or computed. You can add a resolver by specifying a `resolve`
function when defining a field in your schema:

```js
```js {6-8}
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
Expand Down
Loading