Shopify GraphQL Design Tutorial

repository·master·Indexed 25 days ago

https://github.com/shopify/graphql-design-tutorial

A collection of design guidelines and lessons learned from evolving production GraphQL schemas at Shopify over a three-year period. The tutorial provides a top-down approach to building robust APIs, focusing on business domain semantics rather than implementation details, UI constraints, or legacy REST patterns. It covers specific rules for schema construction, including the use of the Node interface, custom scalars, Connection models for pagination, granular mutation design, and handling business errors via payload types.

Tokens
20.2K
Snippets
45
Records
89
Agent score
77%

What's inside shopify-graphql-design-tutorial

  1. Overview of the Shopify GraphQL Design Tutorial

    master

    The Shopify GraphQL Design Tutorial is a collection of design guidelines and lessons learned from evolving production GraphQL schemas at Shopify over a three-year period. It is intended to help developers create and evolve robust GraphQL APIs.

    Note: These guidelines are based on Shopify's specific use cases and may not apply to every scenario. It is recommended to pick and choose the rules that make sense for your specific requirements rather than implementing them blindly.

  2. Overview of the GraphQL Design Tutorial

    master

    This tutorial provides guidance on designing new GraphQL APIs or extending existing ones, based on Shopify's production experience over three years. It is intended to help developers navigate the complexities of schema construction and expansion.

    Key Approach: Instead of diving immediately into field details, nullability, or mutations, the tutorial advocates for a top-down approach: start by defining high-level objects and their relationships (similar to an Entity-Relationship diagram) before refining the implementation details.

  3. Summary of GraphQL Design Principles

    master

    The tutorial provides a comprehensive set of 23 rules for designing high-quality GraphQL APIs. These principles focus on abstraction, business logic, and developer experience. Key themes include:

    Core Abstractions & Modeling

    • Design from high abstraction: Focus on relationships between types before defining specific fields.
    • Avoid implementation details: Do not expose unnecessary internal details or database table structures (e.g., avoid exposing CollectionMembership tables directly).
    • Use the Node interface: Most business objects should implement the Node interface.
    • Group related fields: Extract closely related fields from a single type into a sub-object.
    • Use Enums and Custom Scalars: Use Enums for enumerable values and custom scalars to provide better context for fields.
    • Return objects, not IDs: Whenever possible, return the associated object itself rather than just its ID.

    Field & Type Design

    • Semantic Naming: Name fields based on business semantics rather than database column names.
    • Pagination: Always check if array fields require pagination support.
    • Computed Fields: Provide both raw data fields and business-relevant computed fields.
    • Evolution Caution: Remember that removing a field in GraphQL is significantly harder than adding one.

    Mutation Design

    • Naming Convention: Use the actionObject style (e.g., orderCancel) instead of verbObject (e.g., cancelOrder).
    • Batching: Design mutations to support batch operations where possible.
    • Input Types:
      • Only mark fields as non-nullable in Inputs if they are truly required.
      • Use structured Input types to reduce repetition, even if it means relaxing some type constraints.
      • For complex validation, consider using more generic types (like String) to allow the server to handle validation and return comprehensive error messages at once.
    • Error Handling: Mutations should include an array to identify business-level errors.
    • Payloads: Most fields in a Mutation Payload should be nullable, unless a value is guaranteed even in error scenarios.

    API Philosophy

    • Logic over Data: The API should provide business logic, not just raw data. Implement logic on the server rather than leaving it to clients.
    • Operation-Oriented: Design types based on the actual business operations they must support.
  4. Define required input fields correctly

    master

    In GraphQL input types, the ! operator denotes that a field is required (the client must provide it). Only mark fields as required if they are semantically necessary for the mutation to proceed. For example, if a user can create a collection without a description, the description field should not be marked with !.

    Rule #18: Only make input fields required if they are semantically required for the mutation to proceed.

  5. Handle business-level errors using mutation payloads

    master

    Do not use top-level GraphQL errors for business-logic failures (e.g., validation errors). Instead, define a specific payload type for each mutation that includes a userErrors field.

    Rule #23: Mutations should provide user/business-level errors via a userErrors field on the mutation payload. The top-level query errors entry is reserved for client and server-level errors.

    Rule #24: Most payload fields for a mutation should be nullable, unless there is really a value to return in every possible error case.

    Example Payload Structure:

    type CollectionCreatePayload {
      userErrors: [UserError!]!
      collection: Collection
    }
    
    type UserError {
      message: String!
      field: [String!]
    }
    type CollectionCreatePayload {
      userErrors: [UserError!]!
      collection: Collection
    }
    
    type UserError {
      message: String!
    
      # Path to input field which caused the error.
      field: [String!]
    }
  6. Use object references instead of ID fields for relations

    master

    In GraphQL, avoid the REST pattern of returning only the ID of a related object. Instead, include the object itself as a reference in the graph. This allows clients to query exactly what they need from the related object without making additional round trips.

    Rule #8: Always use object references instead of using other ID fields.

    type Collection implements Node {
      id: ID!
      image: Image
    }
    
    type Image {
      id: ID!
    }
  7. Mutate object relationships in GraphQL

    master

    When designing mutations for relationships (e.g., products in a collection), consider these strategies:

    1. Embedding the entire relationship: Passing a full list (e.g., products: [ProductInput!]!) in an update mutation. This is simple for small, one-to-one relationships but inefficient for large lists.
    2. Embedding "delta" fields: Passing only the changes (e.g., productsToAdd: [ID!]! and productsToRemove: [ID!]!). This is more efficient but keeps actions tied to a single mutation.
    3. Splitting into separate mutations: Creating distinct mutations like addProduct or removeProduct. This is the most flexible and safest approach for large, ordered, or complex relationships.

    Rule #16: When writing separate mutations for relationships, consider whether it would be useful for the mutations to operate on multiple elements at once. (e.g., addProducts instead of addProduct).

  8. Provide business logic via API fields

    master

    When designing a GraphQL API, avoid forcing clients to implement complex business logic (such as iterating through large lists to check for membership). Instead, provide dedicated fields that allow the server to perform these calculations. This ensures the server remains the single source of truth, prevents code duplication across multiple clients, and improves efficiency.

    Rule #12: APIs should provide business logic, not just data. Complex calculations should be performed on the server, not by multiple GraphQL clients.

    type Collection implements Node {
      # ...
      hasProduct(id: ID!): Boolean!
    }
  9. Name mutations based on business domain rather than CRUD

    master

    Do not default to CRUD verb names (create, read, update, delete) as they often represent implementation details rather than business operations. Use meaningful verbs that describe the actual outcome of the action.

    Rule: Prefer domain-specific verbs over CRUD verbs.

    Example: Instead of collectionDelete if the primary outcome is unpublishing, use collectionUnpublish.

  10. Handle business logic errors with userErrors

    master

    Do not use top-level GraphQL query errors for business logic failures (e.g., invalid user input). Instead, define a specific payload type for each mutation that includes a userErrors field.

    • Successful mutation: Returns an empty list for userErrors and the requested data.
    • Failed mutation: Returns one or more UserError objects and null for the primary data field.

    Rule #22: Mutations must provide user/business level errors through a userErrors field in the mutation payload. Top-level query errors are reserved for client and server errors.

    type UserError {
      message: String!
      # Path to input field which caused the error.
      field: [String!]
    }
    
    type CollectionCreatePayload {
      userErrors: [UserError!]!
      collection: Collection
    }
    
    type Mutation {
      collectionCreate(collection: CollectionInput!): CollectionCreatePayload!
    }
  11. Implement business logic on the server

    master

    When designing a GraphQL API, avoid forcing clients to implement complex business logic (such as iterating through a large list to find a specific item). Instead, provide dedicated fields that perform these calculations on the server. This ensures the server remains the single source of truth and prevents code duplication across different clients.

    Rule #12: The API should provide business logic, not just data. Complex calculations should be done on the server, in one place, not on the client, in many places.

    type Collection implements Node {
      # ...
      hasProduct(id: ID!): Boolean!
    }