| name | graphql-schema-design |
| description | Guides expert-level graphql schema design implementation: api-design and design-patterns decision frameworks, production-ready patterns, and concrete templates for graphql schema design workflows.
Use when the user asks about graphql schema design, graphql schema design configuration, or api-design best practices for graphql projects.
Do NOT use when the user needs a different backend infrastructure capability -- check sibling skills in the backend infrastructure subcategory.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"api-design backend design-patterns","category":"backend-systems","subcategory":"backend-infrastructure","depends":"","disclaimer":"none","difficulty":"intermediate"} |
GraphQL Schema Design
When to Use
Use this skill when:
- User is designing a new GraphQL API from scratch and needs schema structure guidance -- type definitions, query/mutation/subscription organization, and naming conventions
- User is migrating a REST API to GraphQL and needs to understand how to translate resource-based endpoints into a graph-based type system
- User is experiencing N+1 query problems, over-fetching, or under-fetching in an existing GraphQL schema and needs structural redesign
- User needs to model complex domain relationships -- bidirectional associations, polymorphic types, recursive structures, or union/interface hierarchies
- User is building a federated GraphQL architecture across multiple microservices and needs schema composition guidance
- User asks how to version or evolve a GraphQL schema without breaking existing clients
- User needs to design mutations with proper input types, error handling patterns, and optimistic UI support
- User is designing a schema for pagination, filtering, and sorting at scale and needs a consistent, extensible pattern
Do NOT use this skill when:
- User needs GraphQL resolver implementation guidance -- this skill covers schema structure, not resolver logic or DataLoader setup
- User needs GraphQL authentication/authorization middleware configuration -- check the backend-security skills
- User needs GraphQL client-side query writing or fragment composition -- this skill is server-schema design only
- User is asking about REST API design without GraphQL involvement -- check API design skills in the sibling subcategory
- User needs GraphQL subscription infrastructure setup (WebSocket servers, pub/sub brokers) -- that is an infrastructure concern beyond schema design
- User needs GraphQL performance monitoring, APM tooling, or query complexity analysis at runtime -- check observability skills
- User is asking about gRPC or OpenAPI/Swagger schema design -- separate skill domains
Process
1. Identify the Domain Model and Access Patterns
Before writing a single type definition, map the domain thoroughly.
- List all core business entities and draw their relationships -- identify which are nodes in a graph (independent entities with identity) vs. value objects (embedded data without identity)
- Distinguish ownership relationships (a
Post belongs to exactly one User) from association relationships (a Post has many Tags that exist independently)
- Catalog the top 10-15 read access patterns the UI actually needs -- screens, components, and dashboards drive schema shape more than the database schema does
- Catalog write operations grouped by actor, frequency, and atomicity requirements -- a single checkout mutation may span inventory, payment, and order creation
- Identify which entities are queried by ID most often vs. by collection with filtering -- entities queried by ID are candidates for the Node interface; filterable collections need pagination and filter input types
- Note cardinality: one-to-one, one-to-many, and many-to-many relationships have distinct modeling strategies in GraphQL
- Flag any data that is computed or derived (aggregates, calculated fields) -- these need special attention to avoid expensive resolver chains
2. Define the Type System Foundation
Establish scalar, enum, interface, and union types before building object types.
- Start with custom scalars for domain primitives:
DateTime (ISO-8601 strings), URL, EmailAddress, UUID, JSON, Decimal (for currency/precise numbers) -- never use raw String for typed values the client will parse
- Define enums for all closed sets of values:
OrderStatus, UserRole, PaymentMethod -- use SCREAMING_SNAKE_CASE for enum values per GraphQL spec convention
- Design interfaces for shared contracts across multiple types. The
Node interface (id: ID!) is the single most important interface -- implement it on every top-level entity to enable generic caching and refetching in clients like Apollo and Relay
- Use the
Error interface pattern to create a structured error type hierarchy: define a base UserError interface with message: String! and code: String!, then create domain-specific error types implementing it
- Define union types for truly polymorphic responses where types share no fields -- use
SearchResult = User | Post | Product rather than a single SearchResult object with nullable fields
- Create
Connection and Edge types following the Relay Cursor Connections specification -- even if not using Relay, this pattern provides a battle-tested pagination contract: UserConnection, UserEdge, PageInfo
- Establish
XxxInput naming convention for all input types used in mutations -- never reuse output object types as mutation inputs because their field requirements differ
3. Design the Query Root
Structure the Query type to be discoverable, consistent, and client-friendly.
- Use singular/plural naming rigorously:
user(id: ID!): User for single-entity lookup, users(filter: UserFilterInput, pagination: PaginationInput): UserConnection! for collection queries
- Never return nullable connections -- a collection query that finds zero results should return an empty connection, not null. Use
UserConnection! not UserConnection
- Group related fields semantically and document them with triple-quoted SDL descriptions -- these appear in GraphQL Playground, GraphiQL, and schema documentation tools
- Implement the Node interface lookup at the root:
node(id: ID!): Node enables clients to refetch any entity by global ID without knowing its type in advance
- Avoid deeply nested required arguments -- a query like
organizationUsersByRole(orgId: ID!, role: UserRole!, status: UserStatus) should use a single filter input type
- Design filter input types to be additive:
UserFilterInput { id: ID, email: String, role: UserRole, status: UserStatus, createdAfter: DateTime } -- all fields optional, combined with AND semantics by default
- Add
orderBy: [UserOrderByInput!] as a separate argument from filtering -- this keeps filtering and sorting concerns independent and allows multiple sort keys
- Consider search vs. filter: full-text search (
query: String) is a different pattern from structured filtering and should be a separate field or argument
4. Design Mutations
Mutations require the most careful design because they represent state changes with side effects.
- Follow the single-responsibility principle strictly: one mutation per logical operation.
createUser, updateUser, deleteUser are separate mutations -- never a generic mutateUser operation
- Use namespaced mutation naming for large schemas:
userCreate, userUpdate, userDelete groups all user mutations alphabetically in tooling. Alternatively use the Mutation.user: UserMutations nested object pattern (note: this breaks introspection-based tooling in some clients -- document the tradeoff)
- Every mutation takes exactly one input object argument:
createUser(input: CreateUserInput!): CreateUserPayload! -- this is the most future-proof signature because new fields can be added to the input without changing the mutation signature
- Every mutation returns a payload type, never a raw object type:
CreateUserPayload { user: User, errors: [UserError!]! } -- the payload wrapper allows adding fields like clientMutationId (Relay), warnings, and side-effect data without a breaking change
- Always include an
errors field in the mutation payload -- use the union/interface error pattern so clients can distinguish between ValidationError, AuthorizationError, NotFoundError, and ConflictError with typed fields
- Design optimistic UI support: ensure the returned payload contains all fields the UI needs to update local state without a separate refetch -- if creating a post, return the full
Post type with resolved author, category, and tag data
- Mark destructive mutations with a
DeleteXxxPayload { deletedId: ID!, success: Boolean! } pattern -- returning the deleted ID enables cache eviction in Apollo Client and similar
- Use
upsert mutations sparingly and only when the create-vs-update distinction is genuinely irrelevant to the caller -- document the idempotency semantics explicitly
5. Model Relationships and Connections
Graph relationships are the core value proposition of GraphQL -- design them carefully.
- Default to returning full nested objects rather than foreign key IDs --
post { author { id name avatarUrl } } is the GraphQL idiom; returning authorId: ID! defeats the purpose and forces the client to make additional queries
- For large one-to-many relationships (more than ~20 items), always paginate using cursor-based connections. Offset pagination (
skip/limit) has correctness issues with real-time data and does not scale -- prefer opaque cursor strings
- The Relay Cursor Connection spec fields:
edges { node cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } -- implement all four pageInfo fields even if bidirectional pagination is not immediately needed
- For many-to-many relationships with edge data (a
User enrolled in a Course with an enrolledAt date and progress percentage), model the join as an explicit Enrollment type with its own connection
- Avoid circular reference problems by designing which side "owns" the traversal --
User.posts is fine; adding Post.author.posts in a single query should be handled by query depth limiting in the server, not prevented in schema design
- Use
@deprecated(reason: "Use newFieldName instead") directive on old fields rather than removing them -- GraphQL's deprecation mechanism is the proper version evolution tool
- For highly connected data (social graphs, org hierarchies), add
depth and limit arguments to recursive fields: user { followers(depth: 2, limit: 100) { ... } } to prevent unbounded traversal
6. Apply Naming Conventions and Documentation
Consistent naming is not cosmetic -- it determines how discoverable and maintainable the schema is.
- Types: PascalCase (
UserProfile, OrderLineItem) -- never abbreviate unless the abbreviation is universal in the domain (SKU, URL, ID are fine; Usr and Ord are not)
- Fields: camelCase (
firstName, createdAt, isActive) -- Boolean fields should use is, has, or can prefix: isVerified, hasChildren, canEdit
- Input types:
[Verb][Noun]Input for mutations (CreateOrderInput, UpdateUserProfileInput), [Noun]FilterInput for queries (ProductFilterInput)
- Payload types:
[Verb][Noun]Payload (CreateOrderPayload, UpdateUserPayload)
- Enum values: SCREAMING_SNAKE_CASE (
PENDING_REVIEW, OUT_OF_STOCK)
- Connection types:
[Noun]Connection and [Noun]Edge (ProductConnection, ProductEdge)
- Write SDL descriptions for every type, every field with non-obvious semantics, and every argument. Use the triple-quote format:
"""Returns the 10 most recent orders. Use the connection args for pagination beyond 10."""
- Document nullability intent: if a field is nullable because it is genuinely optional vs. because it might fail to load vs. because it is computed async, note this in the description
7. Validate Schema Design Decisions
Before finalizing, run the schema through a validation checklist.
- Check for N+1 risk: any field that requires a database query per parent object is an N+1 risk. Identify these fields and ensure they are addressed in the resolver layer with DataLoader batching -- the schema design should not be changed to hide this, but the field should be documented
- Verify all mutations have error payloads and all queries have non-nullable connection returns
- Run the schema through a linter --
graphql-schema-linter enforces naming conventions, deprecation requirements, and description coverage automatically
- Check query depth -- define a maximum allowed query depth (typically 7-10 levels) and verify no legitimate client query requires more than this
- Simulate the top 5 client queries against the schema using a tool like
graphql-inspector -- verify the field paths are intuitive and require no unnecessary traversal
- Validate that no input type has required fields that should be optional, and no output type has non-null fields that could realistically be absent
- Check for breaking changes before any schema deployment using
graphql-inspector diff -- adding fields and types is non-breaking; removing fields, changing argument types, and changing nullability from nullable to non-null are all breaking
8. Document Schema Evolution Strategy
Schema design is not a one-time event -- establish the governance model upfront.
- Commit to semantic versioning via schema changelog, not URL versioning (
/graphql/v2) -- GraphQL evolves gracefully through deprecation and field addition
- Establish a deprecation policy: deprecated fields must remain in the schema for a minimum of 30-90 days (set based on your release cadence and client deployment speed)
- Use the
@deprecated directive immediately when introducing a replacement: oldField: String @deprecated(reason: "Use newField which returns a typed object") -- never remove first, deprecate first
- Set up
graphql-inspector in CI to automatically block pull requests that introduce breaking changes to the public schema
- Maintain a schema changelog (SCHEMA_CHANGELOG.md) alongside the code with entries for every added type, added field, deprecated field, and reason for deprecation
- Consider schema contracts for large teams: use a schema registry (Apollo Schema Registry or open-source Hive) to track schema versions, validate composition in federated setups, and enforce review gates
Output Format
## GraphQL Schema Design Review
### Domain Model Summary
- Core entities: [list with brief description]
- Key relationships: [entity → entity with cardinality]
- Primary access patterns: [top 5 queries driving design]
- Write operations: [top 5 mutations driving design]
### Schema Design Decisions
| Decision | Choice | Rationale | Trade-offs |
|----------|--------|-----------|------------|
| Pagination style | Cursor (Relay spec) / Offset | [reason] | [trade-off] |
| Error handling | Payload union / errors array | [reason] | [trade-off] |
| Nullability policy | Strict non-null / permissive | [reason] | [trade-off] |
| ID strategy | Global ID (Node) / local ID | [reason] | [trade-off] |
| Naming convention | Standard camelCase/PascalCase | [reason] | [trade-off] |
### Schema Structure
#### Custom Scalars
[List with justification for each non-standard scalar]
#### Interfaces
[List with the contract each interface enforces]
#### Type Definitions (SDL)
[Full SDL for all types, organized as:]
-- Scalars and Enums
-- Interfaces
-- Core object types (most important entities first)
-- Connection types
-- Input types (query filters, mutation inputs)
-- Payload types (mutation returns)
-- Query root
-- Mutation root
#### Relationship Map
[Diagram or table showing type → type traversal paths]
### Validation Results
| Check | Status | Notes |
|-------|--------|-------|
| Naming conventions | ✅ / ⚠️ / ❌ | [details] |
| All mutations have payload types | ✅ / ⚠️ / ❌ | [details] |
| All collections paginated | ✅ / ⚠️ / ❌ | [details] |
| N+1 risks documented | ✅ / ⚠️ / ❌ | [details] |
| Breaking change check | ✅ / ⚠️ / ❌ | [details] |
| Description coverage | ✅ / ⚠️ / ❌ | [details] |
### Evolution Notes
- Fields marked for deprecation: [list]
- Planned additions: [list]
- Breaking changes required (with migration plan): [list]
Rules
-
NEVER remove a field without first deprecating it for at least 30 days. Field removal is a breaking change for all clients even if no client currently uses the field -- unknown clients, generated SDKs, and cached queries all depend on introspection stability. Use @deprecated(reason: "...") and remove only after confirming zero usage via query analytics.
-
NEVER change a field from nullable to non-null without treating it as a breaking change. Making a field non-null (String → String!) breaks any client that handles null values for that field. The reverse (non-null to nullable) is safe. When in doubt, start nullable and tighten later only after all clients are verified.
-
ALWAYS put the errors field on every mutation payload. A mutation that returns createUser: User! has no mechanism to communicate domain errors (email already taken, validation failure) without throwing GraphQL errors, which forces clients to parse error extensions. The errors: [UserError!]! field on a payload type is the correct pattern.
-
NEVER use generic types as mutation inputs. Reusing output types as input types (createUser(user: User)) breaks immediately when the output type adds computed fields, relations, or server-generated fields like id and createdAt. Always create dedicated CreateUserInput, UpdateUserInput types with exactly the fields the client should provide.
-
ALWAYS paginate collections that could exceed 20-30 items in production. A field returning [Post!]! with no pagination argument will eventually return thousands of objects, causing memory and latency problems on both server and client. The only exception is truly bounded collections (a user's 2FA recovery codes, a product's fixed set of images with a hard cap of 10).
-
NEVER design around a specific client's current data shape. Schema design based on "this is what the React component tree looks like today" creates a brittle, client-coupled schema that cannot serve mobile apps, partner integrations, or internal tooling. Design around the domain model; let clients compose what they need.
-
ALWAYS implement the Node interface on top-level entities. The node(id: ID!): Node root query with global IDs (base64-encoded TypeName:localId) is not just a Relay pattern -- it enables any client to efficiently refetch stale data, powers cache normalization in Apollo Client, and provides a universal entity lookup mechanism.
Edge Cases
Migrating a REST API to GraphQL Incrementally
When wrapping existing REST endpoints in a GraphQL schema, resist the urge to mirror the REST resource structure 1:1. REST resources are organized around CRUD operations on individual entities; GraphQL schemas should be organized around how clients actually traverse the data graph. A REST endpoint GET /users/{id}/orders?status=COMPLETED becomes user(id: ID!) { orders(filter: { status: COMPLETED }) { ... } } -- the nesting reflects ownership, not endpoint hierarchy. Use schema stitching or a thin gateway layer so the GraphQL schema can evolve independently of the underlying REST endpoints. Start with read-only query types wrapping existing GET endpoints, then introduce mutations only once the query layer is stable.
Polymorphic Types: Interface vs. Union
The choice between interface and union is often confused. Use an interface when the polymorphic types share a meaningful common contract that clients will query against -- SearchResult types that are all Node implementors with id and title fields should be an interface, because clients can write a fragment that applies to all of them. Use a union when the types are genuinely heterogeneous with no shared fields -- PaymentMethod = CreditCard | BankTransfer | CryptoCurrency should be a union because there is no meaningful shared contract. The practical test: if you find yourself adding shared fields to a union type, convert it to an interface. Inline fragments (... on CreditCard { cardNumber lastFour }) work identically on both.
Multi-Tenancy and Viewer-Scoped Data
In multi-tenant SaaS applications, some data is global (product catalog, public user profiles) and some is viewer-scoped (the current user's orders, their organization's settings). Avoid adding currentUser: User fields directly on other types (order { currentUserCanApprove: Boolean! }) because this couples the schema to the authenticated context and makes the schema unusable for batch processing, admin tools, or unauthenticated access. Instead, put viewer-specific capabilities on a dedicated viewer: Viewer root query type that contains the authenticated user's context. The Viewer type holds orders, permissions, organization -- all scoped to the authenticated session -- while global types remain free of viewer context.
Schema Federation Across Microservices
In Apollo Federation or schema stitching architectures, each subgraph owns a slice of the overall schema. The key design rules are: every entity that is referenced across subgraph boundaries must implement @key with a stable, queryable field (almost always the primary id); subgraphs should extend types only to add fields they own the data for; and no subgraph should extend a type just to add computed fields that belong in the consuming application layer. The main failure mode in federated schemas is ownership ambiguity -- two teams both adding fields to the User type without coordination causes type conflicts at composition time. Establish a schema ownership registry (a simple table in a wiki is sufficient for teams under 20) that maps each type and field to an owning team.
Recursive and Hierarchical Data Structures
Organizational hierarchies, category trees, comment threads, and file systems are common recursive structures. Do not model these with unbounded recursive types (Category { children: [Category!]! }). Instead, add depth control arguments (children(depth: Int = 1): [Category!]!) and document the maximum supported depth. For very deep or very large trees (org charts with 10,000 nodes), consider a flattened list representation at the schema level (organization { allDepartments: [Department!]! } with parentId on each Department) and let the client reconstruct the tree -- this approach is more efficient for large datasets and avoids the query complexity explosion of deeply nested traversals.
Schema Design for Real-Time Subscriptions
Subscriptions share the type system with queries and mutations, but their design requirements differ. A subscription payload should contain the minimum data needed to update the client's local state -- it should not be a full entity dump. Design subscription types to include the event type (UserUpdatedEvent { eventType: UserEventType!, user: User!, changedFields: [String!]! }), not just the current state. This allows clients to apply optimistic updates correctly and distinguish create/update/delete events. Never model a subscription to return a connection type -- subscriptions push individual events, not paginated collections. For filtered subscriptions (onOrderUpdated(orderId: ID!)), document the filter semantics precisely and limit the number of filterable dimensions to prevent server-side subscription fan-out.
Large Schema Governance (50+ Types)
When a schema grows beyond 50 types, tooling and process become as important as design patterns. Split the SDL across multiple files organized by domain (user, product, order, payment) and merge them at build time. Use graphql-schema-linter with a configuration file that enforces description coverage (at least 80% of public fields documented), naming conventions, and deprecation format. Establish a schema review process: every PR that adds or modifies schema definitions requires review by the schema owner (typically a platform or API team member) in addition to code review. Track field usage with GraphQL observability tools to identify candidates for deprecation -- fields with zero usage over 90 days should be deprecated proactively.
Example
Input: "We're building an e-commerce platform with a team of 8 engineers. We have Products with variants (size/color), a Cart and Checkout flow, Orders with line items, and User accounts with addresses. We need to support both a web storefront and a mobile app as clients. What should our GraphQL schema look like?"
Output:
GraphQL Schema Design Review -- E-Commerce Platform
Domain Model Summary
- Core entities: User, Product, ProductVariant, Cart, CartItem, Order, OrderLineItem, Address, Category
- Key relationships:
- User → [Address] (one-to-many, user owns addresses)
- User → [Order] (one-to-many, user owns orders)
- User → Cart (one-to-one, user has one active cart)
- Product → [ProductVariant] (one-to-many, bounded -- max ~50 variants per product)
- Product → [Category] (many-to-many via ProductCategoryEdge)
- Cart → [CartItem] (one-to-many, CartItem references ProductVariant)
- Order → [OrderLineItem] (one-to-many, immutable snapshot of purchase)
- Primary access patterns:
- Product listing with filtering by category, price range, availability (paginated)
- Product detail page with all variants, images, and descriptions
- Cart retrieval with current prices and availability status
- Order history for authenticated user (paginated, filterable by status)
- Checkout flow: validate cart → create order → process payment
- Write operations:
- Add/update/remove cart items
- Create order from cart (checkout)
- Update user profile and addresses
- Apply discount codes to cart
- Cancel order (status transition)
Schema Design Decisions
| Decision | Choice | Rationale | Trade-offs |
|---|
| Pagination style | Cursor (Relay spec) | Product and order lists can be large; real-time inventory changes make offset pagination unreliable | Slightly more complex client code vs. offset |
| Error handling | Payload type with errors: [UserError!]! | Checkout and cart operations have rich domain errors (out of stock, invalid coupon) needing typed responses | Clients must check both errors and the entity field |
| Nullability policy | Strict non-null on reliable data; nullable on computed/external-dependent fields | Reduces client null-checking burden on core fields | Server must guarantee non-null fields -- requires careful resolver implementation |
| ID strategy | Global ID implementing Node interface | Both web and mobile clients use Apollo Client; global IDs enable cache normalization without configuration | Base64 encoding adds minor overhead; local IDs exposed via separate legacyId field for REST migration |
| Naming convention | Standard GraphQL conventions: camelCase fields, PascalCase types, SCREAMING_SNAKE_CASE enums | Consistent with client SDK generation tools and GraphQL community norms | No trade-off for new projects |
Schema Structure
Custom Scalars
DateTime -- ISO-8601 timestamp strings (2024-03-15T10:30:00Z); used on all createdAt, updatedAt, expiresAt fields
Decimal -- Arbitrary-precision decimal as string ("19.99"); used for all monetary amounts to avoid floating-point errors
URL -- Validated URL string; used for imageUrl, thumbnailUrl fields
JSON -- Arbitrary JSON object; used for ProductVariant.attributes to handle the open-ended nature of variant attributes (size charts, material specs)
Interfaces
Node -- id: ID!; implemented by User, Product, ProductVariant, Cart, Order, Address, Category -- enables node(id: ID!) root lookup and Apollo Client cache normalization
UserError -- message: String!, code: String!; base for all domain errors returned in mutation payloads
Type Definitions (SDL)
scalar DateTime
scalar Decimal
scalar URL
scalar JSON
enum OrderStatus {
PENDING
PAYMENT_PROCESSING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
REFUNDED
}
enum CartItemStatus {
AVAILABLE
LOW_STOCK
OUT_OF_STOCK
PRICE_CHANGED
}
enum AddressType {
SHIPPING
BILLING
}
enum SortDirection {
ASC
DESC
}
enum ProductSortField {
PRICE
NAME
CREATED_AT
POPULARITY
}
interface Node {
id: ID!
}
interface UserError {
message: String!
code: String!
}
type ValidationError implements UserError {
message: String!
String
String
NotFoundError implements UserError
String
String
String
OutOfStockError implements UserError
String
String
ProductVariant
Int
Int
InvalidCouponError implements UserError
String
String
String
User implements Node
ID
String
String
String
String
Boolean
DateTime
DateTime
addresses AddressType Address
Address
orders
OrderFilterInput
Int
String
Int
String
OrderConnection
Address implements Node
ID
AddressType
String
String
String
String
String
String
String
String
Boolean
Product implements Node
ID
String
String
String
String
URL
URL
Category
ProductVariant
Decimal
Decimal
Boolean
DateTime
DateTime
ProductVariant implements Node
ID
Product
String
sizeLcolorRedmaterialCottonfitSlim
JSON
String
Decimal
Decimal
Int
Boolean
URL
Category implements Node
ID
String
String
String
Category
childCategories Int Category
products
ProductFilterInput
ProductOrderByInput
Int
String
ProductConnection
Cart implements Node
ID
CartItem
Decimal
Decimal
Decimal
Decimal
String
Boolean
DateTime
CartItem implements Node
ID
ProductVariant
Int
Decimal
Decimal
Decimal
CartItemStatus
Order implements Node
ID
String
OrderStatus
OrderLineItem
Address
Address
Decimal
Decimal
Decimal
Decimal
Decimal
String
String
DateTime
DateTime
DateTime
OrderLineItem implements Node
ID
ProductVariant
String
String
Decimal
Int
Decimal
ProductConnection
ProductEdge
PageInfo
Int
ProductEdge
Product
String
OrderConnection
OrderEdge
PageInfo
Int
OrderEdge
Order
String
PageInfo
Boolean
Boolean
String
String
ProductFilterInput
ID
Decimal
Decimal
Boolean
String
ProductOrderByInput
ProductSortField
SortDirection
OrderFilterInput
OrderStatus
DateTime
DateTime
AddCartItemInput
ID
Int
UpdateCartItemInput
ID
Int
ApplyCouponInput
ID
String
CheckoutInput
ID
ID
ID
Boolean
CreateAddressInput
AddressType
String
String
String
String
String
String
String
String
Boolean
UpdateUserProfileInput
String
String
String
AddCartItemPayload
Cart
CartItem
UserError
UpdateCartItemPayload
Cart
UserError
ApplyCouponPayload
Cart
UserError
CheckoutPayload
Order
UserError
CreateAddressPayload
Address
UserError
UpdateUserProfilePayload
User
UserError
CancelOrderPayload
Order
UserError
Viewer
User
Cart
node ID Node
Viewer
product ID Product
productBySlug String Product
products
ProductFilterInput
ProductOrderByInput
Int
String
ProductConnection
category ID Category
categoryBySlug String Category
Category
cart String Cart
addCartItem AddCartItemInput AddCartItemPayload
updateCartItem UpdateCartItemInput UpdateCartItemPayload
applyCoupon ApplyCouponInput ApplyCouponPayload
checkout CheckoutInput CheckoutPayload
cancelOrder ID CancelOrderPayload
createAddress CreateAddressInput CreateAddressPayload
updateUserProfile UpdateUserProfileInput UpdateUserProfilePayload
Relationship Map
| From | Field | To | Cardinality | Paginated |
|---|
| User | addresses | Address | 1→many | No (bounded by address book size ~10) |
| User | orders | OrderConnection | 1→many | Yes (cursor) |
| Viewer | cart | Cart | 1→1 | No |
| Product | variants | [ProductVariant] | 1→many | No (bounded ~50) |
| Product | categories | [Category] | many→many | No (bounded ~5 per product) |
| Category | products | ProductConnection | many→many | Yes (cursor) |
| Category | childCategories | [Category] | 1→many | No (depth argument limits scope) |
| Cart | items | [CartItem] | 1→many | No (bounded by cart max ~100 items) |
| CartItem | productVariant | ProductVariant | many→1 | No |
| Order | lineItems | [OrderLineItem] | 1→many | No (bounded at checkout time) |
| OrderLineItem | productVariant | ProductVariant | many→1 | No (nullable -- variant may be deleted) |
Validation Results
| Check | Status | Notes |
|---|
| Naming conventions | ✅ | All types PascalCase, fields camelCase, enums SCREAMING_SNAKE_CASE |
| All mutations have payload types | ✅ | All 7 mutations return XxxPayload with errors: [UserError!]! |
| All large collections paginated | ✅ | products, category.products, user.orders use cursor connections |
| Bounded collections not paginated | ✅ | cart.items, order.lineItems, product.variants are bounded by domain |
| N+1 risks documented | ⚠️ | CartItem.productVariant, OrderLineItem.productVariant, Product.categories all require DataLoader batching in resolvers |
| Node interface coverage | ✅ | User, Product, ProductVariant, Cart, CartItem, Order, OrderLineItem, Address, Category all implement Node |
| Breaking change baseline | ✅ | Initial schema -- establish as v0 baseline in schema registry before first deploy |
| Description coverage | ✅ | All types described; non-obvious fields have inline documentation |
Evolution Notes
- Planned additions (non-breaking):
Product.reviews: ReviewConnection, Order.refunds: [Refund!]!, User.wishlist: ProductConnection!
- Known N+1 risks requiring DataLoader:
CartItem.productVariant (batched by variantId), Product.categories (batched by productId), OrderLineItem.productVariant (batched by variantId)
- Future consideration: When adding a loyalty/rewards system, add
User.rewardPoints: Int! and CheckoutInput.rewardPointsToApply: Int -- the payload pattern means no breaking changes to the checkout mutation signature
- Guest checkout: The
cart(token: String!) query supports anonymous carts now; when adding guest checkout (order without User), add guestEmail: String to CheckoutInput and make Order.user: User nullable -- this will be a nullable-ization (non-null → nullable), which is a safe non-breaking change