From 5561f400812173eee6d81b453235fd651ba004c5 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Thu, 29 May 2025 10:52:39 -0400 Subject: [PATCH 1/5] docs: add guide for operation complexity controls (#4402) Adds guide "Operation Complexity Controls" --------- Co-authored-by: Benjie --- cspell.yml | 3 +- website/pages/docs/_meta.ts | 1 + .../docs/operation-complexity-controls.mdx | 264 ++++++++++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 website/pages/docs/operation-complexity-controls.mdx diff --git a/cspell.yml b/cspell.yml index efaf7a3048..3870f6f531 100644 --- a/cspell.yml +++ b/cspell.yml @@ -39,6 +39,8 @@ overrides: - URQL - tada - Graphile + - precompiled + - debuggable validateDirectives: true ignoreRegExpList: @@ -186,4 +188,3 @@ words: - overcomplicating - cacheable - pino - - debuggable diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 094e286afd..97c5bc2b2e 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -27,6 +27,7 @@ const meta = { 'cursor-based-pagination': '', 'custom-scalars': '', 'advanced-custom-scalars': '', + 'operation-complexity-controls': '', 'n1-dataloader': '', 'caching-strategies': '', 'resolver-anatomy': '', diff --git a/website/pages/docs/operation-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx new file mode 100644 index 0000000000..9aff4dc67e --- /dev/null +++ b/website/pages/docs/operation-complexity-controls.mdx @@ -0,0 +1,264 @@ +--- +title: Operation Complexity Controls +--- + +import { Callout } from 'nextra/components' + +# Operation Complexity Controls + +GraphQL gives clients a lot of flexibility to shape responses, but that +flexibility can also introduce risk. Clients can request deeply nested fields or +large volumes of data in a single operation. Without controls, these operations can slow +down your server or expose security vulnerabilities. + +This guide explains how to measure and limit operation complexity in GraphQL.js +using static analysis. You'll learn how to estimate the cost +of an operation before execution and reject it if it exceeds a safe limit. + + + In production, we recommend using [trusted documents](/docs/going-to-production#only-allow-trusted-documents) + rather than analyzing arbitrary documents at runtime. Complexity analysis can still be + useful at build time to catch expensive operations before they're deployed. + + +## Why complexity control matters + +GraphQL lets clients choose exactly what data they want. That flexibility is powerful, +but it also makes it hard to predict the runtime cost of a query just by looking +at the schema. + +Without safeguards, clients could: + +- Request deeply nested object relationships +- Use nested fragments to multiply field resolution +- Exploit pagination arguments to retrieve excessive data + +Certain field types (e.g., lists, interfaces, unions) can also significantly +increase cost by multiplying the number of values returned or resolved. + +Complexity controls help prevent these issues. They allow you to: + +- Protect your backend from denial-of-service attacks or accidental load +- Enforce cost-based usage limits between clients or environments +- Detect expensive queries early in development +- Add an additional layer of protection alongside authentication, depth limits, and validation + +For more information, see [Security best practices](https://graphql.org/learn/security/). + +## Estimating operation cost + +To measure a query's complexity, you typically: + +1. Parse the incoming operation into a GraphQL document. +2. Walk the query's Abstract Syntax Tree (AST), which represents its structure. +3. Assign a cost to each field, often using static heuristics or metadata. +4. Reject or log the operation if it exceeds a maximum allowed complexity. + +You can do this using custom middleware or validation rules that run before execution. +No resolvers are called unless the operation passes these checks. + + + Fragment cycles or deep nesting can cause some complexity analyzers to perform + poorly or get stuck. Always run complexity analysis after validation unless your analyzer + explicitly handles cycles safely. + + +## Simple complexity calculation + +There are several community-maintained tools for complexity analysis. The examples in this +guide use [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity) as +an option, but we recommend choosing the approach that best fits your project. + +Here's a basic example using its `simpleEstimator`, which assigns a flat cost to every field: + +```js +import { parse } from 'graphql'; +import { getComplexity, simpleEstimator } from 'graphql-query-complexity'; +import { schema } from './schema.js'; + +const query = ` + query { + users { + id + name + posts { + id + title + } + } + } +`; + +const complexity = getComplexity({ + schema, + query: parse(query), + estimators: [simpleEstimator({ defaultComplexity: 1 })], + variables: {}, +}); + +if (complexity > 100) { + throw new Error(`Query is too complex: ${complexity}`); +} + +console.log(`Query complexity: ${complexity}`); +``` + +In this example, every field costs 1 point. The total complexity is the number of fields, +adjusted for nesting and fragments. The complexity is calculated before execution begins, +allowing you to reject costly operations early. + +## Custom cost estimators + +Some fields are more expensive than others. For example, a paginated list might be more +costly than a scalar field. You can define per-field costs using +`fieldExtensionsEstimator`, a feature supported by some complexity tools. + +This estimator reads cost metadata from the field's `extensions.complexity` function in +your schema. For example: + +```js +import { GraphQLObjectType, GraphQLList, GraphQLInt } from 'graphql'; +import { PostType } from './post-type.js'; + +const UserType = new GraphQLObjectType({ + name: 'User', + fields: { + posts: { + type: GraphQLList(PostType), + args: { + limit: { type: GraphQLInt }, + }, + extensions: { + complexity: ({ args, childComplexity }) => { + const limit = args.limit ?? 10; + return childComplexity * limit; + }, + }, + }, + }, +}); +``` + +In this example, the cost of `posts` depends on the number of items requested (`limit`) and the +complexity of each child field. + + + Most validation steps don't have access to variable values. If your complexity + calculation depends on variables (like `limit`), make sure it runs after validation, not + as part of it. + + +To evaluate the cost before execution, you can combine estimators like this: + +```js +import { parse } from 'graphql'; +import { + getComplexity, + simpleEstimator, + fieldExtensionsEstimator, +} from 'graphql-query-complexity'; +import { schema } from './schema.js'; + +const query = ` + query { + users { + id + posts(limit: 5) { + id + title + } + } + } +`; + +const document = parse(query); + +const complexity = getComplexity({ + schema, + query: document, + variables: {}, + estimators: [ + fieldExtensionsEstimator(), + simpleEstimator({ defaultComplexity: 1 }), + ], +}); + +console.log(`Query complexity: ${complexity}`); +``` + +Estimators are evaluated in order. The first one to return a numeric value is used +for a given field. This lets you define detailed logic for specific fields and fall back +to a default cost elsewhere. + +## Enforcing limits in your server + +Some tools allow you to enforce complexity limits during validation by adding a rule +to your GraphQL.js server. For example, `graphql-query-complexity` provides a +`createComplexityRule` helper: + +```js +import { graphql, specifiedRules, parse } from 'graphql'; +import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity'; +import { schema } from './schema.js'; + +const source = ` + query { + users { + id + posts { + title + } + } + } +`; + +const document = parse(source); + +const result = await graphql({ + schema, + source, + validationRules: [ + ...specifiedRules, + createComplexityRule({ + estimators: [simpleEstimator({ defaultComplexity: 1 })], + maximumComplexity: 50, + onComplete: (complexity) => { + console.log('Query complexity:', complexity); + }, + }), + ], +}); + +console.log(result); +``` + + + Only use complexity rules in validation if you're sure the analysis is cycle-safe. + Otherwise, run complexity checks after validation and before execution. + + +## Complexity in trusted environments + +In environments that use persisted or precompiled operations, complexity analysis is still +useful, just in a different way. You can run it at build time to: + +- Warn engineers about expensive operations during development +- Track changes to operation cost across schema changes +- Define internal usage budgets by team, client, or role + +## Best practices + +- Only accept trusted documents in production when possible. +- Use complexity analysis as a development-time safeguard. +- Avoid running untrusted operations without additional validation and cost checks. +- Account for list fields and abstract types, which can significantly increase cost. +- Avoid estimating complexity before validation unless you're confident in your tooling. +- Use complexity analysis as part of your layered security strategy, alongside depth limits, +field guards, and authentication. + +## Additional resources + +- [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity): A community-maintained static analysis tool +- [`graphql-depth-limit`](https://github.com/graphile/depth-limit): A lightweight tool to restrict the maximum query depth +- [GraphQL Specification: Operations and execution](https://spec.graphql.org/draft/#sec-Language.Operations) +- [GraphQL.org: Security best practices](https://graphql.org/learn/security/) From 4a6bbe1cdfc19570517163f357eb81ece94455fc Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 29 May 2025 16:03:55 +0100 Subject: [PATCH 2/5] Editorial for #4405 (nullability) (#4416) --- website/pages/docs/nullability.mdx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/website/pages/docs/nullability.mdx b/website/pages/docs/nullability.mdx index 7d514b334f..9fddfe73ac 100644 --- a/website/pages/docs/nullability.mdx +++ b/website/pages/docs/nullability.mdx @@ -21,8 +21,8 @@ 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. +nullable parent field with `null`. This behavior is known formally as "error +propagation" but more commonly as null bubbling. Understanding nullability requires familiarity with the GraphQL type system, execution semantics, and the trade-offs involved in schema design. @@ -55,8 +55,7 @@ You can use `GraphQLNonNull` with: - Field types - Argument types -- Input object fields -- Return types for resolvers +- Input object field types You can also combine it with other types to create more specific constraints. For example: @@ -134,9 +133,14 @@ field can affect large portions of the response. 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`. +- Changing an output position (field type) from non-nullable to nullable is a + breaking change - clients may now receive `null` values which they do not have + handling code for. +- Changing an input position (argument or input field type) from nullable to + non-nullable is a breaking change - clients are now required to provide this + value, which they may not have been supplying previously. +- Changing an output position from nullable to non-nullable will not break + deployed clients since their null handling code will simply not be exercised. To reduce the risk of versioning issues, start with nullable fields and add constraints as your schema matures. @@ -211,8 +215,11 @@ constraints when data consistency is guaranteed. correctness. - Validate early. Add checks in resolvers to prevent returning `null` for non-null fields. +- Consider error boundaries. Were an error to occur, where should it stop + bubbling? - Watch for nesting. Distinguish between: - - `[User]!` - nullable list of non-null users + - `[User!]` - nullable list of non-null users + - `[User]!` - non-null list of nullable users - `[User!]!` - non-null list of non-null users ## Additional resources @@ -220,9 +227,9 @@ non-null fields. - [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 +- [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 +- [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 +- [Constructing Types](website/pages/docs/constructing-types.mdx): Shows how to define and compose types in GraphQL.js. From 0bfba7cd8849a3cb3e415100a8d55271fc20832e Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 29 May 2025 16:04:29 +0100 Subject: [PATCH 3/5] Indicate that field arguments should always be preferred over directives (#4417) Follow up to #4401 --- website/pages/docs/using-directives.mdx | 49 ++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/website/pages/docs/using-directives.mdx b/website/pages/docs/using-directives.mdx index f45f02b204..863b2e83e9 100644 --- a/website/pages/docs/using-directives.mdx +++ b/website/pages/docs/using-directives.mdx @@ -145,7 +145,10 @@ You can apply directives to: - Fields - Fragment spreads +- Named fragments - Inline fragments +- Operations (query, mutation or subscription) +- Variable definitions The following examples show how to apply directives: @@ -253,6 +256,49 @@ Some common use cases for custom directives include: - **Observability**: `@log`, `@tag(name: "important")`, or `@metrics(id: "xyz")` - **Execution control**: Mask or transform fields at runtime with schema visitors +## When to avoid custom directives + +Custom directives should not be used where an argument could achieve the same +goal. Custom directives in GraphQL requests complicate caching (particularly +normalized caches) and are less well understood by tooling such as GraphQL; by +using arguments instead we maximize compatibility and consistency. For example, +our `@uppercase` directive would fit naturally as a field argument: + +```js +import { + graphql, + buildSchema, +} from 'graphql'; + +const schema = buildSchema(` + enum Format { + VERBATIM + UPPERCASE + } + type Query { + greeting(format: Format! = VERBATIM): String + } +`); + +const rootValue = { + greeting: (source, args) => { + const result = 'Hello, world'; + + if (args.format === "UPPERCASE") { + return result.toUpperCase(); + } + + return result; + }, +}; + +const query = ` + query { + greeting(format: UPPERCASE) + } +`; + + ## Best practices When working with custom directives in GraphQL.js, keep the following best practices in mind: @@ -261,6 +307,7 @@ When working with custom directives in GraphQL.js, keep the following best pract 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. +- Always prefer field arguments over directives where possible. - 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 @@ -272,4 +319,4 @@ writing custom validation rules to enforce correct usage. - [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 +- Apollo Server's guide on [Directives](https://www.apollographql.com/docs/apollo-server/schema/directives) From b5a4342df4add0dbbc48bfa32ac2071a24e98270 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 29 May 2025 16:05:53 +0100 Subject: [PATCH 4/5] docs: editorial on abstract types page (#4394) Editorial on #4393 --- website/pages/docs/abstract-types.mdx | 76 +++++++++++++++------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/website/pages/docs/abstract-types.mdx b/website/pages/docs/abstract-types.mdx index 607da1d309..e55204d314 100644 --- a/website/pages/docs/abstract-types.mdx +++ b/website/pages/docs/abstract-types.mdx @@ -5,7 +5,7 @@ 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 +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). @@ -22,20 +22,20 @@ 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`, + - 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. + - 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. +must include a `name`, definition of the shared `fields`, and should include a `resolveType` +function telling GraphQL which concrete type a given value corresponds to. The following example defines a `ContentItem` interface for a publishing platform: -```js +```js filename="ContentItemInterface.js" import { GraphQLInterfaceType, GraphQLString, GraphQLNonNull } from 'graphql'; const ContentItemInterface = new GraphQLInterfaceType({ @@ -55,10 +55,13 @@ const ContentItemInterface = new GraphQLInterfaceType({ return null; }, }); + +exports.ContentItemInterface = ContentItemInterface; ``` -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. +The `resolveType` function must return either the string type name corresponding +to the `GraphQLObjectType` of the given `value`, or `null` if the type could not +be determined. ## Implementing interfaces with object types @@ -70,6 +73,7 @@ conform to the `ContentItem` interface: ```js import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; +import { ContentItemInterface } from './ContentItemInterface.js'; const ArticleType = new GraphQLObjectType({ name: 'Article', @@ -105,11 +109,8 @@ GraphQL uses `resolveType`. 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 +A union requires a name and a list of object types (`types`). It should also be +provided a `resolveType` function the same as explained for interfaces above. The following example defines a `SearchResult` union: @@ -134,39 +135,42 @@ const SearchResultType = new GraphQLUnionType({ }); ``` -Unlike interfaces, unions don't declare any fields of their own. Clients use inline fragments -to query fields from the concrete types. +Unlike interfaces, unions don't declare any fields their members must implement. +Clients use a fragment with a type condition to query fields from a concrete type. ## Resolving abstract types at runtime -GraphQL resolves abstract types dynamically during execution using the `resolveType` function. +GraphQL resolves abstract types dynamically during execution using the `resolveType` function, if +present. This function receives the following arguments: +{/* prettier-ignore */} ```js resolveType(value, context, info) ``` It can return: -- A `GraphQLObjectType` instance (recommended) - The name of a type as a string +- `null` if the type could not be determined - A `Promise` resolving to either of the above -If `resolveType` isn't defined, GraphQL falls back to checking each possible type's `isTypeOf` +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. +To query a field that returns an abstract type, use 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") { +query Search($term: String! = "deep learning") { + search(term: $term) { + # Inline fragments with type condition: ... on Book { title isbn @@ -175,30 +179,34 @@ For example: name bio } - ... on Publisher { - name - catalogSize - } + # Named fragment: + ...publisherFrag } } + +fragment publisherFrag 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. +enables code generation and editor tooling to provide type-aware completions; however you should +keep in mind the possibility that more types will implement the interface or be included in the +union in future, and thus ensure that you have a default case to handle additional types. ## 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. + 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 +- GraphQL Specification: + - [Interfaces](https://spec.graphql.org/October2021/#sec-Interfaces) + - [Unions](https://spec.graphql.org/October2021/#sec-Unions) From f5278f733895fe7abe2b561efb702f79c57288b7 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Fri, 30 May 2025 04:53:56 -0400 Subject: [PATCH 5/5] docs: cleanup and fixes (#4419) This PR has some cleanup tasks: - Reorganized the information architecture a bit so that sections flow better - Fixed broken links - Fixed a code snippet that wasn't closed off/bleeding into a section - Updated auth strategy guide w/ callouts for not using resolver auth in production per @benjie request Please let me know if there are any other tweaks I can include! --------- Co-authored-by: Benjie Co-authored-by: Jovi De Croock --- website/pages/docs/_meta.ts | 24 ++++++------- website/pages/docs/abstract-types.mdx | 4 ++- .../authentication-and-express-middleware.mdx | 4 ++- .../pages/docs/authorization-strategies.mdx | 34 ++++++++++++------- website/pages/docs/basic-types.mdx | 2 ++ website/pages/docs/caching-strategies.mdx | 2 +- website/pages/docs/constructing-types.mdx | 2 ++ .../pages/docs/cursor-based-pagination.mdx | 2 ++ website/pages/docs/defer-stream.mdx | 2 ++ website/pages/docs/going-to-production.mdx | 7 ++-- website/pages/docs/graphql-clients.mdx | 2 ++ .../pages/docs/mutations-and-input-types.mdx | 2 ++ website/pages/docs/n1-dataloader.mdx | 10 +++--- website/pages/docs/nullability.mdx | 8 ++--- website/pages/docs/object-types.mdx | 2 ++ website/pages/docs/oneof-input-objects.mdx | 2 ++ website/pages/docs/passing-arguments.mdx | 2 ++ .../running-an-express-graphql-server.mdx | 2 ++ website/pages/docs/using-directives.mdx | 8 +++++ 19 files changed, 84 insertions(+), 37 deletions(-) diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 97c5bc2b2e..2e6156976b 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -2,30 +2,33 @@ const meta = { index: '', '-- 1': { type: 'separator', - title: 'GraphQL.JS Tutorial', + title: 'Getting Started', }, 'getting-started': '', 'running-an-express-graphql-server': '', 'graphql-clients': '', + 'authentication-and-express-middleware': '', + '-- 2': { + type: 'separator', + title: 'Core Concepts', + }, 'basic-types': '', 'passing-arguments': '', 'object-types': '', 'mutations-and-input-types': '', - 'authentication-and-express-middleware': '', - 'authorization-strategies': '', - '-- 2': { + nullability: '', + 'abstract-types': '', + 'custom-scalars': '', + '-- 3': { 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': '', 'operation-complexity-controls': '', 'n1-dataloader': '', @@ -33,13 +36,10 @@ const meta = { 'resolver-anatomy': '', 'graphql-errors': '', 'using-directives': '', - '-- 3': { - type: 'separator', - title: 'Testing', - }, + 'authorization-strategies': '', '-- 4': { type: 'separator', - title: 'FAQ', + title: 'Production & Scaling', }, 'going-to-production': '', 'scaling-graphql': '', diff --git a/website/pages/docs/abstract-types.mdx b/website/pages/docs/abstract-types.mdx index e55204d314..1762f9bb53 100644 --- a/website/pages/docs/abstract-types.mdx +++ b/website/pages/docs/abstract-types.mdx @@ -2,6 +2,8 @@ title: Abstract types in GraphQL.js --- +# 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. @@ -206,7 +208,7 @@ union in future, and thus ensure that you have a default case to handle addition ## Additional resources -- [Constructing Types](https://www.graphql-js.org/docs/constructing-types/) +- [Constructing Types](./constructing-types) - GraphQL Specification: - [Interfaces](https://spec.graphql.org/October2021/#sec-Interfaces) - [Unions](https://spec.graphql.org/October2021/#sec-Unions) diff --git a/website/pages/docs/authentication-and-express-middleware.mdx b/website/pages/docs/authentication-and-express-middleware.mdx index 44c89f75a7..a633168f52 100644 --- a/website/pages/docs/authentication-and-express-middleware.mdx +++ b/website/pages/docs/authentication-and-express-middleware.mdx @@ -3,6 +3,8 @@ title: Using Express Middleware with GraphQL.js sidebarTitle: Using Express Middleware --- +# Authentication and Express Middleware + import { Tabs } from 'nextra/components'; It's simple to use any Express middleware in conjunction with `graphql-http`. In particular, this is a great pattern for handling authentication. @@ -101,4 +103,4 @@ If you aren't familiar with any of these authentication mechanisms, we recommend 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 +Want to control access to specific operations or fields? See [Authorization Strategies](./authorization-strategies). \ No newline at end of file diff --git a/website/pages/docs/authorization-strategies.mdx b/website/pages/docs/authorization-strategies.mdx index c467ce6640..d43e19f123 100644 --- a/website/pages/docs/authorization-strategies.mdx +++ b/website/pages/docs/authorization-strategies.mdx @@ -2,6 +2,8 @@ title: Authorization Strategies --- +import { Callout } from 'nextra/components' + 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. @@ -10,6 +12,12 @@ 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`. + + In production systems authorization should be handled in your business logic layer, not your + GraphQL resolvers. GraphQL is intended to be a thin execution layer that calls into your application's + domain logic, which enforces access control. + + ## What is authorization? Authorization determines what a user is allowed to do. It's different from @@ -23,11 +31,7 @@ In GraphQL, authorization typically involves restricting: ## 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: +You can implement simple authorization checks directly in resolvers using `context.user`: ```js export const resolvers = { @@ -42,12 +46,15 @@ export const resolvers = { }; ``` -This works well for smaller schemas or one-off checks. +This approach can help when you're learning how context works or building quick prototypes. +However, for production systems, you should enforce access control in your business logic layer +rather than in GraphQL resolvers. ## 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: +If you're experimenting or building a small project, repeating checks like +`context.user.role !== 'admin'` across resolvers can become error-prone. One +way to manage that duplication is by extracting shared logic into utility functions: ```js export function requireUser(user) { @@ -64,7 +71,7 @@ export function requireRole(user, role) { } ``` -You can use these helpers in resolvers: +Then use those helpers in resolvers: ```js import { requireRole } from './auth.js'; @@ -79,7 +86,11 @@ export const resolvers = { }; ``` -This pattern makes your access rules easier to read, test, and update. +This pattern improves readability and reusability, but like all resolver-based authorization, +it's best suited for prototypes or early-stage development. + +For production use, move authorization into your business logic layer. These helpers can still be useful +there, but they should be applied outside the GraphQL execution layer. ## Field-level access control @@ -148,8 +159,7 @@ 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. +- Keep authorization logic in your business logic layer, not in your GraphQL resolvers. - Use shared helper functions to reduce duplication and improve clarity. - Avoid tightly coupling authorization logic to your schema. Make it reusable where possible. diff --git a/website/pages/docs/basic-types.mdx b/website/pages/docs/basic-types.mdx index 08fbe2a44e..4913b5a11e 100644 --- a/website/pages/docs/basic-types.mdx +++ b/website/pages/docs/basic-types.mdx @@ -2,6 +2,8 @@ title: Basic Types --- +# Basic Types + import { Tabs } from 'nextra/components'; In most situations, all you need to do is to specify the types for your API using the GraphQL schema language, taken as an argument to the `buildSchema` function. diff --git a/website/pages/docs/caching-strategies.mdx b/website/pages/docs/caching-strategies.mdx index 112e0feacd..b33cdb4d6e 100644 --- a/website/pages/docs/caching-strategies.mdx +++ b/website/pages/docs/caching-strategies.mdx @@ -142,7 +142,7 @@ export const resolvers = { - 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). +see [Solving the N+1 Problem with DataLoader](./n1-dataloader). ## Operation result caching diff --git a/website/pages/docs/constructing-types.mdx b/website/pages/docs/constructing-types.mdx index 40bd1bfa87..2744ab7c81 100644 --- a/website/pages/docs/constructing-types.mdx +++ b/website/pages/docs/constructing-types.mdx @@ -2,6 +2,8 @@ title: Constructing Types --- +# Constructing Types + import { Tabs } from 'nextra/components'; For many apps, you can define a fixed schema when the application starts, and define it using GraphQL schema language. In some cases, it's useful to construct a schema programmatically. You can do this using the `GraphQLSchema` constructor. diff --git a/website/pages/docs/cursor-based-pagination.mdx b/website/pages/docs/cursor-based-pagination.mdx index e14cc9796d..93eefdd98a 100644 --- a/website/pages/docs/cursor-based-pagination.mdx +++ b/website/pages/docs/cursor-based-pagination.mdx @@ -4,6 +4,8 @@ title: Implementing Cursor-based Pagination import { Callout } from "nextra/components"; +# Implementing Cursor-based Pagination + 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. diff --git a/website/pages/docs/defer-stream.mdx b/website/pages/docs/defer-stream.mdx index 166bcb3eb3..d52296f0a5 100644 --- a/website/pages/docs/defer-stream.mdx +++ b/website/pages/docs/defer-stream.mdx @@ -2,6 +2,8 @@ title: Enabling Defer & Stream --- +# Enabling Defer and Stream + import { Callout } from 'nextra/components' diff --git a/website/pages/docs/going-to-production.mdx b/website/pages/docs/going-to-production.mdx index bce92a5474..da69a36942 100644 --- a/website/pages/docs/going-to-production.mdx +++ b/website/pages/docs/going-to-production.mdx @@ -2,8 +2,11 @@ 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. +# 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. 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, diff --git a/website/pages/docs/graphql-clients.mdx b/website/pages/docs/graphql-clients.mdx index 552865c56a..e1c56e1fa7 100644 --- a/website/pages/docs/graphql-clients.mdx +++ b/website/pages/docs/graphql-clients.mdx @@ -2,6 +2,8 @@ title: GraphQL Clients --- +# 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: diff --git a/website/pages/docs/mutations-and-input-types.mdx b/website/pages/docs/mutations-and-input-types.mdx index 7d45bbe6f4..397ff64f21 100644 --- a/website/pages/docs/mutations-and-input-types.mdx +++ b/website/pages/docs/mutations-and-input-types.mdx @@ -2,6 +2,8 @@ title: Mutations and Input Types --- +# Mutations and Input Types + 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. diff --git a/website/pages/docs/n1-dataloader.mdx b/website/pages/docs/n1-dataloader.mdx index 57e8f351aa..00601ed783 100644 --- a/website/pages/docs/n1-dataloader.mdx +++ b/website/pages/docs/n1-dataloader.mdx @@ -1,8 +1,10 @@ --- -title: Solving the N+1 Problem with `DataLoader` +title: Solving the N+1 Problem with DataLoader --- -When building your first server with GraphQL.js, it's common to encounter +# Solving the N+1 Problem with DataLoader + +When building a 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. @@ -49,7 +51,7 @@ 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` +## 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)` @@ -146,4 +148,4 @@ 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. +- [GraphQL field resolvers](./resolver-anatomy): Background on how field resolution works. diff --git a/website/pages/docs/nullability.mdx b/website/pages/docs/nullability.mdx index 9fddfe73ac..0e5ce6f0ff 100644 --- a/website/pages/docs/nullability.mdx +++ b/website/pages/docs/nullability.mdx @@ -227,9 +227,9 @@ non-null fields. - [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 +- [Understanding GraphQL.js Errors](./graphql-errors): Explains how GraphQL.js propagates and formats execution-time errors. -- [Anatomy of a Resolver](website/pages/docs/resolver-anatomy.mdx): Breaks down +- [Anatomy of a Resolver](./resolver-anatomy): 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. +- [Constructing Types](./constructing-types): Shows how +to define and compose types in GraphQL.js. \ No newline at end of file diff --git a/website/pages/docs/object-types.mdx b/website/pages/docs/object-types.mdx index 6b15c86646..961d34e4d4 100644 --- a/website/pages/docs/object-types.mdx +++ b/website/pages/docs/object-types.mdx @@ -2,6 +2,8 @@ title: Object Types --- +# Object Types + 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. diff --git a/website/pages/docs/oneof-input-objects.mdx b/website/pages/docs/oneof-input-objects.mdx index 0a968eace7..516da5b538 100644 --- a/website/pages/docs/oneof-input-objects.mdx +++ b/website/pages/docs/oneof-input-objects.mdx @@ -2,6 +2,8 @@ title: OneOf input objects --- +# OneOf input objects + import { Tabs } from 'nextra/components'; Some inputs will behave differently depending on what input we choose. Let's look at the case for diff --git a/website/pages/docs/passing-arguments.mdx b/website/pages/docs/passing-arguments.mdx index c1c010dde0..45eb087cd8 100644 --- a/website/pages/docs/passing-arguments.mdx +++ b/website/pages/docs/passing-arguments.mdx @@ -2,6 +2,8 @@ title: Passing Arguments --- +# 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`: diff --git a/website/pages/docs/running-an-express-graphql-server.mdx b/website/pages/docs/running-an-express-graphql-server.mdx index 06ccf7d5cf..9ea97017d8 100644 --- a/website/pages/docs/running-an-express-graphql-server.mdx +++ b/website/pages/docs/running-an-express-graphql-server.mdx @@ -3,6 +3,8 @@ title: Running an Express GraphQL Server sidebarTitle: Running Express + GraphQL --- +# Running an Express GraphQL Server + import { Tabs } from 'nextra/components'; The simplest way to run a GraphQL API server is to use [Express](https://expressjs.com), a popular web application framework for Node.js. You will need to install two additional dependencies: diff --git a/website/pages/docs/using-directives.mdx b/website/pages/docs/using-directives.mdx index 863b2e83e9..4e24dfe643 100644 --- a/website/pages/docs/using-directives.mdx +++ b/website/pages/docs/using-directives.mdx @@ -298,6 +298,14 @@ const query = ` } `; +const result = await graphql({ + schema, + source: query, + rootValue, +}); + +console.log(result); +``` ## Best practices