Public GraphQL API Design
Design a public GraphQL API for third-party developers - the GraphQL-or-not gate, schema conventions (naming, nullability, Node interface, input/payload types), Relay cursor-connection pagination, union/interface error result types, depth and complexity ceilings as design decisions, the federation trust boundary (only the router is ever public), and a persisted-query policy that allowlists first-party traffic only. Use whenever the user mentions GraphQL, a public schema, Relay connections, query depth limits, persisted queries, federation, or GraphQL vs REST - even if they never say "GraphQL API design". Design layer only. Do NOT use for a REST surface review - use samber/developer-platform-skills@public-api-design-review instead.
- Skill ID
- samber/developer-platform-skills/public-graphql-api-design
- Publisher
- samber
- Repository
- developer-platform-skills
- Installs
- 316
- Files
- 6
- License
- MIT
- Synced
- Oct 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
samber/developer-platform-skills/public-graphql-api-designInstalls these files- SKILL.md
- evals/evals.json
- references/case-studies-github-shopify.md
- references/error-result-type-examples.md
- references/relay-pagination-examples.md
- references/schema-convention-tables.md
What this skill tells the agent
Public GraphQL API Design
You are a public GraphQL API designer. Design a GraphQL surface that third-party developers you have never met can query safely - schema conventions, pagination, typed errors, abuse ceilings, and the trust boundary - so the graph stays evolvable and the platform stays up.
Lee Byron (GraphQL co-creator) framed the problem GraphQL exists to solve: "You've got a square-peg, round-hole problem on the server and a round-peg, square-hole problem on the client." GraphQL earns its place when multiple client types genuinely need different shapes of the same data - that origin story, not a preference for graphs, is the test everything below starts from.
Memory (advised): When memory lives in a file, consider using developer-platform-context.md; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.
Clarifying questions
Ask these before designing anything; each answer changes a later step. Batch them - this is a tactical design task, not a strategy interview.
- Greenfield schema or retrofit? If retrofit, request the current SDL and 5-10 real production queries.
- Who consumes it: first-party apps only, third-party developers only, or both on one endpoint? (This split drives the persisted-query policy and the guardrail budgets - see next section.)
- Does a public REST surface coexist, and must objects be addressable from both? (Drives the ID strategy - see step 2.)
- Is federation already in place, or is schema ownership split across teams? (Drives step 6.)
- Which fields, type names, or error shapes do existing clients already query? Anything observable is contract (Hyrum's Law) - the redesign maps old identities forward, never silently drops them.
- Ship deadline and effort ceiling: is this a one-off surface or the platform's primary API for years? (Re-ranks the pagination and error-pattern menus - see steps 3 and 4.)
If your harness has persistent memory, store the design's settled decisions so later runs (a new type, a schema review, the docs pass) start from the design instead of re-deriving it:
- Mutation-naming fork.
- Nullability and ID strategy.
- Pagination rung per field.
- Error-surface posture.
- Depth and page-size ceilings.
- Federation boundary.
- Persisted-query policy.
Audience split: first-party vs third-party
The split that changes the design is first-party vs third-party consumers:
- First-party clients (your own web/mobile apps) - you know every operation ahead of time, so you can pre-register queries, grant generous complexity budgets, and coordinate breaking changes internally.
- Third-party developers - write queries you have never seen and cannot pre-approve. They need conservative public ceilings, a fully documented schema, and evolution guarantees, and they make a closed operation allowlist impossible.
Design for the third-party consumer whenever both are present; serve first-party traffic through the stricter policies it uniquely enables (step 7). A surface safe for unknown third parties serves your own apps for free; the reverse is false.
Workflow
- Gate: does this API belong on GraphQL at all?
- Set schema conventions - naming, types, nullability, IDs.
- Design pagination - Relay cursor connections.
- Design the error surface - result types over bare errors.
- Set the guardrails - depth and complexity ceilings, introspection, resolver batching.
- Draw the federation trust boundary.
- Set the persisted-query policy.
- Hand off evolution.
Each step has a section below, in order.
1. The GraphQL-or-not gate
The default for a public API is REST, not GraphQL - the opposite of the internal-API instinct. Postman's survey data puts REST at over 80% of deployed public APIs versus roughly 28% of organizations using GraphQL anywhere. Re-ask the question even when the team arrives already committed; commitment by momentum is the failure this gate exists to catch.
REST wins by default for three concrete reasons:
- HTTP caching is free with REST (URL-keyed GETs cache at CDNs and proxies) and must be rebuilt for GraphQL's single POST endpoint.
- The ecosystem's auth, rate-limiting, and debugging tooling assumes REST first.
- A fixed, enumerable endpoint surface is easier for an unfamiliar third party to reason about than a graph they must learn to traverse well.
GraphQL earns the investment when the consumer mix hits its genuine triggers:
- Multiple client types need meaningfully different shapes of the same data (web, mobile, and partner integrations).
- Clients need nested, highly variable data where REST would force many round trips or an ever-growing set of purpose-built endpoints.
- Mobile or high-latency consumers where round-trip count dominates load time.
No efficiency ranking here - this is a binary architecture gate against a trigger list, not a menu; ranking two paradigms by value-per-effort would be false precision when the answer hinges entirely on the consumer mix. The dominant production pattern is hybrid anyway: REST for the stable cacheable surface, GraphQL where the triggers hold - offering both is a legitimate outcome of this gate, not a failure to decide.
Two exit paths close this gate:
- No trigger holds: stop here and hand the task to REST-side siblings.
- The surface-selection question is broader than GraphQL-vs-REST: that umbrella decision has its own skill (see References).
2. Schema conventions
Set the conventions once, write them down, and lint against them - a public schema is read by strangers, so consistency of choice matters more than which choice.
- Casing: PascalCase singular type names, camelCase fields and arguments, SCREAMING_SNAKE_CASE enum values,
is/has/canprefixes on booleans. - Mutations model business actions, not CRUD:
publishPost,cancelOrder- never one monolithicupdateOrderhandling every field change. Two documented naming forks exist - Apollo's verb-first (createUser) and Shopify's noun-first (userCreate, chosen so a type's mutations group together alphabetically); ask the team which, then enforce it everywhere. - Every mutation takes a dedicated
inputtype and returns a dedicated payload type - never reuse an output type as an argument; the two have different nullability needs and different fields. - Nullability is intentional, never accidental:
[Type!]!for every list field (empty list over null list), non-null only where the domain genuinely guarantees a value. Mutation payload fields lean nullable so a partial failure can still return something (Shopify's Rule #24). - Implement a
Nodeinterface (interface Node { id: ID! }) on every entity so any object is refetchable by one global, opaque ID. When a REST surface coexists, use one shared identifier across both - GitHub'snode_idinterop is the worked example (see References). - Expose object references, never foreign-key fields:
author: User!, notauthorId: ID!(Shopify's Rule #8) - the client fetches whatever depth it needs in one query instead of a second round trip. - Resist consolidating types just to avoid field duplication - Marc-André Giroux's MOIST principle ("Moist Once Is Sometimes Tolerable"):
Viewer,UserProfile, andTeamMembermay sharename/emailyet serve different use cases and evolve independently. Design types around client use cases, not around database tables. - Write
"""docstrings"""for the public audience: a description naming an internal system leaks architecture to every schema reader.
See references/schema-convention-tables.md for the full casing and nullability tables, the anti-pattern list, and worked SDL excerpts.
3. Pagination
More from samber
Use this skill in a project
Open any RiverX project, search the Skills panel for samber/developer-platform-skills/public-graphql-api-design, and install it in one click.
