git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/graphql-schema-design" ~/.claude/skills/intense-visions-harness-engineering-graphql-schema-design-0b8339 && rm -rf "$T"
agents/skills/codex/graphql-schema-design/SKILL.mdGraphQL Schema Design
Design expressive, evolvable GraphQL schemas with clear type hierarchies and strong nullability contracts
When to Use
- Designing a new GraphQL API from scratch
- Adding types, queries, or mutations to an existing schema
- Deciding between interfaces, unions, and concrete types
- Establishing nullability conventions for a team
- Planning schema evolution without breaking existing clients
Instructions
-
Start with the domain, not the UI. Model your schema around business entities (Order, Product, User), not around specific screens or components. A well-modeled domain schema serves multiple clients without per-client hacks.
-
Use non-null by default. Mark fields as
(non-null) unless the field genuinely can be absent. Non-null fields simplify client code by eliminating null checks. Reserve nullable fields for truly optional data (e.g.,String!
).middleName: String -
Prefer specific types over generic ones. Use custom scalars (
,DateTime
,URL
) instead of plainEmailAddress
for fields with validation semantics. Use enums for closed sets of values.String
scalar DateTime scalar URL enum OrderStatus { PENDING CONFIRMED SHIPPED DELIVERED CANCELLED } type Order { id: ID! status: OrderStatus! createdAt: DateTime! trackingUrl: URL }
- Use interfaces for shared field contracts. When multiple types share fields and clients query them polymorphically, define an interface. Use unions when types share no fields but appear in the same list.
interface Node { id: ID! } interface Timestamped { createdAt: DateTime! updatedAt: DateTime! } type User implements Node & Timestamped { id: ID! createdAt: DateTime! updatedAt: DateTime! name: String! } union SearchResult = User | Product | Order
- Design mutations around actions, not CRUD. Name mutations after the business action:
,cancelOrder
,approveRefund
— notinviteTeamMember
. Each mutation should have a dedicated input type and a dedicated payload type.updateOrder(status: CANCELLED)
input CancelOrderInput { orderId: ID! reason: String! } type CancelOrderPayload { order: Order! refundAmount: Money errors: [UserError!]! } type Mutation { cancelOrder(input: CancelOrderInput!): CancelOrderPayload! }
- Always include a
type in mutation payloads. This separates expected domain errors (validation failures, business rule violations) from unexpected system errors (which use GraphQL's top-levelUserError
array).errors
type UserError { field: [String!] message: String! code: ErrorCode! }
-
Use the Relay connection spec for paginated lists. Even if you do not use Relay on the client, the
pattern is well-understood, cursor-based, and forward-compatible.Connection/Edge/PageInfo -
Version through evolution, not URL paths. Add new fields freely. Deprecate old fields with
. Never remove fields without a deprecation period and client migration.@deprecated(reason: "Use newField instead") -
Keep the schema file as the source of truth. Whether you use schema-first or code-first, ensure there is one canonical
file (or set of files) that documents every type. Generate code from the schema, not the other way around..graphql -
Document with descriptions. Add descriptions above types and fields — they appear in GraphiQL/Apollo Studio and serve as living API docs.
""" A customer order containing one or more line items. """ type Order { """ Unique identifier for the order. """ id: ID! }
Details
Naming conventions: Types are
PascalCase, fields are camelCase, enums are SCREAMING_SNAKE_CASE. Input types end with Input, payload types end with Payload.
Nullability trade-offs: Non-null fields are safer for clients but less forgiving for servers — if a non-null resolver throws, the error bubbles up to the nearest nullable parent, potentially nullifying an entire object. Place nullable "firewalls" at strategic points (e.g., nullable list items) to limit blast radius.
Schema stitching vs. federation: For monolithic APIs, a single schema file works. For microservices, prefer Apollo Federation where each service owns its slice of the graph and extends shared types with
@key.
Anti-patterns to avoid:
- Generic
mutations with a giant optional input type — they are unvalidatable and untraceableupdate - Deeply nested types without pagination — they cause unbounded query cost
- Using
scalar as a catch-all — it defeats the purpose of a typed schemaJSON - Mixing authentication concerns into the schema (use directives or middleware instead)
Source
https://graphql.org/learn/schema/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: graphql-resolver-pattern, graphql-pagination-patterns, graphql-federation-pattern, api-resource-modeling, api-field-selection
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.