diff --git a/src/components/learn-aggregator/assets/pen.svg b/src/components/learn-aggregator/assets/pen.svg
new file mode 100644
index 0000000000..d211d1d722
--- /dev/null
+++ b/src/components/learn-aggregator/assets/pen.svg
@@ -0,0 +1,64 @@
+
diff --git a/src/components/learn-aggregator/learn-pages.tsx b/src/components/learn-aggregator/learn-pages.tsx
index 5d3fdb18aa..526d764221 100644
--- a/src/components/learn-aggregator/learn-pages.tsx
+++ b/src/components/learn-aggregator/learn-pages.tsx
@@ -111,6 +111,12 @@ const _items: Record<
icon: new URL("./assets/solve.svg", import.meta.url).href,
section: "best-practices",
},
+ "error-handling": {
+ description:
+ "Compare top-level errors and errors-as-data, and learn when to use each approach for exceptional versus expected failures.",
+ icon: new URL("./assets/construction.svg", import.meta.url).href,
+ section: "best-practices",
+ },
"global-object-identification": {
description:
"Use globally unique IDs and the Node interface to enable caching, refetching, and efficient schema traversal.",
@@ -153,6 +159,12 @@ const _items: Record<
icon: new URL("./assets/construction.svg", import.meta.url).href,
section: "best-practices",
},
+ "naming-design": {
+ description:
+ "Learn how to establish naming conventions and design standards for your GraphQL schema.",
+ icon: new URL("./assets/pen.svg", import.meta.url).href,
+ section: "best-practices",
+ },
}
const learnPages = _items as Record
diff --git a/src/pages/learn/_meta.ts b/src/pages/learn/_meta.ts
index b1ce2956c5..55b42ab663 100644
--- a/src/pages/learn/_meta.ts
+++ b/src/pages/learn/_meta.ts
@@ -32,6 +32,7 @@ export default {
authorization: "",
pagination: "",
"schema-design": "Schema Design",
+ "error-handling": "Error Handling",
"global-object-identification": "",
caching: "",
performance: "",
@@ -39,4 +40,9 @@ export default {
federation: "",
"robust-applications": "Robust Applications",
"debug-errors": "Common GraphQL over HTTP Errors",
+ "-- 3": {
+ type: "separator",
+ title: "Schema Governance",
+ },
+ "naming-design": "Naming Conventions and Design Standards",
}
diff --git a/src/pages/learn/error-handling.mdx b/src/pages/learn/error-handling.mdx
new file mode 100644
index 0000000000..5c8dfca466
--- /dev/null
+++ b/src/pages/learn/error-handling.mdx
@@ -0,0 +1,59 @@
+# Error Handling
+
+GraphQL gives you two complementary ways to communicate errors to clients: the top-level `errors` array for exceptional failures, and errors-as-data for expected, domain-specific failures. Using the right approach for each situation keeps your API predictable and makes error states discoverable through the schema.
+
+## Use top-level errors for exceptional failures
+
+The standard GraphQL `errors` array handles infrastructure and system failures:
+
+```json
+{
+ "data": null,
+ "errors": [{
+ "message": "Database connection failed",
+ "extensions": {
+ "code": "INTERNAL_SERVER_ERROR"
+ }
+ }]
+}
+```
+
+## Use errors-as-data for domain errors
+
+Business logic errors belong in the schema as structured types:
+
+```graphql
+type Mutation {
+ createUser(input: CreateUserInput!): CreateUserPayload!
+}
+
+type CreateUserPayload {
+ user: User
+ userErrors: [UserError!]!
+}
+
+type UserError {
+ message: String!
+ field: [String!]
+ code: UserErrorCode!
+}
+
+enum UserErrorCode {
+ USERNAME_TAKEN
+ INVALID_EMAIL
+ PASSWORD_TOO_SHORT
+}
+```
+
+This pattern makes errors discoverable through introspection and provides type safety.
+
+## Choose based on whether errors are exceptional
+
+| Error type | Pattern | Example |
+|-----------|---------|---------|
+| Infrastructure failure | Top-level error | Database timeout, network error |
+| Invalid GraphQL | Top-level error | Syntax error, unknown field |
+| Authentication required | Top-level error | Missing auth token |
+| Business rule violation | errors-as-data | Username taken, invalid email |
+| Validation failure | errors-as-data | Required field missing |
+| Domain constraint | errors-as-data | Insufficient inventory |
diff --git a/src/pages/learn/naming-design.mdx b/src/pages/learn/naming-design.mdx
new file mode 100644
index 0000000000..e48cc0d3b4
--- /dev/null
+++ b/src/pages/learn/naming-design.mdx
@@ -0,0 +1,486 @@
+import { Callout } from "nextra/components"
+
+# Naming Conventions and Design Standards
+
+Naming conventions determine how easily developers understand and use your GraphQL API. Consistent naming patterns make schemas predictable, enable code generation tools to work smoothly, and prevent confusion as your API grows.
+
+The GraphQL specification doesn't define naming conventions. This guide documents patterns that have proven effective in production APIs. These are conventions, not absolute rules. Your API may have valid reasons to deviate, but do so deliberately and document why.
+
+## Apply field and type naming patterns
+
+The following recommendations are based on patterns commonly used in production APIs:
+
+| Element | Convention | Example | Rationale |
+|---------|-----------|---------|-----------|
+| Fields | camelCase | `firstName`, `createdAt` | Matches JavaScript variable conventions |
+| Arguments | camelCase | `userId`, `includeArchived` | Consistency with fields |
+| Types | PascalCase | `User`, `ProductConnection` | Matches class naming in most languages |
+| Enums | PascalCase | `OrderStatus`, `Currency` | Consistency with other types |
+| Enum values | SCREAMING_SNAKE_CASE | `IN_PROGRESS`, `COMPLETED` | Distinguishes constants from types |
+| Interfaces | PascalCase | `Node`, `Timestamped` | Same as types |
+| Unions | PascalCase | `SearchResult`, `MediaItem` | Same as types |
+| Directives | camelCase | `@include`, `@semanticNonNull` | Matches field and argument casing |
+
+### Name boolean fields with prefixes
+
+Boolean fields should start with `is` or `has` to clearly indicate they return true or false values:
+
+```graphql
+type User {
+ id: ID!
+ name: String!
+ isActive: Boolean!
+ hasSubscription: Boolean!
+}
+```
+
+This pattern appears in the GraphQL specification itself with `isDeprecated` in introspection types.
+
+### Use plural names for list fields
+
+List fields should use plural nouns to indicate they return multiple items:
+
+```graphql
+type User {
+ id: ID!
+ posts: [Post!]!
+ featuredPost: Post
+}
+```
+
+### Avoid verb prefixes on query fields
+
+Query fields should describe the data they return, not the action of retrieval:
+
+```graphql
+# Preferred
+type Query {
+ user(id: ID!): User
+ posts(first: Int): PostConnection
+}
+
+# Avoid
+type Query {
+ getUser(id: ID!): User
+ fetchPosts(first: Int): PostConnection
+}
+```
+
+The operation type already indicates this is a query. Adding `get` or `fetch` creates inconsistency with nested fields.
+
+## Choose a mutation naming strategy
+
+Two patterns exist for mutation names, and both work well in production.
+
+### Verb-first mutation naming
+
+Start mutation names with action verbs:
+
+```graphql
+type Mutation {
+ createUser(input: CreateUserInput!): CreateUserPayload!
+ updateProduct(input: UpdateProductInput!): UpdateProductPayload!
+ deletePost(id: ID!): DeletePostPayload!
+ sendPasswordResetEmail(email: String!): SendPasswordResetPayload!
+}
+```
+
+This pattern reads naturally and handles non-CRUD operations well.
+
+### Noun-first mutation naming
+
+Start mutation names with the entity being modified:
+
+```graphql
+type Mutation {
+ userCreate(input: CreateUserInput!): CreateUserPayload!
+ productUpdate(input: UpdateProductInput!): UpdateProductPayload!
+ postDelete(id: ID!): DeletePostPayload!
+ passwordResetEmailSend(email: String!): SendPasswordResetPayload!
+}
+```
+
+This pattern groups related mutations alphabetically, making schema discovery easier. [Shopify uses this convention](https://shopify.dev/docs/api/admin-graphql) across their public API.
+
+### Pick one pattern
+
+Consistency matters more than which pattern you choose:
+
+| Pattern | Pros | Cons | Best for |
+|---------|------|------|----------|
+| Verb-first | Natural language, handles non-CRUD well | Scattered alphabetically | APIs with many business operations |
+| Noun-first | Groups by entity, easy discovery | Awkward for non-CRUD actions | CRUD-heavy APIs, large schemas |
+
+## Name input and output types with clear suffixes
+
+### Mark input types with Input suffix
+
+All input object types should end with `Input`:
+
+```graphql
+input CreateUserInput {
+ email: String!
+ name: String!
+ role: UserRole!
+}
+
+input UpdateProductInput {
+ id: ID!
+ name: String
+ price: Int
+}
+
+input SearchFiltersInput {
+ category: String
+ minPrice: Int
+ maxPrice: Int
+}
+```
+
+### Wrap mutation results in payload types
+
+Return structured payload types from mutations instead of entities directly:
+
+```graphql
+# Inflexible, doesn't evolve well
+type Mutation {
+ createUser(input: CreateUserInput!): User
+}
+
+# Flexible, can add fields without breaking changes
+type Mutation {
+ createUser(input: CreateUserInput!): CreateUserPayload!
+}
+
+type CreateUserPayload {
+ user: User
+ userErrors: [UserError!]!
+}
+```
+
+Payload types let you add validation errors and related entities without breaking existing clients.
+
+## Design nullability patterns thoughtfully
+
+GraphQL fields are nullable by default. Thoughtful nullability design helps schemas evolve gracefully and handle partial failures. The trade-offs between non-null guarantees and resilience are nuanced, and the ecosystem is still evolving here with features like `@semanticNonNull` and client-side `onError` handling. This guide focuses on the mechanics of nullability rather than prescribing a default.
+
+### Understand null bubbling behavior
+
+When a non-nullable field resolves to null, GraphQL propagates that null up to the nearest nullable parent. If `email` fails to resolve below, the entire `user` becomes null:
+
+```graphql
+type Query {
+ user(id: ID!): User
+}
+
+type User {
+ id: ID!
+ name: String!
+ email: String!
+}
+```
+
+### Use consistent list nullability
+
+For list fields, use `[Item!]!` so clients receive an empty array instead of null:
+
+```graphql
+type User {
+ id: ID!
+ posts: [Post!]!
+ comments: [Comment!]!
+}
+```
+
+### Handle input nullability carefully
+
+Input types face a three-way distinction: omitting a field, explicitly passing null, and providing a value. This matters for partial updates where clients need to clear optional fields.
+
+```graphql
+input UpdateUserInput {
+ id: ID!
+
+ "Pass a value to update, null to clear, or omit to leave unchanged"
+ bio: String
+
+ "Pass a value to update, or omit to leave unchanged (cannot be cleared)"
+ name: String
+}
+```
+
+Some teams prefer explicit flags for nullable fields to avoid ambiguity:
+
+```graphql
+input UpdateUserInput {
+ id: ID!
+ name: String
+ bio: String
+ clearBio: Boolean # If true, sets bio to null regardless of bio field
+}
+```
+
+Whichever pattern you choose, document the behavior clearly in field descriptions.
+
+## Document schemas comprehensively
+
+### Write clear descriptions
+
+Describe every type, and every non-obvious field. Descriptions surface in tools like GraphiQL and in generated reference docs, so treat them as user-facing documentation rather than internal notes:
+
+```graphql
+"A product available for purchase in the catalog."
+type Product {
+ id: ID!
+
+ "Display name shown to customers in search results and product pages"
+ name: String!
+
+ """
+ Current price in USD cents. May be null during off-season periods when
+ product pricing is unavailable. Check 'availableForPurchase' before
+ displaying price to customers.
+ """
+ price: Int
+
+ "Indicates whether product can currently be added to cart"
+ availableForPurchase: Boolean!
+}
+```
+
+### Document deprecations with migration paths
+
+Include what to use instead and when removal will occur:
+
+```graphql
+type User {
+ id: ID!
+ userId: ID! @deprecated(reason: "Use 'id' instead. Removal scheduled for v3.0.")
+}
+```
+
+For a complete deprecation lifecycle including tracking usage and coordinating removal, see [Schema Change Management](/learn/governance-versioning/).
+
+## Use custom scalars for domain-specific types
+
+Custom scalars add validation and semantic meaning beyond built-in types. Use them for values with specific formats or constraints.
+
+### Common custom scalars
+
+| Scalar | Use instead of | Purpose |
+|--------|---------------|---------|
+| `DateTime` | `String` | ISO 8601 timestamps with timezone |
+| `Date` | `String` | Calendar dates without time |
+| `Email` | `String` | Validated email addresses |
+| `URL` | `String` | Valid URLs |
+| `UUID` | `ID` or `String` | Standardized unique identifiers |
+| `JSON` | `String` | Arbitrary JSON when schema flexibility is needed |
+
+### When to use custom scalars
+
+```graphql
+# Without custom scalars - no validation, unclear format
+type User {
+ id: ID!
+ email: String!
+ createdAt: String!
+ avatarUrl: String
+}
+
+# With custom scalars - self-documenting, validated
+type User {
+ id: ID!
+ email: Email!
+ createdAt: DateTime!
+ avatarUrl: URL
+}
+```
+
+Custom scalars shift validation to the GraphQL layer, so invalid values fail before reaching resolvers. Many GraphQL implementations provide libraries with common scalars, such as [graphql-scalars](https://the-guild.dev/graphql/scalars) for JavaScript. See also [scalars.graphql.org](https://scalars.graphql.org/) for community-maintained custom scalar specifications.
+
+### Trade-offs to consider
+
+Custom scalars have costs: they require implementation in every client and server, reduce portability, and add complexity. Use them when:
+
+- The format has clear validation rules (emails, URLs, dates)
+- Multiple fields share the same constraints
+- Client code generation benefits from the type distinction
+
+Avoid creating custom scalars for types that are really just strings with business rules, like `Username` or `ProductCode`. These are better enforced in resolver logic.
+
+## Handle errors
+
+GraphQL supports two complementary approaches to errors: the top-level `errors` array for exceptional infrastructure and system failures, and errors-as-data (typed error fields modeled directly in your schema) for expected business-rule failures. Deciding which to use, and how to structure typed errors, is a design concern in its own right.
+
+For guidance on choosing between these patterns, see [Error Handling](/learn/error-handling/).
+
+## Implement pagination with connections
+
+The [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm) is widely used for paginating GraphQL lists. For a broader overview of pagination approaches, see [Pagination](/learn/pagination/).
+
+### Structure connection types
+
+```graphql
+type Query {
+ posts(first: Int, after: String): PostConnection!
+}
+
+type PostConnection {
+ edges: [PostEdge!]!
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+type PostEdge {
+ node: Post!
+ cursor: String!
+}
+
+type PageInfo {
+ hasNextPage: Boolean!
+ hasPreviousPage: Boolean!
+ startCursor: String
+ endCursor: String
+}
+```
+
+Use the pattern `{TypeName}Connection` and `{TypeName}Edge` for naming.
+
+### When to use connections
+
+| Use connections for | Use simple lists for |
+|-------------------|---------------------|
+| Dynamic, frequently changing data | Static, rarely changing data |
+| Large datasets requiring pagination | Small datasets (< 100 items) |
+| Data where items may be added/removed | Configuration or reference data |
+| Social feeds, activity streams | Enum-like lookup tables |
+
+If a list might grow unbounded, use connections from the start.
+
+## Enforce standards with tooling
+
+### Configure schema linting
+
+[GraphQL-ESLint](https://the-guild.dev/graphql/eslint/docs) provides linting with configurable naming rules:
+
+```javascript
+export default {
+ overrides: [{
+ files: ['**/*.graphql'],
+ parser: '@graphql-eslint/eslint-plugin',
+ plugins: ['@graphql-eslint'],
+ rules: {
+ '@graphql-eslint/naming-convention': ['error', {
+ types: 'PascalCase',
+ FieldDefinition: 'camelCase',
+ InputValueDefinition: 'camelCase',
+ Argument: 'camelCase',
+ DirectiveDefinition: 'camelCase',
+ EnumValueDefinition: 'SCREAMING_SNAKE_CASE',
+ 'FieldDefinition[parent.name.value=Query]': {
+ forbiddenPrefixes: ['get', 'fetch'],
+ },
+ 'FieldDefinition[parent.name.value=Mutation]': {
+ forbiddenSuffixes: ['Mutation'],
+ },
+ }],
+ '@graphql-eslint/require-description': ['error', {
+ types: true,
+ FieldDefinition: true,
+ }],
+ '@graphql-eslint/require-deprecation-reason': 'error',
+ },
+ }],
+};
+```
+
+Adjust the mutation rules based on your chosen naming pattern. For noun-first APIs, add `forbiddenPrefixes: ['create', 'update', 'delete']` to enforce consistency.
+
+For broader schema validation beyond linting — including breaking change detection, coverage analysis, and schema diffing — consider [GraphQL Inspector](https://the-guild.dev/graphql/inspector).
+
+
+
+See the [tools and libraries](/community/tools-and-libraries/) page for more schema validation and linting tools.
+
+
+
+### Validate in CI/CD pipelines
+
+```yaml
+name: Schema Validation
+on:
+ pull_request:
+ paths:
+ - 'schema/**/*.graphql'
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint schema
+ run: npx eslint "schema/**/*.graphql"
+
+ - name: Check documentation
+ run: |
+ npx graphql-schema-linter \
+ --rules fields-have-descriptions \
+ --rules types-have-descriptions \
+ schema/**/*.graphql
+```
+
+
+
+For monitoring schema quality and tracking naming convention compliance over time, see the [monitoring resources](/resources/monitoring/) page.
+
+
+
+## Avoid common anti-patterns
+
+### Don't expose IDs instead of objects
+
+```graphql
+# Avoid
+type Post {
+ id: ID!
+ title: String!
+ authorId: ID!
+}
+
+# Prefer
+type Post {
+ id: ID!
+ title: String!
+ author: User!
+}
+```
+
+Model relationships as edges in the graph, not as foreign key IDs.
+
+### Don't mirror database structure
+
+Schemas should reflect your business domain, not your database tables:
+
+```graphql
+# Avoid
+type UserTable {
+ user_id: Int!
+ first_name: String
+ last_name: String
+ created_timestamp: String
+}
+
+# Prefer
+type User {
+ id: ID!
+ name: String!
+ createdAt: DateTime!
+ profile: UserProfile
+}
+```