diff --git a/.c8rc.json b/.c8rc.json index 4781e9f9a0..3a1c03886f 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -7,7 +7,8 @@ "src/jsutils/Maybe.ts", "src/jsutils/ObjMap.ts", "src/jsutils/PromiseOrValue.ts", - "src/utilities/typedQueryDocumentNode.ts" + "src/utilities/typedQueryDocumentNode.ts", + "src/**/__tests__/**/*.ts" ], "clean": true, "report-dir": "reports/coverage", diff --git a/README.md b/README.md index 8908c605f5..0802a97772 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![GraphQLConf 2024 Banner: September 10-12, San Francisco. Hosted by the GraphQL Foundation](https://github.com/user-attachments/assets/2d048502-e5b2-4e9d-a02a-50b841824de6)](https://graphql.org/conf/2024/?utm_source=github&utm_medium=graphql_js&utm_campaign=readme) +[![GraphQLConf 2025 Banner: September 08-10, Amsterdam. Hosted by the GraphQL Foundation](./assets/graphql-conf-2025.png)](https://graphql.org/conf/2025/?utm_source=github&utm_medium=graphql_js&utm_campaign=readme) # GraphQL.js @@ -110,7 +110,7 @@ graphql({ schema, source }).then((result) => { **Note**: Please don't forget to set `NODE_ENV=production` if you are running a production server. It will disable some checks that can be useful during development but will significantly improve performance. -### Want to ride the bleeding edge? +## Want to ride the bleeding edge? The `npm` branch in this repository is automatically maintained to be the last commit to `main` to pass all tests, in the same form found on npm. It is @@ -134,7 +134,7 @@ the portions of the library you use. This works because GraphQL.js is distribute with both CommonJS (`require()`) and ESModule (`import`) files. Ensure that any custom build configurations look for `.mjs` files! -### Contributing +## Contributing We actively welcome pull requests. Learn how to [contribute](./.github/CONTRIBUTING.md). @@ -146,10 +146,53 @@ You can find [detailed information here](https://github.com/graphql/graphql-wg/t If your company benefits from GraphQL and you would like to provide essential financial support for the systems and people that power our community, please also consider membership in the [GraphQL Foundation](https://foundation.graphql.org/join). -### Changelog +## Changelog Changes are tracked as [GitHub releases](https://github.com/graphql/graphql-js/releases). -### License +## License GraphQL.js is [MIT-licensed](./LICENSE). + +## Version Support + +GraphQL.JS follows Semantic Versioning (SemVer) for its releases. Our version support policy is as follows: + +- Latest Major Version: We provide full support, including bug fixes and security updates, for the latest major version of GraphQL.JS. +- Previous Major Version: We offer feature support for the previous major version for 12 months after the release of the newest major version. + This means that for 12 months we can backport features for specification changes _if_ they don't cause any breaking changes. We'll continue + supporting the previous major version with bug and security fixes. +- Older Versions: Versions older than the previous major release are considered unsupported. While the code remains available, + we do not actively maintain or provide updates for these versions. + One exception to this rule is when the older version has been released < 1 year ago, in that case we + will treat it like the "Previous Major Version". + +### Long-Term Support (LTS) + +We do not currently offer a Long-Term Support version of GraphQL.JS. Users are encouraged to upgrade to the latest stable version +to receive the most up-to-date features, performance improvements, and security updates. + +### End-of-Life (EOL) Schedule + +We will announce the EOL date for a major version at least 6 months in advance. +After a version reaches its EOL, it will no longer receive updates, even for critical security issues. + +### Upgrade Assistance + +To assist users in upgrading to newer versions: + +- We maintain detailed release notes for each version, highlighting new features, breaking changes, and deprecations. +- [Our documentation](https://www.graphql-js.org/) includes migration guides for moving between major versions. +- The [community forum (Discord channel #graphql-js)](https://discord.graphql.org) is available for users who need additional assistance with upgrades. + +### Security Updates + +We prioritize the security of GraphQL.JS: + +- Critical security updates will be applied to both the current and previous major version. +- For versions that have reached EOL, we strongly recommend upgrading to a supported version to receive security updates. + +### Community Contributions + +We welcome community contributions for all versions of GraphQL.JS. However, our maintainers will primarily focus on reviewing +and merging contributions for supported versions. diff --git a/assets/graphql-conf-2025.png b/assets/graphql-conf-2025.png new file mode 100644 index 0000000000..d2c7ec22b0 Binary files /dev/null and b/assets/graphql-conf-2025.png differ diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index b93f075534..ac6a22a989 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -5,6 +5,10 @@ export const bigSchemaSDL = fs.readFileSync( 'utf8', ); +export const bigDocumentSDL = JSON.parse( + fs.readFileSync(new URL('kitchen-sink.graphql', import.meta.url), 'utf8'), +); + export const bigSchemaIntrospectionResult = JSON.parse( fs.readFileSync(new URL('github-schema.json', import.meta.url), 'utf8'), ); diff --git a/benchmark/kitchen-sink.graphql b/benchmark/kitchen-sink.graphql new file mode 100644 index 0000000000..8d9c6ab341 --- /dev/null +++ b/benchmark/kitchen-sink.graphql @@ -0,0 +1,65 @@ +query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { + whoever123is: node(id: [123, 456]) { + id + ... on User @onInlineFragment { + field2 { + id + alias: field1(first: 10, after: $foo) @include(if: $foo) { + id + ...frag @onFragmentSpread + } + } + } + ... @skip(unless: $foo) { + id + } + ... { + id + } + } +} + +mutation likeStory @onMutation { + like(story: 123) @onField { + story { + id @onField + } + } +} + +subscription StoryLikeSubscription( + $input: StoryLikeSubscribeInput @onVariableDefinition +) @onSubscription { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + +fragment frag on Friend @onFragmentDefinition { + foo( + size: $size + bar: $b + obj: { + key: "value" + block: """ + block string uses \""" + """ + } + ) +} + +{ + unnamed(truthy: true, falsy: false, nullish: null) + query +} + +query { + __typename +} diff --git a/benchmark/printer-benchmark.js b/benchmark/printer-benchmark.js new file mode 100644 index 0000000000..e8da1f2b97 --- /dev/null +++ b/benchmark/printer-benchmark.js @@ -0,0 +1,14 @@ +import { parse } from 'graphql/language/parser.js'; +import { print } from 'graphql/language/printer.js'; + +import { bigDocumentSDL } from './fixtures.js'; + +const document = parse(bigDocumentSDL); + +export const benchmark = { + name: 'Print kitchen sink document', + count: 1000, + measure() { + print(document); + }, +}; diff --git a/cspell.yml b/cspell.yml index 9454626484..efaf7a3048 100644 --- a/cspell.yml +++ b/cspell.yml @@ -27,6 +27,18 @@ overrides: - swcrc - noreferrer - xlink + - codegen + - composability + - deduplication + - debuggable + - subschema + - subschemas + - NATS + - benjie + - codegen + - URQL + - tada + - Graphile validateDirectives: true ignoreRegExpList: @@ -114,6 +126,8 @@ words: - svgr - ruru - oneof + - vercel + - unbatched # used as href anchors - graphqlerror @@ -169,3 +183,7 @@ words: - XXXF - bfnrt - wrds + - overcomplicating + - cacheable + - pino + - debuggable diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 6e72609fec..05e1c293f9 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -10,6 +10,7 @@ import { Kind } from '../../language/kinds.js'; import { parse } from '../../language/parser.js'; import { + GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, @@ -1380,4 +1381,74 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { foo: { bar: 'bar' } } }); expect(possibleTypes).to.deep.equal([fooObject]); }); + + it('uses a different number of max coercion errors', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + dummy: { type: GraphQLString }, + }, + }), + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: { + updateUser: { + type: GraphQLString, + args: { + data: { + type: new GraphQLInputObjectType({ + name: 'User', + fields: { + email: { type: new GraphQLNonNull(GraphQLString) }, + }, + }), + }, + }, + }, + }, + }), + }); + + const document = parse(` + mutation ($data: User) { + updateUser(data: $data) + } + `); + + const options = { + maxCoercionErrors: 1, + }; + + const result = executeSync({ + schema, + document, + variableValues: { + data: { + email: '', + wrongArg: 'wrong', + wrongArg2: 'wrong', + wrongArg3: 'wrong', + }, + }, + options, + }); + + // Returns at least 2 errors, one for the first 'wrongArg', and one for coercion limit + expect(result.errors).to.have.lengthOf(options.maxCoercionErrors + 1); + + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + 'Variable "$data" has invalid value: Expected value of type "User" not to include unknown field "wrongArg", found: { email: "", wrongArg: "wrong", wrongArg2: "wrong", wrongArg3: "wrong" }.', + locations: [{ line: 2, column: 17 }], + }, + { + message: + 'Too many errors processing variables, error limit reached. Execution aborted.', + }, + ], + }); + }); }); diff --git a/src/execution/__tests__/union-interface-test.ts b/src/execution/__tests__/union-interface-test.ts index 6f8408c487..347c59f5a3 100644 --- a/src/execution/__tests__/union-interface-test.ts +++ b/src/execution/__tests__/union-interface-test.ts @@ -143,7 +143,7 @@ const PetType = new GraphQLUnionType({ if (value instanceof Cat) { return CatType.name; } - /* c8 ignore next 3 */ + // Not reachable, all possible types have been considered. expect.fail('Not reachable'); }, @@ -191,6 +191,70 @@ const john = new Person( [garfield, fern], ); +const SearchableInterface = new GraphQLInterfaceType({ + name: 'Searchable', + fields: { + id: { type: GraphQLString }, + }, +}); + +const TypeA = new GraphQLObjectType({ + name: 'TypeA', + interfaces: [SearchableInterface], + fields: () => ({ + id: { type: GraphQLString }, + nameA: { type: GraphQLString }, + }), + isTypeOf: (_value, _context, _info) => + new Promise((_resolve, reject) => + setTimeout(() => reject(new Error('TypeA_isTypeOf_rejected')), 10), + ), +}); + +const TypeB = new GraphQLObjectType({ + name: 'TypeB', + interfaces: [SearchableInterface], + fields: () => ({ + id: { type: GraphQLString }, + nameB: { type: GraphQLString }, + }), + isTypeOf: (value: any, _context, _info) => value.id === 'b', +}); + +const queryTypeWithSearchable = new GraphQLObjectType({ + name: 'Query', + fields: { + person: { + type: PersonType, + resolve: () => john, + }, + search: { + type: SearchableInterface, + args: { id: { type: GraphQLString } }, + resolve: (_source, { id }) => { + if (id === 'a') { + return { id: 'a', nameA: 'Object A' }; + } else if (id === 'b') { + return { id: 'b', nameB: 'Object B' }; + } + }, + }, + }, +}); + +const schemaWithSearchable = new GraphQLSchema({ + query: queryTypeWithSearchable, + types: [ + PetType, + TypeA, + TypeB, + SearchableInterface, + PersonType, + DogType, + CatType, + ], +}); + describe('Execute: Union and intersection types', () => { it('can introspect on union and intersection types', () => { const document = parse(` @@ -633,4 +697,51 @@ describe('Execute: Union and intersection types', () => { }, }); }); + + it('handles promises from isTypeOf correctly when a later type matches synchronously', async () => { + const document = parse(` + query TestSearch { + search(id: "b") { + __typename + id + ... on TypeA { + nameA + } + ... on TypeB { + nameB + } + } + } + `); + + let unhandledRejection: any = null; + const unhandledRejectionListener = (reason: any) => { + unhandledRejection = reason; + }; + // eslint-disable-next-line + process.on('unhandledRejection', unhandledRejectionListener); + + const result = await execute({ + schema: schemaWithSearchable, + document, + }); + + expect(result.errors).to.equal(undefined); + expect(result.data).to.deep.equal({ + search: { + __typename: 'TypeB', + id: 'b', + nameB: 'Object B', + }, + }); + + // Give the TypeA promise a chance to reject and the listener to fire + + await new Promise((resolve) => setTimeout(resolve, 20)); + + // eslint-disable-next-line + process.removeListener('unhandledRejection', unhandledRejectionListener); + + expect(unhandledRejection).to.equal(null); + }); }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f5215585cf..70edfeb8a8 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -199,6 +199,11 @@ export interface ExecutionArgs { enableEarlyExecution?: Maybe; hideSuggestions?: Maybe; abortSignal?: Maybe; + /** Additional execution options. */ + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + }; } export interface StreamUsage { @@ -472,6 +477,7 @@ export function validateExecutionArgs( perEventExecutor, enableEarlyExecution, abortSignal, + options, } = args; if (abortSignal?.aborted) { @@ -534,7 +540,7 @@ export function validateExecutionArgs( variableDefinitions, rawVariableValues ?? {}, { - maxErrors: 50, + maxErrors: options?.maxCoercionErrors ?? 50, hideSuggestions, }, ); @@ -2042,12 +2048,14 @@ export const defaultTypeResolver: GraphQLTypeResolver = if (isPromise(isTypeOfResult)) { promisedIsTypeOfResults[i] = isTypeOfResult; } else if (isTypeOfResult) { - if (promisedIsTypeOfResults.length > 0) { - Promise.all(promisedIsTypeOfResults).then(undefined, () => { - /* ignore errors */ - }); + if (promisedIsTypeOfResults.length) { + // Explicitly ignore any promise rejections + Promise.allSettled(promisedIsTypeOfResults) + /* c8 ignore next 3 */ + .catch(() => { + // Do nothing + }); } - return type.name; } } diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 7fbb703909..40324849cd 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -220,10 +220,7 @@ export function visit( } } } else { - node = Object.defineProperties( - {}, - Object.getOwnPropertyDescriptors(node), - ); + node = { ...node }; for (const [editKey, editValue] of edits) { node[editKey] = editValue; } diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 963474ed7e..bda8225b57 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -122,12 +122,13 @@ describe('coerceInputValue', () => { }); describe('for GraphQLInputObject', () => { - const TestInputObject = new GraphQLInputObjectType({ + const TestInputObject: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'TestInputObject', - fields: { + fields: () => ({ foo: { type: new GraphQLNonNull(GraphQLInt) }, bar: { type: GraphQLInt }, - }, + nestedObject: { type: TestInputObject }, + }), }); it('returns no error for a valid input', () => { @@ -153,6 +154,18 @@ describe('coerceInputValue', () => { it('invalid for an unknown field', () => { test({ foo: 123, unknownField: 123 }, TestInputObject, undefined); }); + + it('invalid when supplied with an array', () => { + test([{ foo: 123 }, { bar: 456 }], TestInputObject, undefined); + }); + + it('invalid when a nested input object is supplied with an array', () => { + test( + { foo: 123, nested: [{ foo: 123 }, { bar: 456 }] }, + TestInputObject, + undefined, + ); + }); }); describe('for GraphQLInputObject that isOneOf', () => { diff --git a/src/utilities/__tests__/validateInputValue-test.ts b/src/utilities/__tests__/validateInputValue-test.ts index 7b1667c912..120a317554 100644 --- a/src/utilities/__tests__/validateInputValue-test.ts +++ b/src/utilities/__tests__/validateInputValue-test.ts @@ -228,12 +228,13 @@ describe('validateInputValue', () => { }); describe('for GraphQLInputObject', () => { - const TestInputObject = new GraphQLInputObjectType({ + const TestInputObject: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'TestInputObject', - fields: { + fields: () => ({ foo: { type: new GraphQLNonNull(GraphQLInt) }, bar: { type: GraphQLInt }, - }, + nested: { type: TestInputObject }, + }), }); it('returns no error for a valid input', () => { @@ -292,6 +293,30 @@ describe('validateInputValue', () => { ]); }); + it('returns error when supplied with an array', () => { + test([{ foo: 123 }, { bar: 456 }], TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" to be an object, found: [{ foo: 123 }, { bar: 456 }].', + path: [], + }, + ]); + }); + + it('returns error when a nested input object is supplied with an array', () => { + test( + { foo: 123, nested: [{ foo: 123 }, { bar: 456 }] }, + TestInputObject, + [ + { + error: + 'Expected value of type "TestInputObject" to be an object, found: [{ foo: 123 }, { bar: 456 }].', + path: ['nested'], + }, + ], + ); + }); + it('returns error for a misspelled field', () => { test({ foo: 123, bart: 123 }, TestInputObject, [ { diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 585acdaa29..87895c2262 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -65,7 +65,7 @@ export function coerceInputValue( } if (isInputObjectType(type)) { - if (!isObjectLike(inputValue)) { + if (!isObjectLike(inputValue) || Array.isArray(inputValue)) { return; // Invalid: intentionally return no value. } diff --git a/src/utilities/validateInputValue.ts b/src/utilities/validateInputValue.ts index a8d9b5c257..3f14f2df25 100644 --- a/src/utilities/validateInputValue.ts +++ b/src/utilities/validateInputValue.ts @@ -107,7 +107,7 @@ function validateInputValueImpl( } } } else if (isInputObjectType(type)) { - if (!isObjectLike(inputValue)) { + if (!isObjectLike(inputValue) || Array.isArray(inputValue)) { reportInvalidValue( onError, `Expected value of type "${type}" to be an object, found: ${inspect( diff --git a/website/next.config.js b/website/next.config.js index 3da28a4473..0c4793d198 100644 --- a/website/next.config.js +++ b/website/next.config.js @@ -1,5 +1,9 @@ /* eslint-disable camelcase */ import path from 'node:path'; +import fs from 'node:fs'; + +const fileContents = fs.readFileSync('./vercel.json', 'utf-8'); +const vercel = JSON.parse(fileContents); import nextra from 'nextra'; @@ -29,6 +33,7 @@ export default withNextra({ }); return config; }, + redirects: async () => vercel.redirects, output: 'export', images: { loader: 'custom', diff --git a/website/pages/_meta.ts b/website/pages/_meta.ts index b56f19ce54..7bf4b6e9cd 100644 --- a/website/pages/_meta.ts +++ b/website/pages/_meta.ts @@ -1,29 +1,18 @@ const meta = { - index: '', - '-- 1': { - type: 'separator', - title: 'GraphQL.JS Tutorial', + docs: { + type: 'page', + title: 'Documentation', }, - 'getting-started': '', - 'running-an-express-graphql-server': '', - 'graphql-clients': '', - 'basic-types': '', - 'passing-arguments': '', - 'object-types': '', - 'mutations-and-input-types': '', - 'authentication-and-express-middleware': '', - '-- 2': { - type: 'separator', - title: 'Advanced Guides', - }, - 'constructing-types': '', - 'oneof-input-objects': 'OneOf input objects', - 'defer-stream': '', - '-- 3': { - type: 'separator', - title: 'FAQ', + 'upgrade-guides': { + type: 'menu', + title: 'Upgrade Guides', + items: { + 'v16-v17': { + title: 'v16 to v17', + href: '/upgrade-guides/v16-v17', + }, + }, }, - 'going-to-production': '', 'api-v16': { type: 'menu', title: 'API', diff --git a/website/pages/api-v16/error.mdx b/website/pages/api-v16/error.mdx index 1338d321de..50cb70e4ea 100644 --- a/website/pages/api-v16/error.mdx +++ b/website/pages/api-v16/error.mdx @@ -10,8 +10,7 @@ The `graphql/error` module is responsible for creating and formatting GraphQL errors. You can import either from the `graphql/error` module, or from the root `graphql` module. For example: ```js -import { GraphQLError } from 'graphql'; // ES6 -const { GraphQLError } = require('graphql'); // CommonJS +import { GraphQLError } from 'graphql'; ``` ## Overview @@ -56,7 +55,7 @@ class GraphQLError extends Error { source?: Source, positions?: number[], originalError?: Error, - extensions?: { [key: string]: mixed }, + extensions?: Record, ); } ``` diff --git a/website/pages/api-v16/execution.mdx b/website/pages/api-v16/execution.mdx index 2810ed183a..c160797aa0 100644 --- a/website/pages/api-v16/execution.mdx +++ b/website/pages/api-v16/execution.mdx @@ -10,8 +10,7 @@ The `graphql/execution` module is responsible for the execution phase of fulfilling a GraphQL request. You can import either from the `graphql/execution` module, or from the root `graphql` module. For example: ```js -import { execute } from 'graphql'; // ES6 -const { execute } = require('graphql'); // CommonJS +import { execute } from 'graphql'; ``` ## Overview @@ -29,14 +28,28 @@ const { execute } = require('graphql'); // CommonJS ### execute ```ts -export function execute( - schema: GraphQLSchema, - documentAST: Document, - rootValue?: mixed, - contextValue?: mixed, - variableValues?: { [key: string]: mixed }, - operationName?: string, -): MaybePromise; +export function execute({ + schema, + document + rootValue, + contextValue, + variableValues, + operationName, + options, +}: ExecutionParams): MaybePromise; + +type ExecutionParams = { + schema: GraphQLSchema; + document: Document; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Record; + operationName?: string; + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + } +}; type MaybePromise = Promise | T; @@ -50,6 +63,20 @@ interface ExecutionResult< } ``` +We have another approach with positional arguments, this is however deprecated and set +to be removed in v17. + +```ts +export function execute( + schema: GraphQLSchema, + documentAST: Document, + rootValue?: unknown, + contextValue?: unknown, + variableValues?: Record, + operationName?: string, +): MaybePromise; +``` + Implements the "Evaluating requests" section of the GraphQL specification. Returns a Promise that will eventually be resolved and never rejected. @@ -63,22 +90,62 @@ non-empty array if an error occurred. ### executeSync +This is a short-hand method that will call `execute` and when the response can +be returned synchronously it will be returned, when a `Promise` is returned this +method will throw an error. + +```ts +export function executeSync({ + schema, + document, + rootValue, + contextValue, + variableValues, + operationName, + options, +}: ExecutionParams): MaybePromise; + +type ExecutionParams = { + schema: GraphQLSchema; + document: Document; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Record; + operationName?: string; + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + } +}; + +type MaybePromise = Promise | T; + +interface ExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} +``` + +We have another approach with positional arguments, this is however deprecated and set +to be removed in v17. + ```ts export function executeSync( schema: GraphQLSchema, documentAST: Document, - rootValue?: mixed, - contextValue?: mixed, - variableValues?: { [key: string]: mixed }, + rootValue?: unknown, + contextValue?: unknown, + variableValues?: Record, operationName?: string, ): ExecutionResult; - -type ExecutionResult = { - data: Object; - errors?: GraphQLError[]; -}; ``` -This is a short-hand method that will call `execute` and when the response can -be returned synchronously it will be returned, when a `Promise` is returned this -method will throw an error. +#### Execution options + +##### maxCoercionErrors + +Set the maximum number of errors allowed for coercing variables, this implements a default limit of 50 errors. diff --git a/website/pages/api-v16/graphql-http.mdx b/website/pages/api-v16/graphql-http.mdx index 73c36fd310..9b8285cd6c 100644 --- a/website/pages/api-v16/graphql-http.mdx +++ b/website/pages/api-v16/graphql-http.mdx @@ -11,8 +11,7 @@ The [official `graphql-http` package](https://github.com/graphql/graphql-http) p ## Express ```js -import { createHandler } from 'graphql-http/lib/use/express'; // ES6 -const { createHandler } = require('graphql-http/lib/use/express'); // CommonJS +import { createHandler } from 'graphql-http/lib/use/express'; ``` ### createHandler diff --git a/website/pages/api-v16/graphql.mdx b/website/pages/api-v16/graphql.mdx index e6936f279c..2c736c87ff 100644 --- a/website/pages/api-v16/graphql.mdx +++ b/website/pages/api-v16/graphql.mdx @@ -10,8 +10,7 @@ The `graphql` module exports a core subset of GraphQL functionality for creation of GraphQL type systems and servers. ```js -import { graphql } from 'graphql'; // ES6 -const { graphql } = require('graphql'); // CommonJS +import { graphql } from 'graphql'; ``` ## Overview diff --git a/website/pages/api-v16/language.mdx b/website/pages/api-v16/language.mdx index 897bb00927..cd96ce4101 100644 --- a/website/pages/api-v16/language.mdx +++ b/website/pages/api-v16/language.mdx @@ -9,8 +9,7 @@ title: graphql/language The `graphql/language` module is responsible for parsing and operating on the GraphQL language. You can import either from the `graphql/language` module, or from the root `graphql` module. For example: ```js -import { Source } from 'graphql'; // ES6 -const { Source } = require('graphql'); // CommonJS +import { Source } from 'graphql'; ``` ## Overview diff --git a/website/pages/api-v16/type.mdx b/website/pages/api-v16/type.mdx index 4ab3d7d1a2..c829d9708d 100644 --- a/website/pages/api-v16/type.mdx +++ b/website/pages/api-v16/type.mdx @@ -9,8 +9,7 @@ title: graphql/type The `graphql/type` module is responsible for defining GraphQL types and schema. You can import either from the `graphql/type` module, or from the root `graphql` module. For example: ```js -import { GraphQLSchema } from 'graphql'; // ES6 -const { GraphQLSchema } = require('graphql'); // CommonJS +import { GraphQLSchema } from 'graphql'; ``` ## Overview diff --git a/website/pages/api-v16/utilities.mdx b/website/pages/api-v16/utilities.mdx index ba8533c220..1e646d8be7 100644 --- a/website/pages/api-v16/utilities.mdx +++ b/website/pages/api-v16/utilities.mdx @@ -10,8 +10,7 @@ The `graphql/utilities` module contains common useful computations to use with the GraphQL language and type objects. You can import either from the `graphql/utilities` module, or from the root `graphql` module. For example: ```js -import { introspectionQuery } from 'graphql'; // ES6 -const { introspectionQuery } = require('graphql'); // CommonJS +import { introspectionQuery } from 'graphql'; ``` ## Overview diff --git a/website/pages/api-v16/validation.mdx b/website/pages/api-v16/validation.mdx index 6b45caec6a..1acc121da6 100644 --- a/website/pages/api-v16/validation.mdx +++ b/website/pages/api-v16/validation.mdx @@ -10,8 +10,7 @@ The `graphql/validation` module fulfills the Validation phase of fulfilling a GraphQL result. You can import either from the `graphql/validation` module, or from the root `graphql` module. For example: ```js -import { validate } from 'graphql/validation'; // ES6 -const { validate } = require('graphql/validation'); // CommonJS +import { validate } from 'graphql/validation'; ``` ## Overview diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts new file mode 100644 index 0000000000..094e286afd --- /dev/null +++ b/website/pages/docs/_meta.ts @@ -0,0 +1,47 @@ +const meta = { + index: '', + '-- 1': { + type: 'separator', + title: 'GraphQL.JS Tutorial', + }, + 'getting-started': '', + 'running-an-express-graphql-server': '', + 'graphql-clients': '', + 'basic-types': '', + 'passing-arguments': '', + 'object-types': '', + 'mutations-and-input-types': '', + 'authentication-and-express-middleware': '', + 'authorization-strategies': '', + '-- 2': { + type: 'separator', + title: 'Advanced Guides', + }, + 'constructing-types': '', + nullability: '', + 'abstract-types': '', + 'oneof-input-objects': '', + 'defer-stream': '', + subscriptions: '', + 'type-generation': '', + 'cursor-based-pagination': '', + 'custom-scalars': '', + 'advanced-custom-scalars': '', + 'n1-dataloader': '', + 'caching-strategies': '', + 'resolver-anatomy': '', + 'graphql-errors': '', + 'using-directives': '', + '-- 3': { + type: 'separator', + title: 'Testing', + }, + '-- 4': { + type: 'separator', + title: 'FAQ', + }, + 'going-to-production': '', + 'scaling-graphql': '', +}; + +export default meta; diff --git a/website/pages/docs/abstract-types.mdx b/website/pages/docs/abstract-types.mdx new file mode 100644 index 0000000000..607da1d309 --- /dev/null +++ b/website/pages/docs/abstract-types.mdx @@ -0,0 +1,204 @@ +--- +title: Abstract types in GraphQL.js +--- + +GraphQL includes two kinds of abstract types: interfaces and unions. These types let a single +field return values of different object types, while keeping your schema type-safe. + +This guide covers how to define and resolve abstract types using GraphQL.js. It focuses on +constructing types in JavaScript using the GraphQL.js type system, not the schema definition +language (SDL). + +## What are abstract types? + +Most GraphQL types are concrete. They represent a specific kind of object, for example, a +`Book` or an `Author`. Abstract types let a field return different types of objects depending +on the data. + +This is useful when the return type can vary but comes from a known set. For example, a `search` +field might return a book, an author, or a publisher. Abstract types let you model this kind of +flexibility while preserving validation, introspection, and tool support. + +GraphQL provides two kinds of abstract types: + +- Interfaces define a set of fields that multiple object types must implement. + - Use case: A `ContentItem` interface with fields like `id`, `title`, and `publishedAt`, + implemented by types such as `Article` and `PodcastEpisode`. +- Unions group together unrelated types that don't share any fields. + - Use case: A `SearchResult` union that includes `Book`, `Author`, and `Publisher` types. + +## Defining interfaces + +To define an interface in GraphQL.js, use the `GraphQLInterfaceType` constructor. An interface +must include a `name`, a `fields` function, and a `resolveType` function, which tells GraphQL which +concrete type a given value corresponds to. + +The following example defines a `ContentItem` interface for a publishing platform: + +```js +import { GraphQLInterfaceType, GraphQLString, GraphQLNonNull } from 'graphql'; + +const ContentItemInterface = new GraphQLInterfaceType({ + name: 'ContentItem', + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + title: { type: GraphQLString }, + publishedAt: { type: GraphQLString }, + }, + resolveType(value) { + if (value.audioUrl) { + return 'PodcastEpisode'; + } + if (value.bodyText) { + return 'Article'; + } + return null; + }, +}); +``` + +You can return either the type name as a string or the corresponding `GraphQLObjectType` instance. +Returning the instance is recommended when possible for better type safety and tooling support. + +## Implementing interfaces with object types + +To implement an interface, define a `GraphQLObjectType` and include the interface in its +`interfaces` array. The object type must implement all fields defined by the interface. + +The following example implements the `Article` and `PodcastEpisode` types that +conform to the `ContentItem` interface: + +```js +import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; + +const ArticleType = new GraphQLObjectType({ + name: 'Article', + interfaces: [ContentItemInterface], + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + title: { type: GraphQLString }, + publishedAt: { type: GraphQLString }, + bodyText: { type: GraphQLString }, + }, + isTypeOf: (value) => value.bodyText !== undefined, +}); + +const PodcastEpisodeType = new GraphQLObjectType({ + name: 'PodcastEpisode', + interfaces: [ContentItemInterface], + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + title: { type: GraphQLString }, + publishedAt: { type: GraphQLString }, + audioUrl: { type: GraphQLString }, + }, + isTypeOf: (value) => value.audioUrl !== undefined, +}); +``` + +The `isTypeOf` function is optional. It provides a fallback when `resolveType` isn't defined, or +when runtime values could match multiple types. If both `resolveType` and `isTypeOf` are defined, +GraphQL uses `resolveType`. + +## Defining union types + +Use the `GraphQLUnionType` constructor to define a union. A union allows a field to return one +of several object types that don't need to share fields. + +A union requires: + +- A `name` +- A list of object types (`types`) +- A `resolveType` function + +The following example defines a `SearchResult` union: + +```js +import { GraphQLUnionType } from 'graphql'; + +const SearchResultType = new GraphQLUnionType({ + name: 'SearchResult', + types: [BookType, AuthorType, PublisherType], + resolveType(value) { + if (value.isbn) { + return 'Book'; + } + if (value.bio) { + return 'Author'; + } + if (value.catalogSize) { + return 'Publisher'; + } + return null; + }, +}); +``` + +Unlike interfaces, unions don't declare any fields of their own. Clients use inline fragments +to query fields from the concrete types. + +## Resolving abstract types at runtime + +GraphQL resolves abstract types dynamically during execution using the `resolveType` function. + +This function receives the following arguments: + +```js +resolveType(value, context, info) +``` + +It can return: + +- A `GraphQLObjectType` instance (recommended) +- The name of a type as a string +- A `Promise` resolving to either of the above + +If `resolveType` isn't defined, GraphQL falls back to checking each possible type's `isTypeOf` +function. This fallback is less efficient and makes type resolution harder to debug. For most cases, +explicitly defining `resolveType` is recommended. + +## Querying abstract types + +To query a field that returns an abstract type, use inline fragments to select fields from the +possible concrete types. GraphQL evaluates each fragment based on the runtime type of the result. + +For example: + +```graphql +{ + search(term: "deep learning") { + ... on Book { + title + isbn + } + ... on Author { + name + bio + } + ... on Publisher { + name + catalogSize + } + } +} +``` + +GraphQL's introspection system lists all possible types for each interface and union, which +enables code generation and editor tooling to provide type-aware completions. + +## Best practices + +- Always implement `resolveType` for interfaces and unions to handle runtime type resolution. +- Return the `GraphQLObjectType` instance when possible for better clarity and static analysis. +- Keep `resolveType` logic simple, using consistent field shapes or tags to distinguish +types. +- Test `resolveType` logic carefully. Errors in `resolveType` can cause runtime errors that can +be hard to trace. +- Use interfaces when types share fields and unions when types are structurally unrelated. + +## Additional resources + +- [Constructing Types](https://www.graphql-js.org/docs/constructing-types/) +- GraphQL Specification: + - [Interfaces](https://spec.graphql.org/October2021/#sec-Interfaces) + - [Unions](https://spec.graphql.org/October2021/#sec-Unions) \ No newline at end of file diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx new file mode 100644 index 0000000000..b71aa450fc --- /dev/null +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -0,0 +1,223 @@ +--- +title: Best Practices for Custom Scalars +--- + +# Custom Scalars: Best Practices and Testing + +Custom scalars must behave predictably and clearly. To maintain a consistent, reliable +schema, follow these best practices. + +### Document expected formats and validation + +Provide a clear description of the scalar's accepted input and output formats. For example, a +`DateTime` scalar should explain that it expects [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) strings ending with `Z`. + +Clear descriptions help clients understand valid input and reduce mistakes. + +### Validate consistently across `parseValue` and `parseLiteral` + +Clients can send values either through variables or inline literals. +Your `parseValue` and `parseLiteral` functions should apply the same validation logic in +both cases. + +Use a shared helper to avoid duplication: + +```js +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} +``` + +Both `parseValue` and `parseLiteral` should call this function. + +### Return clear errors + +When validation fails, throw descriptive errors. Avoid generic messages like "Invalid input." +Instead, use targeted messages that explain the problem, such as: + +```text +DateTime cannot represent an invalid date: `abc123` +``` + +Clear error messages speed up debugging and make mistakes easier to fix. + +### Serialize consistently + +Always serialize internal values into a predictable format. +For example, a `DateTime` scalar should always produce an ISO string, even if its +internal value is a `Date` object. + +```js +serialize(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); +} +``` + +Serialization consistency prevents surprises on the client side. + +## Testing custom scalars + +Testing ensures your custom scalars work reliably with both valid and invalid inputs. +Tests should cover three areas: coercion functions, schema integration, and error handling. + +### Unit test serialization and parsing + +Write unit tests for each function: `serialize`, `parseValue`, and `parseLiteral`. +Test with both valid and invalid inputs. + +```js +describe('DateTime scalar', () => { + it('serializes Date instances to ISO strings', () => { + const date = new Date('2024-01-01T00:00:00Z'); + expect(DateTime.serialize(date)).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('throws if serializing a non-Date value', () => { + expect(() => DateTime.serialize('not a date')).toThrow(TypeError); + }); + + it('parses ISO strings into Date instances', () => { + const result = DateTime.parseValue('2024-01-01T00:00:00Z'); + expect(result).toBeInstanceOf(Date); + expect(result.toISOString()).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('throws if parsing an invalid date string', () => { + expect(() => DateTime.parseValue('invalid-date')).toThrow(TypeError); + }); +}); +``` + +### Test custom scalars in a schema + +Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior. + +```js +import { graphql, GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { DateTimeResolver as DateTime } from 'graphql-scalars'; + +const Query = new GraphQLObjectType({ + name: 'Query', + fields: { + now: { + type: DateTime, + resolve() { + return new Date(); + }, + }, + }, +}); + +/* + scalar DateTime + + type Query { + now: DateTime + } +*/ +const schema = new GraphQLSchema({ + query: Query, +}); + +async function testQuery() { + const response = await graphql({ + schema, + source: '{ now }', + }); + console.log(response); +} + +testQuery(); +``` + +Schema-level tests verify that the scalar behaves correctly during execution, not just +in isolation. + +## Common use cases for custom scalars + +Custom scalars solve real-world needs by handling types that built-in scalars don't cover. + +- `DateTime`: Serializes and parses ISO-8601 date-time strings. +- `Email`: Validates syntactically correct email addresses. + +```js +function validateEmail(value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + throw new TypeError(`Email cannot represent invalid email address: ${value}`); + } + return value; +} +``` + +- `URL`: Ensures well-formatted, absolute URLs. + +```js +function validateURL(value) { + try { + new URL(value); + return value; + } catch { + throw new TypeError(`URL cannot represent an invalid URL: ${value}`); + } +} +``` + +- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses +GraphQL's strict type checking. + +## When to use existing libraries + +Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if +not handled carefully. + +Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready +scalars for DateTime, EmailAddress, URL, UUID, and many others. + +### Example: Handling email validation + +Handling email validation correctly requires dealing with Unicode, quoted local parts, and +domain validation. Rather than writing your own regex, it's better to use a library scalar +that's already validated against standards. + +If you need domain-specific behavior, you can wrap an existing scalar with custom rules: + +```js +import { EmailAddressResolver } from 'graphql-scalars'; + +const StrictEmailAddress = new GraphQLScalarType({ + ...EmailAddressResolver, + name: 'StrictEmailAddress', + parseValue(value) { + 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 email; + }, +}); +``` + +By following these best practices and using trusted tools where needed, you can build custom +scalars that are reliable, maintainable, and easy for clients to work with. + +## Additional resources + +- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready +library of common custom scalars. +- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This +specification is no longer actively maintained, but useful for historical context. diff --git a/website/pages/authentication-and-express-middleware.mdx b/website/pages/docs/authentication-and-express-middleware.mdx similarity index 80% rename from website/pages/authentication-and-express-middleware.mdx rename to website/pages/docs/authentication-and-express-middleware.mdx index c03f444496..44c89f75a7 100644 --- a/website/pages/authentication-and-express-middleware.mdx +++ b/website/pages/docs/authentication-and-express-middleware.mdx @@ -1,6 +1,6 @@ --- -title: Authentication and Express Middleware -sidebarTitle: Authentication & Middleware +title: Using Express Middleware with GraphQL.js +sidebarTitle: Using Express Middleware --- import { Tabs } from 'nextra/components'; @@ -14,9 +14,9 @@ For example, let's say we wanted our server to log the IP address of every reque ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; const schema = buildSchema(`type Query { ip: String }`); @@ -46,22 +46,29 @@ app.all( app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString, -} = require('graphql'); +} from 'graphql'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', - fields: { ip: { type: GraphQLString } }, + fields: { + ip: { + type: GraphQLString, + resolve: (_, args, context) => { + return context.ip; + } + } + }, }), }); @@ -70,19 +77,12 @@ function loggingMiddleware(req, res, next) { next(); } -const root = { - ip(args, context) { - return context.ip; - }, -}; - const app = express(); app.use(loggingMiddleware); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, context: (req) => ({ ip: req.raw.ip, }), @@ -90,8 +90,8 @@ app.all( ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` @@ -100,3 +100,5 @@ In a REST API, authentication is often handled with a header, that contains an a If you aren't familiar with any of these authentication mechanisms, we recommend using `express-jwt` because it's simple without sacrificing any future flexibility. If you've read through the docs linearly to get to this point, congratulations! You now know everything you need to build a practical GraphQL API server. + +Want to control access to specific operations or fields? See [Authorization Strategies](\pages\docs\authorization-strategies.mdx). \ No newline at end of file diff --git a/website/pages/docs/authorization-strategies.mdx b/website/pages/docs/authorization-strategies.mdx new file mode 100644 index 0000000000..c467ce6640 --- /dev/null +++ b/website/pages/docs/authorization-strategies.mdx @@ -0,0 +1,178 @@ +--- +title: Authorization Strategies +--- + +GraphQL gives you complete control over how to define and enforce access control. +That flexibility means it's up to you to decide where authorization rules live and +how they're enforced. + +This guide covers common strategies for implementing authorization in GraphQL +servers using GraphQL.js. It assumes you're authenticating requests and passing a user or +session object into the `context`. + +## What is authorization? + +Authorization determines what a user is allowed to do. It's different from +authentication, which verifies who a user is. + +In GraphQL, authorization typically involves restricting: + +- Access to certain queries or mutations +- Visibility of specific fields +- Ability to perform mutations based on roles or ownership + +## Resolver-based authorization + +> **Note:** +> All examples assume you're using Node.js 20 or later with [ES module (ESM) support](https://nodejs.org/api/esm.html) enabled. + +The simplest approach is to enforce access rules directly inside resolvers +using the `context.user` value: + +```js +export const resolvers = { + Query: { + secretData: (parent, args, context) => { + if (!context.user || context.user.role !== 'admin') { + throw new Error('Not authorized'); + } + return getSecretData(); + }, + }, +}; +``` + +This works well for smaller schemas or one-off checks. + +## Centralizing access control logic + +As your schema grows, repeating logic like `context.user.role !=='admin'` +becomes error-prone. Instead, extract shared logic into utility functions: + +```js +export function requireUser(user) { + if (!user) { + throw new Error('Not authenticated'); + } +} + +export function requireRole(user, role) { + requireUser(user); + if (user.role !== role) { + throw new Error(`Must be a ${role}`); + } +} +``` + +You can use these helpers in resolvers: + +```js +import { requireRole } from './auth.js'; + +export const resolvers = { + Mutation: { + deleteUser: (parent, args, context) => { + requireRole(context.user, 'admin'); + return deleteUser(args.id); + }, + }, +}; +``` + +This pattern makes your access rules easier to read, test, and update. + +## Field-level access control + +You can also conditionally return or hide data at the field level. This +is useful when, for example, users should only see their own private data: + +```js +export const resolvers = { + User: { + email: (parent, args, context) => { + if (context.user.id !== parent.id && context.user.role !== 'admin') { + return null; + } + return parent.email; + }, + }, +}; +``` + +Returning `null` is a common pattern when fields should be hidden from +unauthorized users without triggering an error. + +## Declarative authorization with directives + +If you prefer a schema-first or declarative style, you can define custom +schema directives like `@auth(role: "admin")` directly in your SDL: + +```graphql +type Query { + users: [User] @auth(role: "admin") +} +``` + +To enforce this directive during execution, you need to inspect it in your resolvers +using `getDirectiveValues`: + +```js +import { getDirectiveValues } from 'graphql'; + +function withAuthCheck(resolverFn, schema, fieldNode, variableValues, context) { + const directive = getDirectiveValues( + schema.getDirective('auth'), + fieldNode, + variableValues + ); + + if (directive?.role && context.user?.role !== directive.role) { + throw new Error('Unauthorized'); + } + + return resolverFn(); +} +``` + +You can wrap individual resolvers with this logic, or apply it more broadly using a +schema visitor or transformation. + +GraphQL.js doesn't interpret directives by default, they're just annotations. +You must implement their behavior manually, usually by: + +- Wrapping resolvers in custom logic +- Using a schema transformation library to inject authorization checks + +Directive-based authorization can add complexity, so many teams start with +resolver-based checks and adopt directives later if needed. + +## Best practices + +- Keep authorization logic close to business logic. Resolvers are often the +right place to keep authorization logic. +- Use shared helper functions to reduce duplication and improve clarity. +- Avoid tightly coupling authorization logic to your schema. Make it +reusable where possible. +- Consider using `null` to hide fields from unauthorized users, rather than +throwing errors. +- Be mindful of tools like introspection or GraphQL Playground that can +expose your schema. Use caution when deploying introspection in production +environments. + +## Additional resources + +- [Anatomy of a Resolver](./resolver-anatomy): Shows how resolvers work and how the `context` +object is passed in. Helpful if you're new to writing custom resolvers or +want to understand where authorization logic fits. +- [GraphQL Specification, Execution section](https://spec.graphql.org/October2021/#sec-Execution): Defines how fields are +resolved, including field-level error propagation and execution order. Useful +background when building advanced authorization patterns that rely on the +structure of GraphQL execution. +- [`graphql-shield`](https://github.com/dimatill/graphql-shield): A community library for adding rule-based +authorization as middleware to resolvers. +- [`graphql-auth-directives`](https://github.com/the-guild-org/graphql-auth-directives): Adds support for custom directives like +`@auth(role: "admin")`, letting you declare access control rules in SDL. +Helpful if you're building a schema-first API and prefer declarative access +control. + + diff --git a/website/pages/basic-types.mdx b/website/pages/docs/basic-types.mdx similarity index 73% rename from website/pages/basic-types.mdx rename to website/pages/docs/basic-types.mdx index b6480a979d..08fbe2a44e 100644 --- a/website/pages/basic-types.mdx +++ b/website/pages/docs/basic-types.mdx @@ -17,9 +17,9 @@ Each of these types maps straightforwardly to JavaScript, so you can just return ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; // Construct a schema, using GraphQL schema language const schema = buildSchema(` @@ -39,7 +39,7 @@ const root = { return Math.random(); }, rollThreeDice() { - return [1, 2, 3].map((\_) => 1 + Math.floor(Math.random() \* 6)); + return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); }, }; @@ -54,62 +54,56 @@ app.all( app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLFloat, GraphQLList, -} = require('graphql'); +} from 'graphql'; // Construct a schema const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - quoteOfTheDay: { type: GraphQLString }, - random: { type: GraphQLFloat }, - rollThreeDice: { type: new GraphQLList(GraphQLFloat) }, + quoteOfTheDay: { + type: GraphQLString, + resolve: () => Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within' + }, + random: { + type: GraphQLFloat, + resolve: () => Math.random() + }, + rollThreeDice: { + type: new GraphQLList(GraphQLFloat), + resolve: () => [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)) + }, }, }), }); -// The root provides a resolver function for each API endpoint -const root = { - quoteOfTheDay() { - return Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within'; - }, - random() { - return Math.random(); - }, - rollThreeDice() { - return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); - }, -}; - const app = express(); - app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` If you run this code with `node server.js` and browse to http://localhost:4000/graphql you can try out these APIs. -These examples show you how to call APIs that return different types. To send different types of data into an API, you will also need to learn about [passing arguments to a GraphQL API](/passing-arguments/). +These examples show you how to call APIs that return different types. To send different types of data into an API, you will also need to learn about [passing arguments to a GraphQL API](./passing-arguments). diff --git a/website/pages/docs/caching-strategies.mdx b/website/pages/docs/caching-strategies.mdx new file mode 100644 index 0000000000..112e0feacd --- /dev/null +++ b/website/pages/docs/caching-strategies.mdx @@ -0,0 +1,298 @@ +--- +title: Caching Strategies +--- + +# Caching Strategies + +Caching is a core strategy for improving the performance and scalability of GraphQL +servers. Because GraphQL allows clients to specify exactly what they need, the server often +does more work per request (but fewer requests) compared to many other APIs. + +This guide explores different levels of caching in a GraphQL.js so you can apply the right +strategy for your application. + +## Why caching matters + +GraphQL servers commonly face performance bottlenecks due to repeated fetching of the +same data, costly resolver logic, or expensive database queries. Since GraphQL shifts +much of the composition responsibility to the server, caching becomes essential for maintaining +fast response times and managing backend load. + +## Caching levels + +There are several opportunities to apply caching within a GraphQL server: + +- **Resolver-level caching**: Cache the result of specific fields. +- **Request-level caching**: Batch and cache repeated access to backend resources +within a single operation. +- **Operation result caching**: Reuse the entire response for repeated identical queries. +- **Schema caching**: Cache the compiled schema when startup cost is high. +- **Transport/middleware caching**: Leverage caching behavior in HTTP servers or proxies. + +Understanding where caching fits in your application flow helps you apply it strategically +without overcomplicating your system. + +## Resolver-level caching + +Resolver-level caching is useful when a specific field’s value is expensive to compute and +commonly requested with the same arguments. Instead of recomputing or refetching the data on +every request, you can store the result temporarily in memory and return it directly when the +same input appears again. + +### Use cases + +- Fields backed by slow or rate-limited APIs +- Fields that require complex computation +- Data that doesn't change frequently + +For example, consider a field that returns information about a product: + +```js +// utils/cache.js +import LRU from 'lru-cache'; + +export const productCache = new LRU({ max: 1000, ttl: 1000 * 60 }); // 1 min TTL +``` + +The next example shows how to use that cache inside a resolver to avoid repeated database +lookups: + +```js +// resolvers/product.js +import { productCache } from '../utils/cache.js'; + +export const resolvers = { + Query: { + product(_, { id }, context) { + const cached = productCache.get(id); + if (cached) return cached; + + const productPromise = context.db.products.findById(id); + productCache.set(id, productPromise); + return productPromise; + }, + }, +}; +``` + +This example uses [`lru-cache`](https://www.npmjs.com/package/lru-cache), which limits the +number of stored items and support TTL-based expiration. You can replace it with Redis or +another cache if you need cross-process consistency. + +### Guidelines + +- Resolver-level caches are global. Be careful with authorization-sensitive data. +- This technique works best for data that doesn't change often or can tolerate short-lived +staleness. +- TTL should match how often the underlying data is expected to change. + +## Request-level caching with DataLoader + +[DataLoader](https://github.com/graphql/dataloader) is a utility for batching and caching +backend access during a single GraphQL operation. It's designed to solve the N+1 problem, +where the same resource is fetched repeatedly in a single query across multiple fields. + +### Use cases + +- Resolving nested relationships +- Avoiding duplicate database or API calls +- Scoping caching to a single request, without persisting globally + +The following example defines a DataLoader instance that batches user lookups by ID: + +```js +// loaders/userLoader.js +import DataLoader from 'dataloader'; +import { batchGetUsers } from '../services/users.js'; + +export const createUserLoader = () => new DataLoader(ids => batchGetUsers(ids)); +``` + +You can then include the loader in the per-request context to isolate it from other +operations: + +```js +// context.js +import { createUserLoader } from './loaders/userLoader.js'; + +export function createContext() { + return { + userLoader: createUserLoader(), + }; +} +``` + +Finally, use the loader in your resolvers to batch-fetch users efficiently: + +```js +// resolvers/user.js +export const resolvers = { + Query: { + async users(_, __, context) { + return context.userLoader.loadMany([1, 2, 3]); + }, + }, +}; +``` + +### Guidelines + +- The cache is scoped to the request. Each request gets a fresh loader instance. +- This strategy works best for resolving repeated references to the same resource type. +- This isn't a long-lived cache. Combine it with other layers for broader coverage. + +To read more about DataLoader and the N+1 problem, +see [Solving the N+1 Problem with DataLoader](https://github.com/graphql/dataloader). + +## Operation result caching + +Operation result caching stores the complete response of a query, keyed by the query string, variables, and potentially +HTTP headers. It can dramatically improve performance when the same query is sent frequently, particularly for read-heavy +applications. + +### Use cases + +- Public data or anonymous content +- Expensive queries that return stable results +- Scenarios where the same query is sent frequently + +The following example defines two functions to interact with a Redis cache, +storing and retrieving cached results: + +```js +// cache/queryCache.js +import Redis from 'ioredis'; +const redis = new Redis(); + +export async function getCachedResponse(cacheKey) { + const cached = await redis.get(cacheKey); + return cached ? JSON.parse(cached) : null; +} + +export async function cacheResponse(cacheKey, result, ttl = 60) { + await redis.set(cacheKey, JSON.stringify(result), 'EX', ttl); +} +``` + +The next example shows how to wrap your execution logic to check the cache first and store results +afterward: + +```js +// graphql/executeWithCache.js +import { getCachedResponse, cacheResponse } from '../cache/queryCache.js'; + +/** + * Stores in-flight requests to executeWithCache such that concurrent + * requests with the same cacheKey will only result in one call to + * `getCachedResponse` / `cacheResponse`. Once a request completes + * (with or without error) it is removed from the map. + */ +const inflight = new Map(); + +export function executeWithCache({ cacheKey, executeFn }) { + const existing = inflight.get(cacheKey); + if (existing) return existing; + + const promise = _executeWithCacheUnbatched({ cacheKey, executeFn }); + inflight.set(cacheKey, promise); + return promise.finally(() => inflight.delete(cacheKey)); +} + +async function _executeWithCacheUnbatched({ cacheKey, executeFn }) { + const cached = await getCachedResponse(cacheKey); + if (cached) return cached; + + const result = await executeFn(); + await cacheResponse(cacheKey, result); + return result; +} +``` + +### Guidelines + +- Don't cache personalized or auth-sensitive data unless you scope the cache per user or token. +- Invalidation is nontrivial. Consider TTLs, cache versioning, or event-driven purging. + +## Schema caching + +Schema caching is useful when your schema construction is expensive, for example, when +you are dynamically generating types, you are stitching multiple schemas, or fetching +remote GraphQL services. This is especially important in serverless environments, +where cold starts can significantly impact performance. + +### Use cases + +- Serverless functions that rebuild the schema on each invocation +- Applications that use schema stitching or remote schema delegation +- Environments where schema generation takes noticeable time on startup + +The following example shows how to cache a schema in memory after the first build: + +```js +import { buildSchema } from 'graphql'; + +let cachedSchema; + +export function getSchema() { + if (!cachedSchema) { + cachedSchema = buildSchema(schemaSDLString); // or makeExecutableSchema() + } + return cachedSchema; +} +``` + +## Cache invalidation + +No caching strategy is complete without an invalidation plan. Cached data can +become stale or incorrect, and serving outdated information can lead to bugs or a +degraded user experience. + +The following are common invalidation techniques: + +- **TTL (time-to-live)**: Automatically expire cached items after a time window +- **Manual purging**: Remove or refresh cache entries when related data is updated +- **Key versioning**: Encode version or timestamp metadata into cache keys +- **Stale-while-revalidate**: Serve stale data while refreshing it in the background + +Design your invalidation strategy based on your data’s volatility and your clients’ +tolerance for staleness. + +## Third-party and edge caching + +While GraphQL.js does not include built-in support for third-party or edge caching, it integrates +well with external tools and middleware that handle full response caching or caching by query +signature. + +### Use cases + +- Serving public, cacheable content to unauthenticated users +- Deploying behind a CDN or reverse proxy +- Using a gateway service that supports persistent response caching + +The following tools and layers are commonly used: + +- Redis or Memcached for in-memory and cross-process caching +- CDN-level caching for static, cache-friendly GraphQL queries +- API gateways + +### Guidelines + +- Partition cache entries for personalized data using auth tokens or headers +- Monitor cache hit/miss ratios to identify tuning opportunities +- Consider varying cache strategy per query or operation type + +## Client-side caching + +GraphQL clients include sophisticated client-side caches that store +normalized query results and reuse them across views or components. While this is out of scope for GraphQL.js +itself, server-side caching should be designed with client behavior in mind. + +### When to consider it in server design + +- You want to avoid redundant server work for cold-started clients +- You need consistency between server and client freshness guarantees +- You're coordinating with clients that rely on local cache behavior + +Server-side and client-side caches should align on freshness guarantees and invalidation +behavior. If the client doesn't re-fetch automatically, server-side staleness +may be invisible but impactful. diff --git a/website/pages/constructing-types.mdx b/website/pages/docs/constructing-types.mdx similarity index 89% rename from website/pages/constructing-types.mdx rename to website/pages/docs/constructing-types.mdx index 2ae7b93872..40bd1bfa87 100644 --- a/website/pages/constructing-types.mdx +++ b/website/pages/docs/constructing-types.mdx @@ -13,9 +13,9 @@ For example, let's say we are building a simple API that lets you fetch user dat ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; const schema = buildSchema(` type User { @@ -61,9 +61,9 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const graphql = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import * as graphql from 'graphql'; // Maps id to User object const fakeDatabase = { @@ -86,7 +86,7 @@ const userType = new graphql.GraphQLObjectType({ }, }); -// Define the Query type +// Define the Query type with inline resolver const queryType = new graphql.GraphQLObjectType({ name: 'Query', fields: { diff --git a/website/pages/docs/cursor-based-pagination.mdx b/website/pages/docs/cursor-based-pagination.mdx new file mode 100644 index 0000000000..e14cc9796d --- /dev/null +++ b/website/pages/docs/cursor-based-pagination.mdx @@ -0,0 +1,324 @@ +--- +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. +This pattern works well with dynamic datasets, where users frequently add or +remove items between requests. + +GraphQL.js doesn't include cursor pagination out of the box, but you can implement +it using custom types and resolvers. This guide shows how to build a paginated field +using the connection pattern popularized by [Relay](https://relay.dev). By the end of this +guide, you will be able to define cursors and return results in a consistent structure +that works well with clients. + +## The connection pattern + +Cursor-based pagination typically uses a structured format that separates +pagination metadata from the actual data. The most widely adopted pattern follows the +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While +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, 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: + +```graphql +query { + users(first: 2) { + edges { + node { + id + name + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +```json +{ + "data": { + "users": { + "edges": [ + { + "node": { + "id": "1", + "name": "Ada Lovelace" + }, + "cursor": "cursor-1" + }, + { + "node": { + "id": "2", + "name": "Alan Turing" + }, + "cursor": "cursor-2" + } + ], + "pageInfo": { + "hasNextPage": true, + "endCursor": "cursor-2" + } + } + } +} +``` + +This structure gives clients everything they need to paginate. It provides the actual data (`node`), +the cursor to continue from (`endCursor`), and a flag (`hasNextPage`) that indicates whether +more data is available. + +## Defining connection types in GraphQL.js + +To support this structure in your schema, define a few custom types: + +```js +const PageInfoType = new GraphQLObjectType({ + name: 'PageInfo', + fields: { + hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) }, + hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) }, + startCursor: { type: GraphQLString }, + endCursor: { type: GraphQLString }, + }, +}); +``` + +The `PageInfo` type provides metadata about the current page of results. +The `hasNextPage` and `hasPreviousPage` fields indicate whether more +results are available in either direction. The `startCursor` and `endCursor` +fields help clients resume pagination from a specific point. + +Next, define an edge type to represent individual items in the connection: + +```js +const UserEdgeType = new GraphQLObjectType({ + name: 'UserEdge', + fields: { + node: { type: UserType }, + cursor: { type: new GraphQLNonNull(GraphQLString) }, + }, +}); +``` + +Each edge includes a `node` and a `cursor`, which marks its position in +the list. + +Then, define the connection type itself: + +```js +const UserConnectionType = new GraphQLObjectType({ + name: 'UserConnection', + fields: { + edges: { + type: new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(UserEdgeType)) + ), + }, + pageInfo: { type: new GraphQLNonNull(PageInfoType) }, + }, +}); +``` + +The connection type wraps a list of edges and includes the pagination +metadata. + +Paginated fields typically accept the following arguments: + +```js +const connectionArgs = { + first: { type: GraphQLInt }, + after: { type: GraphQLString }, + last: { type: GraphQLInt }, + before: { type: GraphQLString }, +}; +``` + +Use `first` and `after` for forward pagination. The `last` and `before` +arguments enable backward pagination if needed. + +## Writing a paginated resolver + +Once you've defined your connection types and pagination arguments, you can write a resolver +that slices your data and returns a connection object. The key steps are: + +1. Decode the incoming cursor. +2. Slice the data based on the decoded index. +3. Generate cursors for each returned item. +4. Build the `edges` and `pageInfo` objects. + +The exact logic will vary depending on how your data is stored. The following example uses an +in-memory list of users: + +```js +// Sample data +const users = [ + { id: '1', name: 'Ada Lovelace' }, + { id: '2', name: 'Alan Turing' }, + { id: '3', name: 'Grace Hopper' }, + { id: '4', name: 'Katherine Johnson' }, +]; + +// Encode/decode cursors +function encodeCursor(index) { + return Buffer.from(`cursor:${index}`).toString('base64'); +} + +function decodeCursor(cursor) { + const decoded = Buffer.from(cursor, 'base64').toString('ascii'); + const match = decoded.match(/^cursor:(\d+)$/); + return match ? parseInt(match[1], 10) : null; +} + +// Resolver for paginated users +const usersField = { + type: UserConnectionType, + args: connectionArgs, + resolve: (_, args) => { + let start = 0; + if (args.after) { + const index = decodeCursor(args.after); + if (Number.isFinite(index)) { + start = index + 1; + } + } + + const slice = users.slice(start, start + (args.first || users.length)); + + const edges = slice.map((user, i) => ({ + node: user, + cursor: encodeCursor(start + i), + })); + + const startCursor = edges.length > 0 ? edges[0].cursor : null; + const endCursor = edges.length > 0 ? edges[edges.length - 1].cursor : null; + const hasNextPage = start + slice.length < users.length; + const hasPreviousPage = start > 0; + + return { + edges, + pageInfo: { + startCursor, + endCursor, + hasNextPage, + hasPreviousPage, + }, + }; + }, +}; +``` + +This resolver handles forward pagination using `first` and `after`. You can extend it to +support `last` and `before` by reversing the logic. + +## Using a database for pagination + +In production, you'll usually paginate data stored in a database. The same cursor-based +logic applies, but you'll translate cursors into SQL query parameters, typically +as an `OFFSET`. + +The following example shows how to paginate a list of users using PostgreSQL and a Node.js +client like `pg`: + +```js +import db from './db'; + +async function resolveUsers(_, args) { + const limit = args.first ?? 10; + let offset = 0; + + if (args.after) { + const index = decodeCursor(args.after); + if (Number.isFinite(index)) { + offset = index + 1; + } + } + + const result = await db.query( + 'SELECT id, name FROM users ORDER BY id ASC LIMIT $1 OFFSET $2', + [limit + 1, offset] // Fetch one extra row to compute hasNextPage + ); + + const slice = result.rows.slice(0, limit); + const edges = slice.map((user, i) => ({ + node: user, + cursor: encodeCursor(offset + i), + })); + + const startCursor = edges.length > 0 ? edges[0].cursor : null; + const endCursor = edges.length > 0 ? edges[edges.length - 1].cursor : null; + + return { + edges, + pageInfo: { + startCursor, + endCursor, + hasNextPage: result.rows.length > limit, + hasPreviousPage: offset > 0, + }, + }; +} +``` + +This approach supports forward pagination by translating the decoded cursor into +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. + + + +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. + + + +## Handling edge cases + +When implementing pagination, consider how your resolver should handle the following scenarios: + +- **Empty result sets**: Return an empty `edges` array and a `pageInfo` object with +`hasNextPage: false` and `endCursor: null`. +- **Invalid cursors**: If decoding a cursor fails, treat it as a `null` or return an error, +depending on your API's behavior. +- **End of list**: If the requested `first` exceeds the available data, return all remaining +items and set `hasNextPage: false`. + +Always test your pagination with multiple boundaries: beginning, middle, end, and out-of-bounds +errors. + +## Additional resources + +To learn more about cursor-based pagination patterns and best practices, see: + +- [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 diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx new file mode 100644 index 0000000000..043a729b29 --- /dev/null +++ b/website/pages/docs/custom-scalars.mdx @@ -0,0 +1,131 @@ +--- +title: Using Custom Scalars +--- + +# Custom Scalars: When and How to Use Them + +In GraphQL, scalar types represent primitive data like strings, numbers, and booleans. +The GraphQL specification defines five built-in scalars: `Int`, `Float`, +`String`, `Boolean`, and `ID`. + +However, these default types don't cover all the formats or domain-specific values real-world +APIs often need. For example, you might want to represent a timestamp as an ISO 8601 string, or +ensure a user-submitted field is a valid email address. In these cases, you can define a custom +scalar type. + +In GraphQL.js, custom scalars are created using the `GraphQLScalarType` class. This gives you +full control over how values are serialized, parsed, and validated. + +Here’s a simple example of a custom scalar that handles date-time strings: + +```js +import { GraphQLScalarType, Kind } from 'graphql'; + +const DateTime = new GraphQLScalarType({ + name: 'DateTime', + description: 'An ISO-8601 encoded UTC date string.', + serialize(value) { + return value instanceof Date ? value.toISOString() : null; + }, + parseValue(value) { + return typeof value === 'string' ? new Date(value) : null; + }, + parseLiteral(ast) { + return ast.kind === Kind.STRING ? new Date(ast.value) : null; + }, +}); +``` +Custom scalars offer flexibility, but they also shift responsibility onto you. You're +defining not just the format of a value, but also how it is validated and how it moves +through your schema. + +This guide covers when to use custom scalars and how to define them in GraphQL.js. + +## When to use custom scalars + +Define a custom scalar when you need to enforce a specific format, encapsulate domain-specific +logic, or standardize a primitive value across your schema. For example: + +- Validation: Ensure that inputs like email addresses, URLs, or date strings match a +strict format. +- Serialization and parsing: Normalize how values are converted between internal and +client-facing formats. +- Domain primitives: Represent domain-specific values that behave like scalars, such as +UUIDs or currency codes. + +Common examples of useful custom scalars include: + +- `DateTime`: An ISO 8601 timestamp string +- `Email`: A syntactically valid email address +- `URL`: A well-formed web address +- `BigInt`: An integer that exceeds the range of GraphQL's built-in `Int` +- `UUID`: A string that follows a specific identifier format + +## When not to use a custom scalar + +Custom scalars are not a substitute for object types. Avoid using a custom scalar if: + +- The value naturally contains multiple fields or nested data (even if serialized as a string). +- Validation depends on relationships between fields or requires complex cross-checks. +- You're tempted to bypass GraphQL’s type system using a catch-all scalar like `JSON` or `Any`. + +Custom scalars reduce introspection and composability. Use them to extend GraphQL's scalar +system, not to replace structured types altogether. + +## How to define a custom scalar in GraphQL.js + +In GraphQL.js, a custom scalar is defined by creating an instance of `GraphQLScalarType`, +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: + +```js +import { GraphQLScalarType, Kind } from '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)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); + }, + + parseValue(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`DateTime can only parse string values, but got: ${ast.kind}`); + } + const date = new Date(ast.value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${ast.value}`); + } + return date; + }, +}); +``` + +These functions give you full control over validation and data flow. + +## Learn more + +- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars. \ No newline at end of file diff --git a/website/pages/defer-stream.mdx b/website/pages/docs/defer-stream.mdx similarity index 100% rename from website/pages/defer-stream.mdx rename to website/pages/docs/defer-stream.mdx diff --git a/website/pages/getting-started.mdx b/website/pages/docs/getting-started.mdx similarity index 69% rename from website/pages/getting-started.mdx rename to website/pages/docs/getting-started.mdx index c84d1509ac..a4876c633b 100644 --- a/website/pages/getting-started.mdx +++ b/website/pages/docs/getting-started.mdx @@ -11,9 +11,10 @@ import { Tabs } from 'nextra/components'; ## Prerequisites -Before getting started, you should have Node v6 installed, although the examples should mostly work in previous versions of Node as well. +Before getting started, you should have at least Node 20 installed, the examples can be tweaked to work with Node versions +before that by switching to require syntax. For this guide, we won't use any language features that require transpilation, but we will use some ES6 features like -[Promises](http://www.html5rocks.com/en/tutorials/es6/promises/), classes, +[Promises](http://web.dev/articles/promises/), classes, and arrow functions, so if you aren't familiar with them you might want to read up on them first. > Alternatively you can start from [this StackBlitz](https://stackblitz.com/edit/stackblitz-starters-znvgwr) - if you choose @@ -28,12 +29,12 @@ npm install graphql --save ## Writing Code -To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a “resolver” for each API endpoint. For an API that just returns “Hello world!”, we can put this code in a file named `server.js`: +To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a "resolver" for each API endpoint. For an API that just returns "Hello world!", we can put this code in a file named `server.js`: ```javascript -const { graphql, buildSchema } = require('graphql'); +import { graphql, buildSchema } from 'graphql'; // Construct a schema, using GraphQL schema language const schema = buildSchema(`type Query { hello: String } `); @@ -53,42 +54,34 @@ graphql({ }).then((response) => { console.log(response); }); -}); -```` +``` ```javascript -const { graphql, GraphQLSchema, GraphQLObjectType } = require('graphql'); +import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; // Construct a schema const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - hello: { type: GraphQLString }, + hello: { + type: GraphQLString, + resolve: () => 'Hello world!' + }, }, }), }); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { - hello() { - return 'Hello world!'; - }, -}; - -// Run the GraphQL query '{ hello }' and print out the response graphql({ schema, source: '{ hello }', - rootValue, }).then((response) => { console.log(JSON.stringify(response, null, 2)); }); -```` - - +``` + If you run this with: @@ -109,4 +102,4 @@ You should see the GraphQL response printed out: Congratulations - you just executed a GraphQL query! -For practical applications, you'll probably want to run GraphQL queries from an API server, rather than executing GraphQL with a command line tool. To use GraphQL for an API server over HTTP, check out [Running an Express GraphQL Server](/running-an-express-graphql-server/). +For practical applications, you'll probably want to run GraphQL queries from an API server, rather than executing GraphQL with a command line tool. To use GraphQL for an API server over HTTP, check out [Running an Express GraphQL Server](./running-an-express-graphql-server). \ No newline at end of file diff --git a/website/pages/docs/going-to-production.mdx b/website/pages/docs/going-to-production.mdx new file mode 100644 index 0000000000..bce92a5474 --- /dev/null +++ b/website/pages/docs/going-to-production.mdx @@ -0,0 +1,493 @@ +--- +title: Going to Production +--- + +Bringing a GraphQL.js server into production involves more than deploying code. In production, +a GraphQL server should be secure, fast, observable, and protected against abusive queries. + +GraphQL.js includes development-time checks that are useful during local testing but should +be disabled in production to reduce overhead. Additional concerns include caching, error handling, +schema management, and operational monitoring. + +This guide covers key practices to prepare a server built with GraphQL.js for production use. + +## Optimize your build for production + +In development, GraphQL.js includes validation checks to catch common mistakes like invalid schemas +or resolver returns. These checks are not needed in production and can increase runtime overhead. + +You can disable them by setting `process.env.NODE_ENV` to `'production'` during your build process. +GraphQL.js will automatically skip over development-only code paths. + +Bundlers are tools that compile and optimize JavaScript for deployment. Most can be configured to +replace environment variables such as `process.env.NODE_ENV` at build time, +allowing for unused code (such as development only code paths) to be elided by +minification tools. + +### Bundler configuration examples + +The following examples show how to configure common bundlers to set `process.env.NODE_ENV` +and remove development-only code: + +#### Vite + +```js +// vite.config.js +import { defineConfig } from 'vite'; + +export default defineConfig({ + define: { + 'process.env.NODE_ENV': '"production"', + }, +}); +``` + +#### Next.js + +When you build your application with `next build` and run it using `next start`, Next.js sets +`process.env.NODE_ENV` to `'production'` automatically. No additional configuration is required. + +```bash +next build +next start +``` + +If you run a custom server, make sure `NODE_ENV` is set manually. + +#### Create React App (CRA) + +To customize Webpack behavior in CRA, you can use a tool like [`craco`](https://craco.js.org/). +This example uses CommonJS syntax instead of ESM syntax, which is required by `craco.config.js`: + +```js +// craco.config.js +const webpack = require('webpack'); + +module.exports = { + webpack: { + plugins: [ + new webpack.DefinePlugin({ + 'globalThis.process': JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ], + }, +}; +``` + +#### esbuild + +```json +{ + "define": { + "globalThis.process": true, + "process.env.NODE_ENV": "production" + } +} +``` + +#### Webpack + +```js +// webpack.config.js +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export default { + mode: 'production', // Automatically sets NODE_ENV + context: __dirname, +}; +``` + +#### Rollup + +```js +// rollup.config.js +import replace from '@rollup/plugin-replace'; + +export default { + plugins: [ + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ], +}; +``` + +#### SWC + +```json filename=".swcrc" +{ + "jsc": { + "transform": { + "optimizer": { + "globals": { + "vars": { + "globalThis.process": true, + "process.env.NODE_ENV": "production" + } + } + } + } + } +} +``` + +## Secure your schema + +GraphQL gives clients a lot of flexibility, which can be a strength or a liability depending on +how it's used. In production, it's important to control how much of your schema is exposed +and how much work a single query is allowed to do. + +Common strategies for securing a schema include: + +- Disabling introspection for some users +- Limiting query depth or cost +- Enforcing authentication and authorization +- Applying rate limits + +These techniques can help protect your server from accidental misuse or intentional abuse. + +### Only allow trusted documents + +The most reliable way to protect your GraphQL endpoint from malicious requests +is to only allow operations that you trust — those written by your own +engineers — to be executed. + +This technique is not suitable for public APIs that are intended to accept +ad-hoc queries from third parties, but if your GraphQL API is only meant to +power your own websites and apps then it is a simple yet incredibly effective +technique to protect your API endpoint. + +Implementing the trusted documents pattern is straightforward: + +- When deploying a website or application, SHA256 hash the GraphQL documents + (queries, mutations, subscriptions and associated fragments) it contains, and + place them in a trusted store the server has access to. +- When issuing a request from the client, omit the document (`"query":"{...}"`) and + instead provide the document hash (`"documentId": "sha256:..."`). +- The server should retrieve the document from your trusted store via this hash; + if no document is found or no hash is provided then the request should be + rejected. + +This pattern not only improves security significantly by preventing malicious +queries, it has a number of additional benefits: + +- Reduces network size since you're sending a hash (~64 bytes) rather than the + entire GraphQL document (which can be tens of kilobytes). +- Makes schema evolution easier because you have a concrete list of all the + fields/types that are in use. +- Makes tracking issues easier because you can tie a hash to the + client/deployment that introduced it. + +Be careful not to confuse trusted documents (the key component of which are +trust) with automatic persisted queries (APQ) which are a network optimization +potentially open for anyone to use. + +Additional resources: + +- [GraphQL over HTTP Appendix +A: Persisted Documents](https://github.com/graphql/graphql-over-http/pull/264) +- [GraphQL Trusted Documents](https://benjie.dev/graphql/trusted-documents) and + [Techniques to Protect Your GraphQL API](https://benjie.dev/talks/techniques-to-protect) at benjie.dev +- [@graphql-codegen/client-preset persisted documents](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#persisted-documents) or [graphql-codegen-persisted-query-ids](https://github.com/valu-digital/graphql-codegen-persisted-query-ids#integrating-with-apollo-client) for Apollo Client +- [Persisted queries in Relay](https://relay.dev/docs/guides/persisted-queries/) +- [Persisted queries in URQL](https://www.npmjs.com/package/@urql/exchange-persisted) +- [Persisted documents in gql.tada](https://gql-tada.0no.co/guides/persisted-documents) +- [persisted queries with `fetch()`](https://github.com/jasonkuhrt/graffle/issues/269) + +### Control schema introspection + +(Unnecessary if you only allow trusted documents.) + +Introspection lets clients query the structure of your schema, including types +and fields. While helpful during development, it may be an unnecessary in +production and disabling it may reduce your API's attack surface. + +You can disable introspection in production, or only for unauthenticated users: + +```js +import { validate, specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql'; + +const validationRules = isPublicRequest + ? [...specifiedRules, NoSchemaIntrospectionCustomRule] + : specifiedRules; +``` + +Note that many developer tools rely on introspection to function properly. Use introspection +control as needed for your tools and implementation. + +### Limit query complexity + +(Can be a development-only concern if you only allow trusted documents.) + +GraphQL allows deeply nested queries, which can be expensive to resolve. You can prevent this +with query depth limits or cost analysis. + +The following example shows how to limit query depth: + +```js +import depthLimit from 'graphql-depth-limit'; + +const validationRules = [ + depthLimit(10), + ...specifiedRules, +]; +``` + +Instead of depth, you can assign each field a cost and reject queries that exceed a total budget. +Tools like [`graphql-cost-analysis`](https://github.com/pa-bru/graphql-cost-analysis) can help. + +### Require authentication and authorization + +GraphQL doesn't include built-in authentication. Instead, you can attach user data to the request +using middleware, and pass this through to the business logic where +authorization should take place: + +```js +// From your business logic +const postRepository = { + getBody({ user, post }) { + if (user?.id && (user.id === post.authorId)) { + return post.body + } + return null + } +} + +// Resolver for the `Post.body` field: +function Post_body(source, args, context, info) { + // return the post body only if the user is the post's author + return postRepository.getBody({ user: context.user, post: obj }) +} +``` + +For more details, see the [Authentication and Middleware](./authentication-and-express-middleware/) guide. + +### Apply rate limiting + +To prevent abuse, you can limit how often clients access specific operations or fields. The +[`graphql-rate-limit`](https://github.com/teamplanes/graphql-rate-limit#field-config) package lets +you define rate limits directly in your schema using custom directives. + +For more control, you can also implement your own rate-limiting logic using the request +context, such as limiting by user, client ID, or operation name. + +## Improve performance + +In production, performance often depends on how efficiently your resolvers fetch and process data. +GraphQL allows flexible queries, which means a single poorly optimized query can result in +excessive database calls or slow response times. + +### Use batching with DataLoader + +The most common performance issue in GraphQL is the N+1 query problem, where nested resolvers +make repeated calls for related data. `DataLoader` helps avoid this by batching and caching +field-level fetches within a single request. + +For more information on this issue and how to resolve it, see +[Solving the N+1 Problem with DataLoader](./n1-dataloader/). + +### Apply caching where appropriate + +You can apply caching at several levels, depending on your server architecture: + +- **Resolver-level caching**: Cache the results of expensive operations for a short duration. +- **HTTP caching**: Use persisted queries and edge caching to avoid re-processing +common queries. +- **Schema caching**: If your schema is static, avoid rebuilding it on every request. + +For larger applications, consider request-scoped caching or external systems like Redis to avoid +memory growth and stale data. + +## Monitor and debug in production + +Observability is key to diagnosing issues and ensuring your GraphQL server is running smoothly +in production. This includes structured logs, runtime metrics, and distributed traces to +follow requests through your system. + +### Add structured logging + +Use a structured logger to capture events in a machine-readable format. This makes logs easier +to filter and analyze in production systems. Popular options include: + +- [`pino`](https://github.com/pinojs/pino): Fast, minimal JSON logger +- [`winston`](https://github.com/winstonjs/winston): More configurable with plugin support + +You might log things like: + +- Incoming operation names +- Validation or execution errors +- Resolver-level timing +- User IDs or request metadata + +Avoid logging sensitive data like passwords or access tokens. + +### Collect metrics + +Operational metrics help track the health and behavior of your server over time. + +You can use tools like [Prometheus](https://prometheus.io) or [OpenTelemetry](https://opentelemetry.io) +to capture query counts, resolver durations, and error rates. + +There's no built-in GraphQL.js metrics hook, but you can wrap resolvers or use the `execute` +function directly to insert instrumentation. + +### Use tracing tools + +Distributed tracing shows how a request flows through services and where time is spent. This +is especially helpful for debugging performance issues. + +GraphQL.js allows you to hook into the execution pipeline using: + +- `execute`: Trace the overall operation +- `parse` and `validate`: Trace early steps +- `formatResponse`: Attach metadata + +Tracing tools that work with GraphQL include: + +- [Apollo Studio](https://www.apollographql.com/docs/studio/) +- [OpenTelemetry](https://opentelemetry.io) + +## Handle errors + +How you handle errors in production affects both security and client usability. Avoid exposing +internal details in errors, and return errors in a format clients can interpret consistently. + +For more information on how GraphQL.js formats and processes errors, see [Understanding GraphQL.js Errors](./graphql-errors/). + +### Control what errors are exposed + +By default, GraphQL.js includes full error messages and stack traces. In production, you may want +to return a generic error to avoid leaking implementation details. + +You can use a custom error formatter to control this: + +```js +import { GraphQLError } from 'graphql'; + +function formatError(error) { + if (process.env.NODE_ENV === 'production') { + return new GraphQLError('Internal server error'); + } + return error; +} +``` + +This function can be passed to your server, depending on the integration. + +### Add structured error metadata + +GraphQL allows errors to include an `extensions` object, which you can use to add +metadata such as error codes. This helps clients distinguish between different types of +errors: + +```js +throw new GraphQLError('Forbidden', { + extensions: { code: 'FORBIDDEN' }, +}); +``` + +You can also create and throw custom error classes to represent specific cases, such as +authentication or validation failures. + +## Manage your schema safely + +Schemas evolve over time, but removing or changing fields can break client applications. +In production environments, it's important to make schema changes carefully and with clear +migration paths. + +### Deprecate fields before removing them + +Use the `@deprecated` directive to mark fields or enum values that are planned for removal. +Always provide a reason so clients know what to use instead: + +```graphql +type User { + oldField: String @deprecated(reason: "Use `newField` instead.") +} +``` + +Only remove deprecated fields once you're confident no clients depend on them. + +### Detect breaking changes during deployment + +You can compare your current schema against the previous version to detect breaking changes. +Tools that support this include: + +- [`graphql-inspector`](https://github.com/graphql-hive/graphql-inspector) +- [`graphql-cli`](https://github.com/Urigo/graphql-cli) + +Integrate these checks into your CI/CD pipeline to catch issues before they reach production. + +## Use environment-aware configuration + +You should tailor your GraphQL server's behavior based on the runtime environment. + +- Disable introspection and show minimal error messages in production. +- Enable playgrounds like GraphiQL or Apollo Sandbox only in development. +- Control logging verbosity and other debug features via environment flags. + +Example: + +```js +const isDev = process.env.NODE_ENV !== 'production'; + +app.use( + '/graphql', + graphqlHTTP({ + schema, + graphiql: isDev, + customFormatErrorFn: formatError, + }) +); +``` + +## Production readiness checklist + +Use this checklist to verify that your GraphQL.js server is ready for production. +Before deploying, confirm the following checks are complete: + +### Build and environment +- Bundler sets `process.env.NODE_ENV` to `'production'` +- Development-only checks are removed from the production build + +### Schema security +- Authentication is required for requests +- Authorization is enforced via business logic +- Rate limiting is applied +- Only allow trusted documents, or: + - Introspection is disabled or restricted in production + - Query depth is limited + - Query cost limits are in place + +### Performance +- `DataLoader` is used to batch data fetching +- Expensive resolvers use caching (request-scoped or shared) +- Public queries use HTTP or CDN caching +- Schema is reused across requests (not rebuilt each time) + +### Monitoring and observability +- Logs are structured and machine-readable +- Metrics are collected (e.g., with Prometheus or OpenTelemetry) +- Tracing is enabled with a supported tool +- Logs do not include sensitive data + +### Error handling +- Stack traces and internal messages are hidden in production +- Custom error types are used for common cases +- Errors include `extensions.code` for consistent client handling +- A `formatError` function is used to control error output + +### Schema lifecycle +- Deprecated fields are marked with `@deprecated` and a clear reason +- Schema changes are validated before deployment +- CI/CD includes schema diff checks + +### Environment configuration +- Playground tools (e.g., GraphiQL) are only enabled in development +- Error formatting, logging, and introspection are environment-specific diff --git a/website/pages/graphql-clients.mdx b/website/pages/docs/graphql-clients.mdx similarity index 91% rename from website/pages/graphql-clients.mdx rename to website/pages/docs/graphql-clients.mdx index 342193450f..552865c56a 100644 --- a/website/pages/graphql-clients.mdx +++ b/website/pages/docs/graphql-clients.mdx @@ -4,7 +4,7 @@ title: GraphQL Clients Since a GraphQL API has more underlying structure than a REST API, there are more powerful clients like [Relay](https://facebook.github.io/relay/) which can automatically handle batching, caching, and other features. But you don't need a complex client to call a GraphQL server. With `graphql-http`, you can just send an HTTP POST request to the endpoint you mounted your GraphQL server on, passing the GraphQL query as the `query` field in a JSON payload. -For example, let's say we mounted a GraphQL server on http://localhost:4000/graphql as in the example code for [running an Express GraphQL server](/running-an-express-graphql-server/), and we want to send the GraphQL query `{ hello }`. We can do this from the command line with `curl`. If you paste this into a terminal: +For example, let's say we mounted a GraphQL server on http://localhost:4000/graphql as in the example code for [running an Express GraphQL server](./running-an-express-graphql-server), and we want to send the GraphQL query `{ hello }`. We can do this from the command line with `curl`. If you paste this into a terminal: ```bash curl -X POST \ @@ -42,9 +42,9 @@ You should see the data returned, logged in the console: data returned: Object { hello: "Hello world!" } ``` -In this example, the query was just a hardcoded string. As your application becomes more complex, and you add GraphQL endpoints that take arguments as described in [Passing Arguments](/passing-arguments/), you will want to construct GraphQL queries using variables in client code. You can do this by including a keyword prefixed with a dollar sign in the query, and passing an extra `variables` field on the payload. +In this example, the query was just a hardcoded string. As your application becomes more complex, and you add GraphQL endpoints that take arguments as described in [Passing Arguments](./passing-arguments), you will want to construct GraphQL queries using variables in client code. You can do this by including a keyword prefixed with a dollar sign in the query, and passing an extra `variables` field on the payload. -For example, let's say you're running the example server from [Passing Arguments](/passing-arguments/) that has a schema of +For example, let's say you're running the example server from [Passing Arguments](./passing-arguments) that has a schema of ```graphql type Query { @@ -82,4 +82,4 @@ Using this syntax for variables is a good idea because it automatically prevents In general, it will take a bit more time to set up a GraphQL client like Relay, but it's worth it to get more features as your application grows. You might want to start out just using HTTP requests as the underlying transport layer, and switching to a more complex client as your application gets more complex. -At this point you can write a client and server in GraphQL for an API that receives a single string. To do more, you will want to [learn how to use the other basic data types](/basic-types/). +At this point you can write a client and server in GraphQL for an API that receives a single string. To do more, you will want to [learn how to use the other basic data types](./basic-types). diff --git a/website/pages/docs/graphql-errors.mdx b/website/pages/docs/graphql-errors.mdx new file mode 100644 index 0000000000..13e286f025 --- /dev/null +++ b/website/pages/docs/graphql-errors.mdx @@ -0,0 +1,203 @@ +--- +title: Understanding GraphQL.js Errors +--- +import { Callout, GitHubNoteIcon } from "nextra/components"; + +# Understanding GraphQL.js Errors + +When executing a GraphQL operation, a server might encounter problems, such as failing to fetch +data, encountering invalid arguments, or running into unexpected internal issues. Instead of +crashing or halting execution, GraphQL.js collects these problems as structured errors +and includes them in the response. + +This guide explains how GraphQL.js represents errors internally, how errors propagate through a +query, and how you can customize error behavior. + +## How GraphQL.js represents errors in a response + +If an error occurs during execution, GraphQL.js includes it in a top-level `errors` array in the +response, alongside any successfully returned data. + +For example: + +```json +{ + "data": { + "user": null + }, + "errors": [ + { + "message": "User not found", + "locations": [{ "line": 2, "column": 3 }], + "path": ["user"] + } + ] +} +``` + +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 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 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. + + + +## Creating and handling errors with `GraphQLError` + +Internally, GraphQL.js represents errors with the `GraphQLError` class, found in the +`graphql/error` module. + +You can create a `GraphQLError` manually: + +```js +import { GraphQLError } from 'graphql'; + +throw new GraphQLError('Something went wrong'); +``` + +To provide more context about an error, you can pass additional options: + +```js +throw new GraphQLError('Invalid input', { + nodes, + source, + positions, + path, + originalError, + extensions, +}); +``` + +Each option helps tie the error to specific parts of the GraphQL execution: + +- `nodes`: The AST nodes associated with the error. +- `source` and `positions`: The source document and character offsets. +- `path`: The field path leading to the error. +- `originalError`: The underlying JavaScript error, if available. +- `extensions`: Any custom metadata you want to include. + +When a resolver throws an error: + +- 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 +depends on the nullability of the field where the error occurs. + +- **Nullable fields**: If a resolver for a nullable field throws an error, GraphQL.js records +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: + +```graphql +type Query { + user: User +} + +type User { + id: ID! + name: String! +} +``` + +If the `name` resolver throws an error during execution: + +- Because `name` is non-nullable (`String!`), GraphQL.js can't return `null` for just that field. +- Instead, the `user` field itself becomes `null`. +- The error is recorded and included in the response. + +The result looks like: + +```json +{ + "data": { + "user": null + }, + "errors": [ + { + "message": "Failed to fetch user's name", + "path": ["user", "name"] + } + ] +} +``` + +This behavior ensures that non-nullability guarantees are respected even in the presence of errors. + +For more detailed rules, see the [GraphQL Specification on error handling](https://spec.graphql.org/October2021/#sec-Errors). + +## Customizing errors with `extensions` + +You can add additional information to errors using the `extensions` field. This is useful for +passing structured metadata like error codes, HTTP status codes, or debugging hints. + +For example: + +```js +throw new GraphQLError('Unauthorized', { + extensions: { + code: 'UNAUTHORIZED', + http: { + status: 401 + } + } +}); +``` + +Clients can inspect the `extensions` field instead of relying on parsing `message` strings. + +Common use cases for `extensions` include: + +- Assigning machine-readable error codes (`code: 'BAD_USER_INPUT'`) +- Specifying HTTP status codes +- Including internal debug information (hidden from production clients) + +Libraries like [Apollo Server](https://www.apollographql.com/docs/apollo-server/data/errors/) and +[Envelop](https://the-guild.dev/graphql/envelop/plugins/use-error-handler) offer conventions for +structured error extensions, if you want to adopt standardized patterns. + +## Best practices for error handling + +- Write clear, actionable messages. Error messages should help developers understand what went +wrong and how to fix it. +- Use error codes in extensions. Define a set of stable, documented error codes for your API +to make client-side error handling easier. +- Avoid leaking internal details. Do not expose stack traces, database errors, or other +sensitive information to clients. +- Wrap unexpected errors. Catch and wrap low-level exceptions to ensure that all errors passed +through your GraphQL server follow the `GraphQLError` structure. + +In larger servers, you might centralize error handling with a custom error formatting function +to enforce these best practices consistently. + +## Additional resources + +- [GraphQLError reference](https://graphql.org/graphql-js/error/#graphqlerror) +- [GraphQL Specification: Error handling](https://spec.graphql.org/October2021/#sec-Errors) +- [Apollo Server: Error handling](https://www.apollographql.com/docs/apollo-server/data/errors/) +- [Envelop: Error plugins](https://the-guild.dev/graphql/envelop/plugins/use-error-handler) \ No newline at end of file diff --git a/website/pages/docs/index.mdx b/website/pages/docs/index.mdx new file mode 100644 index 0000000000..3b45c15e4b --- /dev/null +++ b/website/pages/docs/index.mdx @@ -0,0 +1,19 @@ +--- +title: Overview +sidebarTitle: Overview +--- + +GraphQL.js is the official JavaScript implementation of the +[GraphQL Specification](https://spec.graphql.org/draft/). It provides the core building blocks +for constructing GraphQL servers, clients, tools, and utilities in JavaScript and TypeScript. + +This documentation site is for developers who want to: + +- Understand how GraphQL works +- Build a GraphQL API using GraphQL.js +- Extend, customize, or introspect GraphQL systems +- Learn best practices for using GraphQL.js in production + +Whether you're writing your own server, building a GraphQL clients, or creating tools +that work with GraphQL, this site guides you through core concepts, APIs, and +advanced use cases of GraphQL.js. diff --git a/website/pages/mutations-and-input-types.mdx b/website/pages/docs/mutations-and-input-types.mdx similarity index 71% rename from website/pages/mutations-and-input-types.mdx rename to website/pages/docs/mutations-and-input-types.mdx index 7b4bfa4859..7d45bbe6f4 100644 --- a/website/pages/mutations-and-input-types.mdx +++ b/website/pages/docs/mutations-and-input-types.mdx @@ -6,7 +6,7 @@ import { Tabs } from 'nextra/components'; If you have an API endpoint that alters data, like inserting data into a database or altering data already in a database, you should make this endpoint a `Mutation` rather than a `Query`. This is as simple as making the API endpoint part of the top-level `Mutation` type instead of the top-level `Query` type. -Let's say we have a “message of the day” server, where anyone can update the message of the day, and anyone can read the current one. The GraphQL schema for this is simply: +Let's say we have a "message of the day" server, where anyone can update the message of the day, and anyone can read the current one. The GraphQL schema for this is simply: @@ -18,15 +18,15 @@ type Mutation { type Query { getMessage: String } -```` +``` ```js -const { +import { GraphQLObjectType, GraphQLString, GraphQLSchema, -} = require('graphql'); +} from 'graphql'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -42,7 +42,7 @@ const schema = new GraphQLSchema({ }, }), }); -```` +``` @@ -64,7 +64,7 @@ const root = { }; ``` -You don't need anything more than this to implement mutations. But in many cases, you will find a number of different mutations that all accept the same input parameters. A common example is that creating an object in a database and updating an object in a database often take the same parameters. To make your schema simpler, you can use “input types” for this, by using the `input` keyword instead of the `type` keyword. +You don't need anything more than this to implement mutations. But in many cases, you will find a number of different mutations that all accept the same input parameters. A common example is that creating an object in a database and updating an object in a database often take the same parameters. To make your schema simpler, you can use "input types" for this, by using the `input` keyword instead of the `type` keyword. For example, instead of a single message of the day, let's say we have many messages, indexed in a database by the `id` field, and each message has both a `content` string and an `author` string. We want a mutation API both for creating a new message and for updating an old message. We could use the schema: @@ -91,18 +91,22 @@ type Mutation { updateMessage(id: ID!, input: MessageInput): Message } -```` +``` ```js -const { +import { GraphQLObjectType, GraphQLString, GraphQLSchema, GraphQLID, GraphQLInputObjectType, GraphQLNonNull, -} = require('graphql'); +} from 'graphql'; +import { randomBytes } from 'node:crypto'; + +// Maps username to content +const fakeDatabase = {}; const MessageInput = new GraphQLInputObjectType({ name: 'MessageInput', @@ -130,6 +134,16 @@ const schema = new GraphQLSchema({ args: { id: { type: new GraphQLNonNull(GraphQLID) }, }, + resolve: (_, { id }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + return fakeDatabase[id] ? { + id, + content: fakeDatabase[id].content, + author: fakeDatabase[id].author, + } : null; + } }, }, }), @@ -141,6 +155,16 @@ const schema = new GraphQLSchema({ args: { input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { input }) => { + // Create a random id for our "database". + const id = randomBytes(10).toString('hex'); + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, updateMessage: { type: Message, @@ -148,11 +172,23 @@ const schema = new GraphQLSchema({ id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { id, input }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + // This replaces all old data, but some apps might want partial update. + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, }, }), }); -```` +``` @@ -168,9 +204,11 @@ Here's some runnable code that implements this schema, keeping the data in memor ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; + +const fakeDatabase = {}; // Construct a schema, using GraphQL schema language const schema = buildSchema(` @@ -187,6 +225,27 @@ type Message { type Query { getMessage(id: ID!): Message + getMessages: [Message] +} + +const root = { + getMessage: ({ id }) => { + return fakeDatabase[id] + }, + getMessages: () => { + return Object.values(fakeDatabase) + }, + createMessage: ({ input }) => { + const id = String(Object.keys(fakeDatabase).length + 1) + const message = new Message(id, input) + fakeDatabase[id] = message + return message + }, + updateMessage: ({ id, input }) => { + const message = fakeDatabase[id] + Object.assign(message, input) + return message + } } type Mutation { @@ -204,59 +263,43 @@ class Message { } } -// Maps username to content -const fakeDatabase = {}; - -const root = { - getMessage({ id }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - return new Message(id, fakeDatabase[id]); - }, - createMessage({ input }) { - // Create a random id for our "database". - const id = require('crypto').randomBytes(10).toString('hex'); - - fakeDatabase[id] = input; - return new Message(id, input); - }, - updateMessage({ id, input }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - // This replaces all old data, but some apps might want partial update. - fakeDatabase[id] = input; - return new Message(id, input); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, + rootValue: root, }), ); app.listen(4000, () => { console.log('Running a GraphQL API server at localhost:4000/graphql'); }); - -```` +``` ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, GraphQLString, GraphQLSchema, GraphQLID, GraphQLInputObjectType, GraphQLNonNull, -} = require('graphql'); +} from 'graphql'; + +// If Message had any complex fields, we'd put them on this object. +class Message { + constructor(id, { content, author }) { + this.id = id; + this.content = content; + this.author = author; + } +} + +// Maps username to content +const fakeDatabase = {}; const MessageInput = new GraphQLInputObjectType({ name: 'MessageInput', @@ -284,6 +327,16 @@ const schema = new GraphQLSchema({ args: { id: { type: new GraphQLNonNull(GraphQLID) }, }, + resolve: (_, { id }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + return fakeDatabase[id] ? { + id, + content: fakeDatabase[id].content, + author: fakeDatabase[id].author, + } : null; + } }, }, }), @@ -295,6 +348,17 @@ const schema = new GraphQLSchema({ args: { input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { input }) => { + // Create a random id for our "database". + import { randomBytes } from 'crypto'; + const id = randomBytes(10).toString('hex'); + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, updateMessage: { type: Message, @@ -302,59 +366,34 @@ const schema = new GraphQLSchema({ id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { id, input }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + // This replaces all old data, but some apps might want partial update. + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, }, }), }); -// If Message had any complex fields, we'd put them on this object. -class Message { - constructor(id, { content, author }) { - this.id = id; - this.content = content; - this.author = author; - } -} - -// Maps username to content -const fakeDatabase = {}; - -const root = { - getMessage({ id }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - return new Message(id, fakeDatabase[id]); - }, - createMessage({ input }) { - // Create a random id for our "database". - const id = require('crypto').randomBytes(10).toString('hex'); - - fakeDatabase[id] = input; - return new Message(id, input); - }, - updateMessage({ id, input }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - // This replaces all old data, but some apps might want partial update. - fakeDatabase[id] = input; - return new Message(id, input); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000, () => { console.log('Running a GraphQL API server at localhost:4000/graphql'); }); -```` +``` @@ -402,4 +441,4 @@ fetch('/graphql', { .then((data) => console.log('data returned:', data)); ``` -One particular type of mutation is operations that change users, like signing up a new user. While you can implement this using GraphQL mutations, you can reuse many existing libraries if you learn about [GraphQL with authentication and Express middleware](/authentication-and-express-middleware/). +One particular type of mutation is operations that change users, like signing up a new user. While you can implement this using GraphQL mutations, you can reuse many existing libraries if you learn about [GraphQL with authentication and Express middleware](./authentication-and-express-middleware). diff --git a/website/pages/docs/n1-dataloader.mdx b/website/pages/docs/n1-dataloader.mdx new file mode 100644 index 0000000000..57e8f351aa --- /dev/null +++ b/website/pages/docs/n1-dataloader.mdx @@ -0,0 +1,149 @@ +--- +title: Solving the N+1 Problem with `DataLoader` +--- + +When building your first server with GraphQL.js, it's common to encounter +performance issues related to the N+1 problem: a pattern that +results in many unnecessary database or service calls, +especially in nested query structures. + +This guide explains what the N+1 problem is, why it's relevant in +GraphQL field resolution, and how to address it using +[`DataLoader`](https://github.com/graphql/dataloader). + +## What is the N+1 problem? + +The N+1 problem happens when your API fetches a list of items using one +query, and then issues an additional query for each item in the list. +In GraphQL, this usually occurs in nested field resolvers. + +For example, in the following query: + +```graphql +{ + posts { + id + title + author { + name + } + } +} +``` + +If the `posts` field returns 10 items, and each `author` field fetches +the author by ID with a separate database call, the server performs +11 total queries: one to fetch the posts, and one for each post's author +(10 total authors). As the number of parent items increases, the number +of database calls grows, which can degrade performance. + +Even if several posts share the same author, the server will still issue +duplicate queries unless you implement deduplication or batching manually. + +## Why this happens in GraphQL.js + +In GraphQL.js, each field resolver runs independently. There's no built-in +coordination between resolvers, and no automatic batching. This makes field +resolvers composable and predictable, but it also creates the N+1 problem. +Nested resolutions, such as fetching an author for each post in the previous +example, will each call their own data-fetching logic, even if those calls +could be grouped. + +## Solving the problem with `DataLoader` + +[`DataLoader`](https://github.com/graphql/dataloader) is a utility library designed +to solve this problem. It batches multiple `.load(key)` calls into a single `batchLoadFn(keys)` +call and caches results during the life of a request. This means you can reduce redundant data +fetches and group related lookups into efficient operations. + +To use `DataLoader` in a `graphql-js` server: + +1. Create `DataLoader` instances for each request. +2. Attach the instance to the `contextValue` passed to GraphQL execution. You can attach the +loader when calling [`graphql()`](https://graphql.org/graphql-js/graphql/#graphql) directly, or +when setting up a GraphQL HTTP server such as [express-graphql](https://github.com/graphql/express-graphql). +3. Use `.load(id)` in resolvers to fetch data through the loader. + +### Example: Batching author lookups + +Suppose each `Post` has an `authorId`, and you have a `getUsersByIds(ids)` +function that can fetch multiple users in a single call: + +{/* prettier-ignore */} +```js {14-17,37} +import { + graphql, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLList, + GraphQLID +} from 'graphql'; +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: () => ({ + id: { type: GraphQLID }, + name: { type: GraphQLString }, + }), +}); + +const PostType = new GraphQLObjectType({ + name: 'Post', + fields: () => ({ + id: { type: GraphQLID }, + title: { type: GraphQLString }, + author: { + type: UserType, + resolve(post, args, context) { + return context.userLoader.load(post.authorId); + }, + }, + }), +}); + +const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + posts: { + type: GraphQLList(PostType), + resolve: () => getPosts(), + }, + }), +}); + +const schema = new GraphQLSchema({ query: QueryType }); +``` + +With this setup, all `.load(authorId)` calls are automatically collected and batched +into a single call to `getUsersByIds`. `DataLoader` also caches results for the duration +of the request, so repeated `.load(id)` calls for the same ID don't trigger +additional fetches. + +## Best practices + +- Create a new `DataLoader` instance per request. This ensures that caching is scoped +correctly and avoids leaking data between users. +- Always return results in the same order as the input keys. This is required by the +`DataLoader` contract. If a key is not found, return `null` or throw depending on +your policy. +- Keep batch functions focused. Each loader should handle a specific data access pattern. +- Use `.loadMany()` sparingly. While it's useful when you already have a list of IDs, it's +typically not needed in field resolvers, since `.load()` already batches individual calls +made within the same execution cycle. + +## Additional resources + +- [`DataLoader` GitHub repository](https://github.com/graphql/dataloader): Includes full API docs and usage examples +- [GraphQL field resolvers](https://graphql.org/graphql-js/resolvers/): Background on how field resolution works. diff --git a/website/pages/docs/nullability.mdx b/website/pages/docs/nullability.mdx new file mode 100644 index 0000000000..7d514b334f --- /dev/null +++ b/website/pages/docs/nullability.mdx @@ -0,0 +1,228 @@ +--- +title: Nullability +sidebarTitle: Nullability in GraphQL.js +--- + +# Nullability in GraphQL.js + +Nullability is a core concept in GraphQL that affects how schemas are defined, +how execution behaves, and how clients interpret results. In GraphQL.js, +nullability plays a critical role in both schema construction and +runtime behavior. + +This guide explains how nullability works, how it's represented in GraphQL.js, +and how to design schemas with nullability in mind. + +## How nullability works + +In GraphQL, fields are nullable by default. This means if a resolver function +returns `null`, the result will include a `null` value unless the field is +explicitly marked as non-nullable. + +When a non-nullable field resolves to `null`, the GraphQL execution engine +raises a runtime error and attempts to recover by replacing the nearest +nullable parent field with `null`. This behavior is known +as null bubbling. + +Understanding nullability requires familiarity with the GraphQL type system, +execution semantics, and the trade-offs involved in schema design. + +## The role of `GraphQLNonNull` + +GraphQL.js represents non-nullability using the `GraphQLNonNull` wrapper type. +By default, all fields are nullable: + +```js +import { + GraphQLObjectType, + GraphQLString, + GraphQLNonNull, +} from 'graphql'; + +const UserType = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + id: { type: new GraphQLNonNull(GraphQLString) }, + email: { type: GraphQLString }, + }), +}); +``` + +In this example, the `id` field is non-nullable, meaning it must always +resolve to a string. The `email` field is nullable. + +You can use `GraphQLNonNull` with: + +- Field types +- Argument types +- Input object fields +- Return types for resolvers + +You can also combine it with other types to create more +specific constraints. For example: + +```js +new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UserType))) +``` + +This structure corresponds to [User!]! in SDL: a non-nullable list of non-null +`User` values. When reading code like this, work from the inside out: `UserType` +is non-nullable, and wrapped in a list, which is itself non-nullable. + +## Execution behavior + +GraphQL.js uses nullability rules to determine how to handle `null` values +at runtime: + +- If a nullable field returns `null`, the result includes that field with +a `null` value. +- If a non-nullable field returns `null`, GraphQL throws an error and +sets the nearest nullable parent field to `null`. + +This bubbling behavior prevents partial data from being returned in cases where +a non-nullable guarantee is violated. + +Here's an example that shows this in action: + +```js +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + GraphQLNonNull, +} from 'graphql'; + +const UserType = new GraphQLObjectType({ + name: 'User', + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + }, +}); + +const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: UserType, + resolve: () => ({ id: null }), + }, + }, +}); + +const schema = new GraphQLSchema({ query: QueryType }); +``` + +In this example, the `user` field returns an object with `id: null`. +Because `id` is non-nullable, GraphQL can't return `user.id`, and instead +nullifies the `user` field entirely. An error describing the violation is +added to the `errors` array in the response. + +## Schema design considerations + +Using non-null types communicates clear expectations to clients, but it's +also less forgiving. When deciding whether to use `GraphQLNonNull`, keep +the following in mind: + +- Use non-null types when a value is always expected. This reflects intent +and reduces ambiguity for clients. +- Avoid aggressive use of non-null types in early schema versions. It limits +your ability to evolve the API later. +- Be cautious of error bubbling. A `null` return from a deeply nested non-nullable +field can affect large portions of the response. + +### Versioning + +Non-null constraints are part of a field's contract: + +- Changing a field from non-nullable to nullable is a breaking change. +- Changing from nullable to non-nullable is also breaking unless you can +guarantee that the field will never return `null`. + +To reduce the risk of versioning issues, start with nullable fields and add +constraints as your schema matures. + +## Using `GraphQLNonNull` in schema and resolvers + +Let's walk through two practical scenarios that show how GraphQL.js enforces +nullability. + +### Defining a non-null field + +This example defines a `Product` type with a non-nullable `name` field: + +```js +import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; + +const ProductType = new GraphQLObjectType({ + name: 'Product', + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + }), +}); +``` + +This configuration guarantees that `name` must always be a string +and never `null`. If a resolver returns `null` for this field, an +error will be thrown. + +### Resolver returns `null` for a non-null field + +In this example, the resolver returns an object with `name: null`, violating +the non-null constraint: + +```js +import { + GraphQLObjectType, + GraphQLString, + GraphQLNonNull, + GraphQLSchema, +} from 'graphql'; + +const ProductType = new GraphQLObjectType({ + name: 'Product', + fields: { + name: { type: new GraphQLNonNull(GraphQLString) }, + }, +}); + +const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + product: { + type: ProductType, + resolve: () => ({ name: null }), + }, + }, +}); + +const schema = new GraphQLSchema({ query: QueryType }); +``` + +In this example, the `product` resolver returns an object with `name: null`. +Because the `name` field is non-nullable, GraphQL.js responds by +nullifying the entire `product` field and appending a +corresponding error to the response. + +## Best practices + +- Default to nullable. Start with nullable fields and introduce non-null +constraints when data consistency is guaranteed. +- Express intent. Use non-null when a field must always be present for logical +correctness. +- Validate early. Add checks in resolvers to prevent returning `null` for +non-null fields. +- Watch for nesting. Distinguish between: + - `[User]!` - nullable list of non-null users + - `[User!]!` - non-null list of non-null users + +## Additional resources + +- [GraphQL Specification: Non-null](https://spec.graphql.org/draft/#sec-Non-Null): +Defines the formal behavior of non-null types in the GraphQL type system and +execution engine. +- [Understanding GraphQL.js Errors](website\pages\docs\graphql-errors.mdx): Explains +how GraphQL.js propagates and formats execution-time errors. +- [Anatomy of a Resolver](website\pages\docs\resolver-anatomy.mdx): Breaks down +how resolvers work in GraphQL.js. +- [Constructing Types](website\pages\docs\constructing-types.mdx): Shows how +to define and compose types in GraphQL.js. diff --git a/website/pages/object-types.mdx b/website/pages/docs/object-types.mdx similarity index 75% rename from website/pages/object-types.mdx rename to website/pages/docs/object-types.mdx index 366620c970..6b15c86646 100644 --- a/website/pages/object-types.mdx +++ b/website/pages/docs/object-types.mdx @@ -6,7 +6,7 @@ import { Tabs } from 'nextra/components'; In many cases, you don't want to return a number or a string from an API. You want to return an object that has its own complex behavior. GraphQL is a perfect fit for this. -In GraphQL schema language, the way you define a new object type is the same way we have been defining the `Query` type in our examples. Each object can have fields that return a particular type, and methods that take arguments. For example, in the [Passing Arguments](/passing-arguments/) documentation, we had a method to roll some random dice: +In GraphQL schema language, the way you define a new object type is the same way we have been defining the `Query` type in our examples. Each object can have fields that return a particular type, and methods that take arguments. For example, in the [Passing Arguments](./passing-arguments) documentation, we had a method to roll some random dice: @@ -18,14 +18,14 @@ type Query { ```js -const { +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLList, - GraphQLFloat, -} = require('graphql'); + GraphQLFloat +} from 'graphql'; new GraphQLObjectType({ name: 'Query', @@ -43,8 +43,7 @@ new GraphQLObjectType({ }, }, }) - -```` +``` @@ -60,29 +59,49 @@ type RandomDie { type Query { getDie(numSides: Int): RandomDie } -```` +``` ```js -const { +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLList, GraphQLFloat, -} = require('graphql'); + GraphQLSchema +} from 'graphql'; const RandomDie = new GraphQLObjectType({ name: 'RandomDie', fields: { + numSides: { + type: new GraphQLNonNull(GraphQLInt), + resolve: function(die) { + return die.numSides; + } + }, + rollOnce: { + type: new GraphQLNonNull(GraphQLInt), + resolve: function(die) { + return 1 + Math.floor(Math.random() * die.numSides); + } + }, roll: { type: new GraphQLList(GraphQLInt), args: { numRolls: { type: new GraphQLNonNull(GraphQLInt) + }, + }, + resolve: function(die, { numRolls }) { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); } + return output; } } } @@ -98,13 +117,17 @@ const schema = new GraphQLSchema({ numSides: { type: GraphQLInt } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, + } } } } }) }); - -```` +``` @@ -122,7 +145,7 @@ class RandomDie { roll({ numRolls }) { const output = []; - for (const i = 0; i < numRolls; i++) { + for (let i = 0; i < numRolls; i++) { output.push(this.rollOnce()); } return output; @@ -134,7 +157,7 @@ const root = { return new RandomDie(numSides || 6); }, }; -```` +``` For fields that don't use any arguments, you can use either properties on the object or instance methods. So for the example code above, both `numSides` and `rollOnce` can actually be used to implement GraphQL fields, so that code also implements the schema of: @@ -148,30 +171,32 @@ type RandomDie { } type Query { -getDie(numSides: Int): RandomDie + getDie(numSides: Int): RandomDie } - -```` +``` ```js -const { +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLList, GraphQLFloat, -} = require('graphql'); + GraphQLSchema +} from 'graphql'; const RandomDie = new GraphQLObjectType({ name: 'RandomDie', fields: { numSides: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides }, rollOnce: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => 1 + Math.floor(Math.random() * die.numSides) }, roll: { type: new GraphQLList(GraphQLInt), @@ -179,6 +204,13 @@ const RandomDie = new GraphQLObjectType({ numRolls: { type: new GraphQLNonNull(GraphQLInt) }, + }, + resolve: (die, { numRolls }) => { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); + } + return output; } } } @@ -194,13 +226,17 @@ const schema = new GraphQLSchema({ numSides: { type: GraphQLInt } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, + } } } } }) }); -```` - +``` @@ -209,9 +245,9 @@ Putting this all together, here is some sample code that runs a server with this ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; // Construct a schema, using GraphQL schema language const schema = buildSchema(` @@ -233,13 +269,13 @@ class RandomDie { } rollOnce() { - return 1 + Math.floor(Math.random() \* this.numSides); + return 1 + Math.floor(Math.random() * this.numSides); } roll({ numRolls }) { const output = []; - for (const i = 0; i < numRolls; i++) { - output.push(this.rollOnce()); + for (let i = 0; i < numRolls; i++) { + output.push(this.rollOnce()); } return output; } @@ -262,29 +298,31 @@ app.all( ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, - GraphQLString, GraphQLList, GraphQLFloat, -} = require('graphql'); + GraphQLSchema +} from 'graphql'; const RandomDie = new GraphQLObjectType({ name: 'RandomDie', fields: { numSides: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides }, rollOnce: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => 1 + Math.floor(Math.random() * die.numSides) }, roll: { type: new GraphQLList(GraphQLInt), @@ -292,6 +330,13 @@ const RandomDie = new GraphQLObjectType({ numRolls: { type: new GraphQLNonNull(GraphQLInt) }, + }, + resolve: (die, { numRolls }) => { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); + } + return output; } } } @@ -307,50 +352,27 @@ const schema = new GraphQLSchema({ numSides: { type: GraphQLInt } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, + } } } } }) }); -// This class implements the RandomDie GraphQL type -class RandomDie { - constructor(numSides) { - this.numSides = numSides; - } - - rollOnce() { - return 1 + Math.floor(Math.random() * this.numSides); - } - - roll({ numRolls }) { - const output = []; - for (const i = 0; i < numRolls; i++) { - output.push(this.rollOnce()); - } - return output; - } -} - -// The root provides the top-level API endpoints -const root = { - getDie({ numSides }) { - return new RandomDie(numSides || 6); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` - +``` @@ -369,4 +391,4 @@ If you run this code with `node server.js` and browse to http://localhost:4000/g This way of defining object types often provides advantages over a traditional REST API. Instead of doing one API request to get basic information about an object, and then multiple subsequent API requests to find out more information about that object, you can get all of that information in one API request. That saves bandwidth, makes your app run faster, and simplifies your client-side logic. -So far, every API we've looked at is designed for returning data. In order to modify stored data or handle complex input, it helps to [learn about mutations and input types](/mutations-and-input-types/). +So far, every API we've looked at is designed for returning data. In order to modify stored data or handle complex input, it helps to [learn about mutations and input types](./mutations-and-input-types). diff --git a/website/pages/oneof-input-objects.mdx b/website/pages/docs/oneof-input-objects.mdx similarity index 98% rename from website/pages/oneof-input-objects.mdx rename to website/pages/docs/oneof-input-objects.mdx index 95be65d2c2..0a968eace7 100644 --- a/website/pages/oneof-input-objects.mdx +++ b/website/pages/docs/oneof-input-objects.mdx @@ -24,7 +24,7 @@ const schema = buildSchema(` shelfNumber: Int! positionOnShelf: Int! } - + input ProductSpecifier @oneOf { id: ID name: String @@ -88,4 +88,4 @@ const schema = new GraphQLSchema({ It doesn't matter whether you have 2 or more inputs here, all that matters is that your user will have to specify one, and only one, for this input to be valid. -The values are not limited to scalars, lists and other input object types are also allowed. \ No newline at end of file +The values are not limited to scalars, lists and other input object types are also allowed. diff --git a/website/pages/passing-arguments.mdx b/website/pages/docs/passing-arguments.mdx similarity index 70% rename from website/pages/passing-arguments.mdx rename to website/pages/docs/passing-arguments.mdx index 0017a69638..c1c010dde0 100644 --- a/website/pages/passing-arguments.mdx +++ b/website/pages/docs/passing-arguments.mdx @@ -4,7 +4,7 @@ title: Passing Arguments import { Tabs } from 'nextra/components'; -Just like a REST API, it's common to pass arguments to an endpoint in a GraphQL API. By defining the arguments in the schema language, typechecking happens automatically. Each argument must be named and have a type. For example, in the [Basic Types documentation](/basic-types/) we had an endpoint called `rollThreeDice`: +Just like a REST API, it's common to pass arguments to an endpoint in a GraphQL API. By defining the arguments in the schema language, typechecking happens automatically. Each argument must be named and have a type. For example, in the [Basic Types documentation](./basic-types) we had an endpoint called `rollThreeDice`: ```graphql type Query { @@ -12,7 +12,7 @@ type Query { } ``` -Instead of hard coding “three”, we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides. We can add arguments to the GraphQL schema language like this: +Instead of hard coding "three", we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides. We can add arguments to the GraphQL schema language like this: @@ -24,51 +24,67 @@ type Query { ```js -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, - GraphQLNonNull, - GraphQLInt, - GraphQLString, + GraphQLSchema, GraphQLList, GraphQLFloat, -} = require('graphql'); - -new GraphQLObjectType({ - name: 'Query', - fields: { - rollDice: { - type: new GraphQLList(GraphQLFloat), - args: { - numDice: { - type: new GraphQLNonNull(GraphQLInt) - }, - numSides: { - type: new GraphQLNonNull(GraphQLInt) + GraphQLInt, + GraphQLNonNull +} from 'graphql'; + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + rollDice: { + type: new GraphQLList(GraphQLFloat), + args: { + numDice: { type: new GraphQLNonNull(GraphQLInt) }, + numSides: { type: GraphQLInt }, }, + resolve: (_, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + } }, }, - }, -}) + }), +}); -```` +const app = express(); +app.all( + '/graphql', + createHandler({ + schema: schema, + }), +); +app.listen(4000); +console.log('Running a GraphQL API server at localhost:4000/graphql'); +``` The exclamation point in `Int!` indicates that `numDice` can't be null, which means we can skip a bit of validation logic to make our server code simpler. We can let `numSides` be null and assume that by default a die has 6 sides. -So far, our resolver functions took no arguments. When a resolver takes arguments, they are passed as one “args” object, as the first argument to the function. So rollDice could be implemented as: +So far, our resolver functions took no arguments. When a resolver takes arguments, they are passed as one "args" object, as the first argument to the function. So rollDice could be implemented as: ```js const root = { rollDice(args) { const output = []; - for (const i = 0; i < args.numDice; i++) { + for (let i = 0; i < args.numDice; i++) { output.push(1 + Math.floor(Math.random() * (args.numSides || 6))); } return output; }, }; -```` +``` It's convenient to use [ES6 destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) for these parameters, since you know what format they will be. So we can also write `rollDice` as @@ -76,7 +92,7 @@ It's convenient to use [ES6 destructuring assignment](https://developer.mozilla. const root = { rollDice({ numDice, numSides }) { const output = []; - for (const i = 0; i < numDice; i++) { + for (let i = 0; i < numDice; i++) { output.push(1 + Math.floor(Math.random() * (numSides || 6))); } return output; @@ -91,9 +107,9 @@ The entire code for a server that hosts this `rollDice` API is: ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { buildSchema } = require('graphql'); +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; // Construct a schema, using GraphQL schema language const schema = buildSchema(/_ GraphQL _/ ` type Query { rollDice(numDice: Int!, numSides: Int): [Int] }`); @@ -102,8 +118,8 @@ const schema = buildSchema(/_ GraphQL _/ ` type Query { rollDice(numDice: Int!, const root = { rollDice({ numDice, numSides }) { const output = []; - for (const i = 0; i < numDice; i++) { - output.push(1 + Math.floor(Math.random() \* (numSides || 6))); + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); } return output; }, @@ -119,21 +135,21 @@ app.all( ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); - -```` +``` ```js -const express = require('express'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLList, GraphQLFloat, -} = require('graphql'); + GraphQLSchema +} from 'graphql'; // Construct a schema, using GraphQL schema language const schema = new GraphQLSchema({ @@ -150,34 +166,28 @@ const schema = new GraphQLSchema({ type: new GraphQLNonNull(GraphQLInt) }, }, + resolve: (_, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + } }, }, }) }) -// The root provides a resolver function for each API endpoint -const root = { - rollDice({ numDice, numSides }) { - const output = []; - for (const i = 0; i < numDice; i++) { - output.push(1 + Math.floor(Math.random() * (numSides || 6))); - } - return output; - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` - +``` @@ -221,4 +231,4 @@ fetch('/graphql', { Using `$dice` and `$sides` as variables in GraphQL means we don't have to worry about escaping on the client side. -With basic types and argument passing, you can implement anything you can implement in a REST API. But GraphQL supports even more powerful queries. You can replace multiple API calls with a single API call if you learn how to [define your own object types](/object-types/). +With basic types and argument passing, you can implement anything you can implement in a REST API. But GraphQL supports even more powerful queries. You can replace multiple API calls with a single API call if you learn how to [define your own object types](./object-types). diff --git a/website/pages/docs/resolver-anatomy.mdx b/website/pages/docs/resolver-anatomy.mdx new file mode 100644 index 0000000000..6799d021a0 --- /dev/null +++ b/website/pages/docs/resolver-anatomy.mdx @@ -0,0 +1,139 @@ +--- +title: Anatomy of a Resolver +--- + +# Anatomy of a Resolver + +In GraphQL.js, a resolver is a function that returns the value for a +specific field in a schema. Resolvers connect a GraphQL query to the +underlying data or logic needed to fulfill it. + +This guide breaks down the anatomy of a resolver, how GraphQL.js uses +them during query execution, and best practices for writing them effectively. + +## What is a resolver? + +A resolver is responsible for returning the value for a specific field in a +GraphQL query. During execution, GraphQL.js calls a resolver for each field, +either using a custom function you provide or falling back to a default +behavior. + +If no resolver is provided, GraphQL.js tries to retrieve a property from the +parent object that matches the field name. If the property is a function, it +calls the function and uses the result. Otherwise, it returns the property +value directly. + +You can think of a resolver as a translator between the schema and the +actual data. The schema defines what can be queried, while resolvers +determine how to fetch or compute the data at runtime. + +## Resolver function signature + +When GraphQL.js executes a resolver, it calls the resolver function +with four arguments: + +```js +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 (after resolving any +lists). For root fields, it is the `rootValue` passed to GraphQL, which is often +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`. +- `context`: A shared object available to every resolver in an operation. +It is commonly used to store per-request state like authentication +information, database connections, or caching utilities. +- `info`: Information about the current execution state, including +the field name, path to the field from the root, the return type, the parent +type, and the full schema. It is mainly useful for advanced scenarios such +as query optimization or logging. + +Resolvers can use any combination of these arguments, depending on the needs +of the field they are resolving. + +## Default resolvers + +If you do not provide a resolver for a field, GraphQL.js uses a built-in +default resolver called `defaultFieldResolver`. + +The default behavior is simple: + +- It looks for a property on the `source` object that matches the name of +the field. +- If the property exists and is a function, it calls the function and uses the +result. +- Otherwise, it returns the property value directly. + +This default resolution makes it easy to build simple schemas without +writing custom resolvers for every field. For example, if your `source` object +already contains fields with matching names, GraphQL.js can resolve them +automatically. + +You can override the default behavior by specifying a `resolve` function when +defining a field in the schema. This is necessary when the field’s value +needs to be computed dynamically, fetched from an external service, or +otherwise requires custom logic. + +## Writing a custom resolver + +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 {6-8} +const UserType = new GraphQLObjectType({ + name: 'User', + fields: { + fullName: { + type: GraphQLString, + resolve(source) { + return `${source.firstName} ${source.lastName}`; + }, + }, + }, +}); +``` + +Resolvers can be synchronous or asynchronous. If a resolver returns a +Promise, GraphQL.js automatically waits for the Promise to resolve before +continuing execution: + +```js +resolve(source, args, context) { + return database.getUserById(args.id); +} +``` + +Custom resolvers are often used to implement patterns such as batching, +caching, or delegation. For example, a resolver might use a batching utility +like DataLoader to fetch multiple related records efficiently, or delegate +part of the query to another API or service. + +## Best practices + +When writing resolvers, it's important to keep them focused and maintainable: + +- Keep business logic separate. A resolver should focus on fetching or +computing the value for a field, not on implementing business rules or +complex workflows. Move business logic into separate service layers +whenever possible. +- Handle errors carefully. Resolvers should catch and handle errors +appropriately, either by throwing GraphQL errors or returning `null` values +when fields are nullable. Avoid letting unhandled errors crash the server. +- Use context effectively. Store shared per-request information, such as +authentication data or database connections, in the `context` object rather +than passing it manually between resolvers. +- Prefer batching over nested requests. For fields that trigger multiple +database or API calls, use batching strategies to minimize round trips and +improve performance. A common solution for batching in GraphQL is [dataloader](https://github.com/graphql/dataloader). +- Keep resolvers simple. Aim for resolvers to be small, composable functions +that are easy to read, test, and reuse. + +Following these practices helps keep your GraphQL server reliable, efficient, +and easy to maintain as your schema grows. \ No newline at end of file diff --git a/website/pages/running-an-express-graphql-server.mdx b/website/pages/docs/running-an-express-graphql-server.mdx similarity index 76% rename from website/pages/running-an-express-graphql-server.mdx rename to website/pages/docs/running-an-express-graphql-server.mdx index 5b8e201ec1..06ccf7d5cf 100644 --- a/website/pages/running-an-express-graphql-server.mdx +++ b/website/pages/docs/running-an-express-graphql-server.mdx @@ -16,15 +16,15 @@ Let's modify our “hello world” example so that it's an API server rather tha ```javascript -const { buildSchema } = require('graphql'); -const { createHandler } = require('graphql-http/lib/use/express'); -const express = require('express'); +import { buildSchema } from 'graphql'; +import { createHandler } from 'graphql-http/lib/use/express'; +import express from 'express'; // Construct a schema, using GraphQL schema language const schema = buildSchema(`type Query { hello: String } `); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { +// The root provides a resolver function for each API endpoint +const root = { hello() { return 'Hello world!'; }, @@ -45,31 +45,27 @@ app.all( app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); -```` +``` ```javascript -const { GraphQLObjectType, GraphQLSchema } = require('graphql'); -const { createHandler } = require('graphql-http/lib/use/express'); -const express = require('express'); +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { createHandler } from 'graphql-http/lib/use/express'; +import express from 'express'; // Construct a schema const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - hello: { type: GraphQLString }, + hello: { + type: GraphQLString, + resolve: () => 'Hello world!' + }, }, }), }); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { - hello() { - return 'Hello world!'; - }, -}; - const app = express(); // Create and use the GraphQL handler. @@ -77,16 +73,15 @@ app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); // Start the server at port app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); -```` - +``` + You can run this GraphQL server with: @@ -95,6 +90,8 @@ You can run this GraphQL server with: node server.js ``` +At this point you will have a running GraphQL API; but you can't just visit it in your web browser to use it - you need a GraphQL client to issue GraphQL queries to the API. Let's take a look at how to add the GraphiQL (GraphQL with an `i` in the middle) integrated development environment to your server. + ## Using GraphiQL [GraphiQL](https://github.com/graphql/graphiql) is GraphQL's IDE; a great way of querying and exploring your GraphQL API. @@ -102,7 +99,7 @@ One easy way to add it to your server is via the MIT-licensed [ruru](https://git To do so, install the `ruru` module with `npm install --save ruru` and then add the following to your `server.js` file, then restart the `node server.js` command: ```js -const { ruruHTML } = require('ruru/server'); +import { ruruHTML } from 'ruru/server'; // Serve the GraphiQL IDE. app.get('/', (_req, res) => { @@ -114,4 +111,4 @@ app.get('/', (_req, res) => { If you navigate to [http://localhost:4000](http://localhost:4000), you should see an interface that lets you enter queries; now you can use the GraphiQL IDE tool to issue GraphQL queries directly in the browser. -At this point you have learned how to run a GraphQL server. The next step is to learn how to [issue GraphQL queries from client code](/graphql-clients/). +At this point you have learned how to run a GraphQL server. The next step is to learn how to [issue GraphQL queries from client code](./graphql-clients). diff --git a/website/pages/docs/scaling-graphql.mdx b/website/pages/docs/scaling-graphql.mdx new file mode 100644 index 0000000000..7a29b313f4 --- /dev/null +++ b/website/pages/docs/scaling-graphql.mdx @@ -0,0 +1,161 @@ +--- +title: Scaling your GraphQL API +--- + +As your application grows, so does your GraphQL schema. What starts as a small, +self-contained monolith may eventually need to support multiple teams, services, and +domains. + +This guide introduces three common patterns for structuring GraphQL APIs at different +stages of scale: monolithic schemas, schema stitching, and federation. It also explains +how these patterns relate to GraphQL.js and what tradeoffs to consider as your +architecture evolves. + +## Monolithic schemas + +A monolithic schema is a single GraphQL schema served from a single service. All types, +resolvers, and business logic are located and deployed together. + +This is the default approach when using GraphQL.js. You define the entire schema in one +place using the `GraphQLSchema` constructor and expose it through a single HTTP endpoint. + +The following example defines a minimal schema that serves a single `hello` field: + +```js +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; + +const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLString, + resolve: () => 'Hello from a monolithic schema!', + }, + }, +}); + +export const schema = new GraphQLSchema({ query: QueryType }); +``` + +This structure works well for small to medium projects, especially when a single team owns the entire +graph. It's simple to test, deploy, and reason about. As long as the schema remains manageable +in size and scope, there's often no need to introduce additional architectural complexity. + +## Schema stitching + +As your application evolves, you may want to split your schema across modules or services while +still presenting a unified graph to clients. Schema stitching allows you to do this by merging +multiple schemas into one executable schema at runtime. + +GraphQL.js does not include stitching capabilities directly, but the +[`@graphql-tools/stitch`](https://the-guild.dev/graphql/stitching/docs/approaches) package +implements stitching features on top of GraphQL.js primitives. + +The following example stitches two subschemas into a single stitched schema: + +```js +import { stitchSchemas } from '@graphql-tools/stitch'; + +export const schema = stitchSchemas({ + subschemas: [ + { schema: userSchema }, + { schema: productSchema }, + ], +}); +``` + +Each subschema can be developed and deployed independently. The stitched schema handles query delegation, +merging, and resolution across them. + +Stitching is useful when: + +- Integrating existing GraphQL services behind a single endpoint +- Incrementally breaking up a monolithic schema +- Creating internal-only aggregators + +However, stitching can add runtime complexity and often requires manual conflict resolution for +overlapping types or fields. + +## Federation + +Federation is a distributed architecture that composes a single GraphQL schema from multiple independently +developed services, known as subgraphs. Each subgraph owns a portion of the schema and is responsible +for defining and resolving its fields. + +Unlike schema stitching, federation is designed for large organizations where teams need autonomy over +their part of the schema and services must be deployed independently. + +Federation introduces a set of conventions to coordinate between services. For example: + +- `@key` declares how an entity is identified across subgraphs +- `@external`, `@requires`, and `@provides` describe field-level dependencies across service boundaries + +Rather than merging schemas at runtime, federation uses a composition step to build the final schema. +A dedicated gateway routes queries to subgraphs and resolves shared entities. + +GraphQL.js does not provide built-in support for federation. To implement a federated subgraph using +GraphQL.js, you'll need to: + +- Add custom directives to the schema +- Implement resolvers for reference types +- Output a schema that conforms to a federation-compliant format + +Most federation tooling today is based on +[Apollo Federation](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/federation). +However, other approaches exist: + +- [GraphQL Mesh](https://the-guild.dev/graphql/mesh) allows federation-like composition across +services using plugins. +- Custom gateways and tooling can be implemented using GraphQL.js or other frameworks. +- The [GraphQL Composite Schemas WG](https://github.com/graphql/composite-schemas-wg/) (formed of Apollo, ChilliCream, The Guild, Netflix, Graphile and many more) are working on an open specification for the next generation of GraphQL Federation + +Federation is most useful when schema ownership is distributed and teams need to evolve their subgraphs +independently under a shared contract. + +## Choosing the right architecture + +The best structure for your GraphQL API depends on your team size, deployment model, and how your +schema is expected to grow. + +| Pattern | Best for | GraphQL.js support | Tooling required | +|---|---|---|---| +| Monolith | Default choice for most projects; simpler, faster, easier to reason about | Built-in | None | +| Schema stitching | Aggregating services you control | External tooling required | `@graphql-tools/stitch` +| Federation | Large enterprises; many teams contributing to a distributed graph independently | Manual implementation | Significant tooling and infrastructure | + +## Migration paths + +Architectural patterns aren't mutually exclusive. In many cases, teams evolve from one approach to another +over time. + +### Monolith to schema stitching + +Schema stitching can act as a bridge when breaking apart a monolithic schema. Teams can extract parts +of the schema into standalone services while maintaining a unified entry point. This allows for gradual +refactoring without requiring a full rewrite. + +### Stitching to federation + +Federation formalizes ownership boundaries and removes the need to manually coordinate overlapping types. +If schema stitching becomes difficult to maintain, federation can offer better scalability and governance. + +### Starting with federation + +Some teams choose to adopt federation early, particularly in large organizations with multiple backend +domains and team boundaries already in place. This can work well if you have the infrastructure and +experience to support it. + +## Guidelines + +The following guidelines can help you choose and evolve your architecture over time: + +- Start simple. If you're building a new API, a monolithic schema is usually the right place +to begin. It's easier to reason about, test, and iterate on. +- Split only when needed. Don't reach for composition tools prematurely. Schema stitching or federation +should be introduced in response to real organizational or scalability needs. +- Favor clarity over flexibility. Stitching and federation add power, but they also increase complexity. +Make sure your team has the operational maturity to support the tooling and patterns involved. +- Define ownership boundaries. Federation is most useful when teams need clear control over parts of +the schema. Without strong ownership models, a federated graph can become harder to manage. +- Consider alternatives. Not all use cases need stitching or federation. Sometimes, versioning, modular +schema design, or schema delegation patterns within a monolith are sufficient. \ No newline at end of file diff --git a/website/pages/docs/subscriptions.mdx b/website/pages/docs/subscriptions.mdx new file mode 100644 index 0000000000..7afeefed1b --- /dev/null +++ b/website/pages/docs/subscriptions.mdx @@ -0,0 +1,151 @@ +--- +title: Subscriptions +--- + +# Subscriptions + +Subscriptions allow a GraphQL server to push updates to clients in real time. Unlike queries and mutations, which use a request/response model, subscriptions maintain a long-lived connection between the client and server to stream data as events occur. This is useful for building features like chat apps, live dashboards, or collaborative tools that require real-time updates. + +This guide covers how to implement subscriptions in GraphQL.js, when to use them, and what to consider in production environments. + +## What is a subscription? + +A subscription is a GraphQL operation that delivers ongoing results to the client when a specific event happens. Unlike queries or mutations, which return a single response, a subscription delivers data over time through a persistent connection. + +GraphQL.js implements the subscription execution algorithm, but it's up to you to connect it to your event system and transport layer. Most implementations use WebSockets for transport, though any full-duplex protocol can work. + +## How execution works + +The core of subscription execution in GraphQL.js is the `subscribe` function. It works similarly to `graphql()`, but returns an `AsyncIterable` of execution results +instead of a single response: + +```js +import { subscribe, parse } from 'graphql'; +import schema from './schema.js'; + +const document = parse(` + subscription { + messageSent + } +`); + +const iterator = await subscribe({ schema, document }); + +for await (const result of iterator) { + console.log(result); +} +``` + +Each time your application publishes a new `messageSent` event, the iterator emits a new result. It's up to your transport layer to manage the connection and forward these updates to the client. + +## When to use subscriptions + +Subscriptions are helpful when your application needs to respond to real-time events. For example: + +- Receiving new messages in a chat +- Updating a live feed or activity stream +- Displaying presence indicators (e.g., "user is online") +- Reflecting real-time price changes + +If real-time delivery isn’t essential, consider using polling with queries instead. Subscriptions require more infrastructure and introduce additional complexity, especially around scaling and connection management. + +## Implementing subscriptions in GraphQL.js + +GraphQL.js supports subscription execution, but you’re responsible for setting up the transport and event system. At a minimum, you’ll need: + +- A `Subscription` root type in your schema +- A `subscribe` resolver that returns an `AsyncIterable` +- An event-publishing mechanism +- A transport layer to maintain the connection + +The following examples use the [`graphql-subscriptions`](https://github.com/apollographql/graphql-subscriptions) package to set up an in-memory event system. + +### Install dependencies + +Start by installing the necessary packages: + +```bash +npm install graphql graphql-subscriptions +``` + +To serve subscriptions over a network, you’ll also need a transport implementation. One option is [`graphql-ws`](https://github.com/enisdenjo/graphql-ws), a community-maintained WebSocket library. This guide focuses on schema-level implementation. + +### Set up a pub/sub instance + +Create a `PubSub` instance to manage your in-memory event system: + +```js +import { PubSub } from 'graphql-subscriptions'; + +const pubsub = new PubSub(); +``` + +This `pubsub` object provides `publish` and `asyncIterator` methods, allowing +you to broadcast and listen for events. + +### Define a subscription type + +Next, define your `Subscription` root type. Each field on this type should return +an `AsyncIterable`. Subscribe functions can also accept standard resolver arguments +such as `args`, `context`, and `info`, depending on your use case: + +```js +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; + +const SubscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { + messageSent: { + type: GraphQLString, + subscribe: () => pubsub.asyncIterator(['MESSAGE_SENT']), + }, + }, +}); +``` + +This schema defines a `messageSent` field that listens for the `MESSAGE_SENT` event and returns a string. + +### Publish events + +You can trigger a subscription event from any part of your application using the +`publish` method: + +```js +pubsub.publish('MESSAGE_SENT', { messageSent: 'Hello world!' }); +``` + +Clients subscribed to the `messageSent` field will receive this message. + +### Construct your schema + +Finally, build your schema and include the `Subscription` type: + +```js +const schema = new GraphQLSchema({ + subscription: SubscriptionType, +}); +``` + +A client can then initiate a subscription like this: + +```graphql +subscription { + messageSent +} +``` + +Whenever your server publishes a `MESSAGE_SENT` event, clients subscribed to +`messageSent` will receive the updated value over their active connection. + +## Planning for production + +The in-memory `PubSub` used in this example is suitable for development only. +It does not support multiple server instances or distributed environments. + +For production, consider using a more robust event system such as: + +- Redis Pub/Sub +- Message brokers like Kafka or NATS +- Custom implementations with persistent queues or durable event storage + +Subscriptions also require careful handling of connection limits, authentication, rate limiting, and network reliability. These responsibilities fall to your transport layer and infrastructure. diff --git a/website/pages/docs/type-generation.mdx b/website/pages/docs/type-generation.mdx new file mode 100644 index 0000000000..fdcaf8e3a4 --- /dev/null +++ b/website/pages/docs/type-generation.mdx @@ -0,0 +1,263 @@ +--- +title: Type Generation for GraphQL +sidebarTitle: Type Generation +--- + +# Type Generation for GraphQL + +Writing a GraphQL server in JavaScript or TypeScript often involves managing complex +types. As your API grows, keeping these types accurate and aligned with your schema +becomes increasingly difficult. + +Type generation tools automate this process. Instead of manually defining or maintaining +TypeScript types for your schema and operations, these tools can generate them for you. +This improves safety, reduces bugs, and makes development easier to scale. + +This guide walks through common type generation workflows for projects using +`graphql-js`, including when and how to use them effectively. + +## Why use type generation? + +Type generation improves reliability and developer experience across the development +lifecycle. It's especially valuable when: + +- You want strong type safety across your server logic +- Your schema is defined separately in SDL files +- Your API surface is large, rapidly evolving, or used by multiple teams +- You rely on TypeScript for editor tooling, autocomplete, or static analysis + +By generating types directly from your schema, you can avoid drift between schema +definitions and implementation logic. + +## Code-first development + +In a code-first workflow, the schema is constructed entirely in JavaScript or TypeScript +using `graphql-js` constructors like `GraphQLObjectType`, `GraphQLSchema`, and others. +This approach is flexible and lets you build your schema programmatically using native +language features. + +If you're using this approach with TypeScript, you already get some built-in type safety +with the types exposed by `graphql-js`. For example, TypeScript can help ensure your resolver +functions return values that match their expected shapes. + +However, code-first development has tradeoffs: + +- You won't get automatic type definitions for your resolvers unless you generate +them manually or infer them through wrappers. +- Schema documentation, testing, and tool compatibility may require you to provide + a description of the schema in SDL first. + +You can still use type generation tools like GraphQL Code Generator in a code-first setup. +You just need to convert your schema into SDL. + +To produce an SDL description of your schema: + +```ts +import { printSchema } from 'graphql'; +import { schema } from './schema'; +import { writeFileSync } from 'fs'; + +writeFileSync('./schema.graphql', printSchema(schema)); +``` + +Once you've written the SDL, you can treat the project like an SDL-first project +for type generation. + +## Schema-first development + +In a schema-first workflow, your GraphQL schema is written in SDL, for example, `.graphql` +or `.gql` (discouraged) files. This serves as the source of truth for your server. This approach +emphasizes clarity because your schema is defined independently from your business logic. + +Schema-first development pairs well with type generation because the schema is +serializable and can be directly used by tools like [GraphQL Code Generator](https://the-guild.dev/graphql/codegen). + +With a schema-first workflow, you can: + +- Generate resolver type definitions and files that match your schema +- Generate operation types for client queries, integration tests, or internal tooling +- Detect breaking changes and unused types through schema diffing tools + +## Generating resolver types + +To get started, install the required packages: + +```bash +npm install graphql @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files +``` + +This scoped package is published by a community maintainer and is widely used for GraphQL server +type generation. + +We recommend using the [Server Preset](https://www.npmjs.com/package/@eddeee888/gcg-typescript-resolver-files) for a +managed workflow. It automatically generates types and files based on your schema without needing extra plugin setup. + +The Server Preset generates: + +- Resolver types, including parent types, arguments, return values, and context +- Resolver files with types wired up, ready for your business logic +- A resolver map and type definitions to plug into GraphQL servers + +This setup expects your schema is split into modules to improve readability and maintainability. For example: + +```text +├── src/ +│ ├── schema/ +│ │ ├── base/ +│ │ │ ├── schema.graphql +│ │ ├── user/ +│ │ │ ├── schema.graphql +│ │ ├── book/ +│ │ │ ├── schema.graphql +``` + +Here's an example `codegen.ts` file using the Server Preset: + +```ts filename="codegen.ts" +import type { CodegenConfig } from "@graphql-codegen/cli"; +import { defineConfig } from "@eddeee888/gcg-typescript-resolver-files"; + +const config: CodegenConfig = { + schema: "src/**/schema.graphql", + generates: { + "src/schema": defineConfig({ + resolverGeneration: "minimal", + }), + }, +}; + +export default config; +``` + +To generate the resolver types and files, run: + +```bash +npx graphql-codegen +``` + +This creates resolver files like: + +```ts filename="src/schema/user/resolvers/Query/user.ts" +import type { QueryResolvers } from "./../../../types.generated"; + +export const user: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + // Implement Query.user resolver logic here +}; +``` + +The user query resolver is typed to ensure that the user resolver expects an id argument and returns a +User, giving you confidence and autocomplete while implementing your server logic, which may look like this: + +```ts filename="src/schema/user/resolvers/Query/user.ts" +export const user: NonNullable = async ( + parent, + args, + context, +) => { + return context.db.getUser(args.id); +}; +``` + +See the official [Server Preset guide](https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset) to learn about its other features, including mappers convention and static analysis for runtime safety. + +## Generating operation types + +In addition to resolver types, you can generate types for GraphQL operations such as queries, mutations, and +fragments. This is especially useful for shared integration tests or client logic that needs to match the schema +precisely. + +To get started, install the required packages: + +```bash +npm install graphql @graphql-codegen/cli +``` + +We recommend using the GraphQL Code Generator [Client Preset](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client) for a managed workflow: + +- Write operations with GraphQL syntax in the same file where it is used +- Get type-safety when using the result + +Here's an example configuration using the Client Preset: + +```ts filename="codegen.ts" +import type { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + schema: "src/**/schema.graphql", + documents: ["src/**/*.ts"], + ignoreNoDocuments: true, + generates: { + "./src/graphql/": { + preset: "client", + config: { + documentMode: "string", + }, + }, + }, +}; + +export default config; +``` + +To keep generated types up to date as you edit your code, run the generator in watch mode: + +```bash +npx graphql-codegen --config codegen.ts --watch +``` + +Once generated, import the `graphql` function from `src/graphql/` to write GraphQL operations +directly in your TypeScript files: + +```ts filename="src/index.ts" +import { graphql } from "./graphql"; + +const UserQuery = graphql(` + query User($id: ID!) { + user(id: ID!) { + id + fullName + } + } +`); + +const response = await fetch("https://graphql.org/graphql/", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/graphql-response+json", + }, + body: JSON.stringify({ + query: UserQuery, + variables: { id: "1" }, + }), +}); + +if (!response.ok) { + throw new Error("Network response was not ok"); +} + +const result: ResultOf = await response.json(); + +console.log(result); +``` + +For guides on using the Client Preset with popular frameworks and tools, see: + +- [Vanilla TypeScript](https://the-guild.dev/graphql/codegen/docs/guides/vanilla-typescript) +- [React Query](https://the-guild.dev/graphql/codegen/docs/guides/react-query) +- [React / Vue](https://the-guild.dev/graphql/codegen/docs/guides/react-vue) + +## Best practices for CI and maintenance + +To keep your type generation reliable and consistent: + +- Check in generated files to version control so teammates and CI systems don't produce +divergent results. +- Run type generation in CI to ensure types stay in sync with schema changes. +- Use schema diffing tools like `graphql-inspector` to catch breaking changes before +they're merged. +- Automate regeneration with pre-commit hooks, GitHub Actions, or lint-staged workflows. \ No newline at end of file diff --git a/website/pages/docs/using-directives.mdx b/website/pages/docs/using-directives.mdx new file mode 100644 index 0000000000..f45f02b204 --- /dev/null +++ b/website/pages/docs/using-directives.mdx @@ -0,0 +1,275 @@ +--- +title: Using Directives in GraphQL.js +sidebarTitle: Using Directives +--- + +# Using Directives in GraphQL.js + +Directives let you customize query execution at a fine-grained level. They act like +annotations in a GraphQL document, giving the server instructions about whether to +include a field, how to format a response, or how to apply custom behavior. + +GraphQL.js supports built-in directives like `@include`, `@skip`, and `@deprecated` out +of the box. If you want to create your own directives and apply custom behavior, you'll +need to implement the logic yourself. + +This guide covers how GraphQL.js handles built-in directives, how to define and apply +custom directives, and how to implement directive behavior during execution. + +## How GraphQL.js handles built-in directives + +GraphQL defines several built-in directives, each serving a specific purpose during +execution or in the schema. These include: + +- `@include` and `@skip`: Used in the execution language to conditionally include or skip +fields and fragments at runtime. +- `@deprecated`: Used in Schema Definition Language (SDL) to mark fields or enum values as +deprecated, with an optional reason. It appears in introspection but doesn't affect query execution. + +For example, the `@include` directive conditionally includes a field based on a Boolean variable: + +```graphql +query($shouldInclude: Boolean!) { + greeting @include(if: $shouldInclude) +} +``` + +At runtime, GraphQL.js evaluates the `if` argument. If `shouldInclude` is `false`, the +`greeting` field in this example is skipped entirely and your resolver won't run. + +```js +import { graphql, buildSchema } from 'graphql'; + +const schema = buildSchema(` + type Query { + greeting: String + } +`); + +const rootValue = { + greeting: () => 'Hello!', +}; + +const query = ` + query($shouldInclude: Boolean!) { + greeting @include(if: $shouldInclude) + } +`; + +const variables = { shouldInclude: true }; + +const result = await graphql({ + schema, + source: query, + rootValue, + variableValues: variables, +}); + +console.log(result); +// → { data: { greeting: 'Hello!' } } +``` + +If `shouldInclude` is `false`, the result would be `{ data: {} }`. + +The `@deprecated` directive is used in the schema to indicate that a field or enum +value should no longer be used. It doesn't affect execution, but is included +in introspection output: + +```graphql +{ + __type(name: "MyType") { + fields { + name + isDeprecated + deprecationReason + } + } +} +``` + +GraphQL.js automatically includes deprecation metadata in introspection. Tools like +GraphiQL use this to show warnings, but GraphQL.js itself doesn't block or modify behavior. +You can still query deprecated fields unless you add validation rules yourself. + +## Declaring custom directives in GraphQL.js + +To use a custom directive, you first define it in your schema using the +`GraphQLDirective` class. This defines the directive's name, where it can +be applied, and any arguments it accepts. + +A directive in GraphQL.js is just metadata. It doesn't perform any behavior on its own. + +Here's a basic example that declares an `@uppercase` directive that can be applied to fields: + +```js +import { + GraphQLDirective, + DirectiveLocation, + GraphQLNonNull, + GraphQLBoolean, +} from 'graphql'; + +const UppercaseDirective = new GraphQLDirective({ + name: 'uppercase', + description: 'Converts the result of a field to uppercase.', + locations: [DirectiveLocation.FIELD], + args: { + enabled: { + type: GraphQLNonNull(GraphQLBoolean), + defaultValue: true, + description: 'Whether to apply the transformation.', + }, + }, +}); +``` + +To make the directive available to your schema, you must explicitly include it: + +```js +import { GraphQLSchema } from 'graphql'; + +const schema = new GraphQLSchema({ + query: QueryType, + directives: [UppercaseDirective], +}); +``` + +Once added, tools like validation and introspection will recognize it. + +## Applying directives in queries + +After defining and adding your directive to the schema, clients can apply it in queries using +the `@directiveName` syntax. Arguments are passed in parentheses, similar to field arguments. + +You can apply directives to: + +- Fields +- Fragment spreads +- Inline fragments + +The following examples show how to apply directives: + +```graphql +# Applied to a field +{ + greeting @uppercase +} +``` + +```graphql +# Applied to a fragment spread +{ + ...userFields @include(if: true) +} +``` + +```graphql +# Applied to an inline fragment +{ + ... on User @skip(if: false) { + email + } +} +``` + +When a query is parsed, GraphQL.js includes directive nodes in the field's +Abstract Syntax Tree (AST). You can access these via `info.fieldNodes` inside +a resolver. + +## Implementing custom directive behavior + +GraphQL.js doesn't execute custom directive logic for you. You must handle it during +execution. There are two common approaches: + +### 1. Handle directives in resolvers + +Inside a resolver, use the `info` object to access AST nodes and inspect directives. +You can check whether a directive is present and change behavior accordingly. + +```js +import { + graphql, + buildSchema, + getDirectiveValues, +} from 'graphql'; + +const schema = buildSchema(` + directive @uppercase(enabled: Boolean = true) on FIELD + + type Query { + greeting: String + } +`); + +const rootValue = { + greeting: (source, args, context, info) => { + const directive = getDirectiveValues( + schema.getDirective('uppercase'), + info.fieldNodes[0], + info.variableValues + ); + + const result = 'Hello, world'; + + if (directive?.enabled) { + return result.toUpperCase(); + } + + return result; + }, +}; + +const query = ` + query { + greeting @uppercase + } +`; + +const result = await graphql({ schema, source: query, rootValue }); +console.log(result); +// → { data: { greeting: 'HELLO, WORLD' } } +``` + +### 2. Use AST visitors or schema wrappers + +For more complex logic, you can preprocess the schema or query using AST visitors or wrap +field resolvers. This lets you inject directive logic globally across +multiple types or fields. + +This approach is useful for: + +- Authorization +- Logging +- Schema transformations +- Feature flags + +## Use cases for custom directives + +Some common use cases for custom directives include: + +- **Formatting**: `@uppercase`, `@date(format: "YYYY-MM-DD")`, `@currency` +- **Authorization**: `@auth(role: "admin")` to protect fields +- **Feature flags**: `@feature(name: "newHeader")` to expose experimental features +- **Observability**: `@log`, `@tag(name: "important")`, or `@metrics(id: "xyz")` +- **Execution control**: Mask or transform fields at runtime with schema visitors + +## Best practices + +When working with custom directives in GraphQL.js, keep the following best practices in mind: + +- GraphQL.js doesn't have a directive middleware system. All custom directive logic must be implemented +manually. +- Weigh schema-driven logic against resolver logic. Directives can make queries more expressive, but they +may also hide behavior from developers reading the schema or resolvers. +- Keep directive behavior transparent and debuggable. Since directives are invisible at runtime unless +logged or documented, try to avoid magic behavior. +- Use directives when they offer real value. Avoid overusing directives to replace things that could be +handled more clearly in schema design or resolvers. +- Validate directive usage explicitly if needed. If your directive has rules or side effects, consider +writing custom validation rules to enforce correct usage. + +## Additional resources + +- [GraphQL Specification: Directives](https://spec.graphql.org/draft/#sec-Language.Directives) +- The Guild's guide on [Schema Directives](https://the-guild.dev/graphql/tools/docs/schema-directives) +- Apollo Server's guide on [Directives](https://www.apollographql.com/docs/apollo-server/schema/directives) \ No newline at end of file diff --git a/website/pages/going-to-production.mdx b/website/pages/going-to-production.mdx deleted file mode 100644 index 862932fb10..0000000000 --- a/website/pages/going-to-production.mdx +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Going to Production ---- - -GraphQL.JS contains a few development checks which in production will cause slower performance and -an increase in bundle-size. Every bundler goes about these changes different, in here we'll list -out the most popular ones. - -## Bundler-specific configuration - -Here are some bundler-specific suggestions for configuring your bundler to remove `globalThis.process` and `process.env.NODE_ENV` on build time. - -### Vite - -```js -export default defineConfig({ - // ... - define: { - 'globalThis.process': JSON.stringify(true), - 'process.env.NODE_ENV': JSON.stringify('production'), - }, -}); -``` - -### Next.js - -```js -// ... -/** @type {import('next').NextConfig} */ -const nextConfig = { - webpack(config, { webpack }) { - config.plugins.push( - new webpack.DefinePlugin({ - 'globalThis.process': JSON.stringify(true), - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - ); - return config; - }, -}; - -module.exports = nextConfig; -``` - -### create-react-app - -With `create-react-app`, you need to use a third-party package like [`craco`](https://craco.js.org/) to modify the bundler configuration. - -```js -const webpack = require('webpack'); -module.exports = { - webpack: { - plugins: [ - new webpack.DefinePlugin({ - 'globalThis.process': JSON.stringify(true), - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - ], - }, -}; -``` - -### esbuild - -```json -{ - "define": { - "globalThis.process": true, - "process.env.NODE_ENV": "production" - } -} -``` - -### Webpack - -```js -config.plugins.push( - new webpack.DefinePlugin({ - 'globalThis.process': JSON.stringify(true), - 'process.env.NODE_ENV': JSON.stringify('production'), - }), -); -``` - -### Rollup - -```js -export default [ - { - // ... input, output, etc. - plugins: [ - minify({ - mangle: { - toplevel: true, - }, - compress: { - toplevel: true, - global_defs: { - '@globalThis.process': JSON.stringify(true), - '@process.env.NODE_ENV': JSON.stringify('production'), - }, - }, - }), - ], - }, -]; -``` - -### SWC - -```json filename=".swcrc" -{ - "jsc": { - "transform": { - "optimizer": { - "globals": { - "vars": { - "globalThis.process": true, - "process.env.NODE_ENV": "production" - } - } - } - } - } -} -``` diff --git a/website/pages/index.mdx b/website/pages/index.mdx deleted file mode 100644 index 4c8fb78b56..0000000000 --- a/website/pages/index.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Overview -sidebarTitle: Overview ---- - -GraphQL.JS is the reference implementation to the [GraphQL Specification](https://spec.graphql.org/draft/), it's designed to be simple to use and easy to understand -while closely following the Specification. - -You can build GraphQL servers, clients, and tools with this library, it's designed so you can choose which parts you use, for example, you can build your own parser -and use the execution/validation from the library. There also a lot of useful utilities for schema-diffing, working with arguments and [many more](./utilities.mdx). - -In the following chapters you'll find out more about the three critical pieces of this library - -- The GraphQL language -- Document validation -- GraphQL Execution - -You can also code along on [a tutorial](./getting-started.mdx). diff --git a/website/pages/upgrade-guides/v16-v17.mdx b/website/pages/upgrade-guides/v16-v17.mdx new file mode 100644 index 0000000000..00b8a27343 --- /dev/null +++ b/website/pages/upgrade-guides/v16-v17.mdx @@ -0,0 +1,187 @@ +--- +title: Upgrading from v16 to v17 +sidebarTitle: v16 to v17 +--- + +import { Tabs } from 'nextra/components'; +import { Callout } from 'nextra/components' + + + Currently GraphQL v17 is in alpha, this guide is based on the alpha release and is subject to change. + + +# Breaking changes + +## Default values + +GraphQL schemas allow default values for input fields and arguments. Historically, GraphQL.js did not rigorously validate or coerce these +defaults during schema construction, leading to potential runtime errors or inconsistencies. For example: + +- A default value of "5" (string) for an Int-type argument would pass schema validation but fail at runtime. +- Internal serialization methods like astFromValue could produce invalid ASTs if inputs were not properly coerced. + +With the new changes default values will be validated and coerced during schema construction. + +```graphql +input ExampleInput { + value: Int = "invalid" # Now triggers a validation error +} +``` + +This goes hand-in-hand with the deprecation of `astFromValue` in favor of `valueToLiteral` or `default: { value: }`. + +```ts +// Before (deprecated) +const defaultValue = astFromValue(internalValue, type); +// After +const defaultValue = valueToLiteral(externalValue, type); +``` + +If you want to continue using the old behavior, you can use `defaultValue` in your schema definitions. The new +behavior will be exposed as `default: { literal: }`. + +## GraphQLError constructor arguments + +The `GraphQLError` constructor now only accepts a message and options object as arguments. Previously, it also accepted positional arguments. + +```diff +- new GraphQLError('message', 'source', 'positions', 'path', 'originalError', 'extensions'); ++ new GraphQLError('message', { source, positions, path, originalError, extensions }); +``` + +## `createSourceEventStream` arguments + +The `createSourceEventStream` function now only accepts an object as an argument. Previously, it also accepted positional arguments. + +```diff +- createSourceEventStream(schema, document, rootValue, contextValue, variableValues, operationName); ++ createSourceEventStream({ schema, document, rootValue, contextValue, variableValues, operationName }); +``` + +## `execute` will error for incremental delivery + +The `execute` function will now throw an error if it sees a `@defer` or `@stream` directive. Use `experimentalExecuteIncrementally` instead. +If you know you are dealing with incremental delivery requests, you can replace the import. + +```diff +- import { execute } from 'graphql'; ++ import { experimentalExecuteIncrementally as execute } from 'graphql'; +``` + +## Remove incremental delivery support from `subscribe` + +In case you have fragments that you use with `defer/stream` that end up in a subscription, +use the `if` argument of the directive to disable it in your subscription operation + +## `subscribe` return type + +The `subscribe` function can now also return a non-Promise value, previously this was only a Promise. +This shouldn't change a lot as `await value` will still work as expected. This could lead to +some typing inconsistencies though. + +## Remove `singleResult` from incremental results + +You can remove branches that check for `singleResult` in your code, as it is no longer used. + +## Node support + +Dropped support for Node 14 (subject to change) + +## Removed `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types + +We have removed the `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types, +use `Kind`, `TokenKind` and `DirectiveLocation` instead. https://github.com/graphql/graphql-js/pull/3579 + +## Removed `graphql/subscription` module + +use `graphql/execution` instead for subscriptions, all execution related exports have been +unified there. + +## Removed `GraphQLInterfaceTypeNormalizedConfig` export + +Use `ReturnType` if you really need this + +## Empty AST collections will be undefined + +Empty AST collections will be presented by `undefined` rather than an empty array. + +## `Info.variableValues` + +The shape of `Info.variableValues` has changed to be an object containing +`sources` and `coerced` as keys. + +A Source contains the `signature` and provided `value` pre-coercion for the +variable. A `signature` is an object containing the `name`, `input-type` and +`defaultValue` for the variable. + +## Stream directive can't be on multiple instances of the same field + +The `@stream` directive can't be on multiple instances of the same field, +this won't pass `validate` anymore. + +See https://github.com/graphql/graphql-js/pull/4342 + +## Stream initialCount becomes non-nullable + +The `initialCount` argument of the `@stream` directive is now non-nullable. + +See https://github.com/graphql/graphql-js/pull/4322 + +## GraphQLSchemas converted to configuration may no longer be assumed valid + +The `assumeValid` config property exported by the `GraphQLSchema.toConfig()` method now passes through the original +flag passed on creation of the `GraphQLSchema`. +Previously, the `assumeValid` property would be to `true` if validation had been run, potentially concealing the original intent. + +See https://github.com/graphql/graphql-js/pull/4244 and https://github.com/graphql/graphql-js/issues/3448 + +## `coerceInputValue` returns `undefined` on error + +`coerceInputValue` now aborts early when an error occurs, to optimize execution speed on the happy path. +Use the `validateInputValue` helper to retrieve the actual errors. + +## Removals + +- Removed deprecated `getOperationType` function, use `getRootType` on the `GraphQLSchema` instance instead +- Removed deprecated `getVisitFn` function, use `getEnterLeaveForKind` instead +- Removed deprecated `printError` and `formatError` utilities, you can use `toString` or `toJSON` on the error as a replacement +- Removed deprecated `assertValidName` and `isValidNameError` utilities, use `assertName` instead +- Removed deprecated `assertValidExecutionArguments` function, use `assertValidSchema` instead +- Removed deprecated `getFieldDefFn` from `TypeInfo` +- Removed deprecated `TypeInfo` from `validate` https://github.com/graphql/graphql-js/pull/4187 + +## Deprecations + +- Deprecated `astFromValue` use `valueToLiteral` instead, when leveraging `valueToLiteral` ensure + that you are working with externally provided values i.e. the SDL provided defaultValue to a variable. +- Deprecated `valueFromAST` use `coerceInputLiteral` instead +- Deprecated `findBreakingChanges()` and `findDangerousChanges()`. Use `findSchemaChanges()` instead, which can also be used to find safe changes. +- Deprecated `serialize`. `parseValue`, and `parseLiteral` properties on scalar type configuration. Use `coerceOutputValue`, `coerceInputValue`, and `coerceInputLiteral` instead. + +## Experimental Features + +### Experimental Support for Incremental Delivery + +- [Spec PR](https://github.com/graphql/graphql-spec/pull/1110) / [RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md) +- enabled only when using `experimentalExecuteIncrementally()`, use of a schema or operation with `@defer`/`@stream` directives within `execute()` will now throw. +- enable early execution with the new `enableEarlyExecution` configuration option for `experimentalExecuteIncrementally()`. + +### Experimental Support for Fragment Arguments + +- [Spec PR](https://github.com/graphql/graphql-spec/pull/1081) +- enable with the new `experimentalFragmentArguments` configuration option for `parse()`. +- new experimental `Kind.FRAGMENT_ARGUMENT` for visiting +- new experimental `TypeInfo` methods and options for handling fragment arguments. +- coerce AST via new function `coerceInputLiteral()` with experimental fragment variables argument (as opposed to deprecated `valueFromAST()` function). + +## Features + +- Added `hideSuggestions` option to `execute`/`validate`/`subscribe`/... to hide schema-suggestions in error messages +- Added `abortSignal` option to `graphql()`, `execute()`, and `subscribe()` allows cancellation of these methods; + the `abortSignal` can also be passed to field resolvers to cancel asynchronous work that they initiate. +- `extensions` support `symbol` keys, in addition to the normal string keys. +- Added ability for resolver functions to return async iterables. +- Added `perEventExecutor` execution option to allows specifying a custom executor for subscription source stream events, which can be useful for preparing a per event execution context argument. +- Added `validateInputValue` and `validateInputLiteral` helpers to validate input values and literals, respectively. +- Added `replaceVariableValues` helper to replace variables within complex scalars uses as inputs. Internally, this allows variables embedded within complex scalars to finally use the correct default values. +- Added new `printDirective` helper. diff --git a/website/tailwind.config.js b/website/tailwind.config.js index 384ecc396e..6cf5041653 100644 --- a/website/tailwind.config.js +++ b/website/tailwind.config.js @@ -4,7 +4,6 @@ const config = { content: [ './pages/**/*.{ts,tsx,mdx}', './icons/**/*.{ts,tsx,mdx}', - './css/**/*.css', './theme.config.tsx', ], theme: { diff --git a/website/vercel.json b/website/vercel.json new file mode 100644 index 0000000000..9f11ce21b7 --- /dev/null +++ b/website/vercel.json @@ -0,0 +1,14 @@ +{ + "redirects": [ + { + "source": "/api", + "destination": "/api-v16/graphql", + "permanent": true + }, + { + "source": "/", + "destination": "/docs", + "permanent": true + } + ] +}