# GraphQL Schema Specification: [API or graph name]

> Specification of one GraphQL API: schema overview, types, queries,
> mutations, subscriptions, pagination, errors, nullability, naming,
> deprecation, authorisation, query cost limits, persisted queries,
> federation, performance and evolution. Written against the GraphQL
> specification, September 2025 edition (spec.graphql.org), with the
> widely used cursor connections convention for pagination. Replace every
> `[bracketed]` value and every row marked *Example*. Worked examples use
> a bookshop catalogue and ordering graph.

<!--
Guidance: How to use this template
- Keep the SDL (schema definition language) file in the repository as the
  source of truth and link it in section 3. This document explains the
  design decisions and conventions; it does not replace the schema.
- Fill sections 2 (overview), 7 (nullability) and 9 (naming) first. They
  shape everything else and are hard to change after clients ship.
- GraphQL APIs are usually evolved without versions. Record every
  deprecation in section 10 with a removal date and usage evidence.
- Run schema checks (breaking change detection, linting) in CI and link
  the results in section 18.
-->

## Document control

<!-- Guidance: Identify the specification and the schema version it describes. -->

| Field | Value |
|---|---|
| Document ID | [API-GQL-001] |
| Graph / API name | [bookshop-graph] |
| Schema version or tag | [2026-03-14 / commit abc1234] |
| GraphQL spec edition | September 2025 |
| Endpoint | [https://api.example.com/graphql] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Superseded] |
| Owner | [@graph-team] |
| Classification | [Internal / Partner / Public] |
| Last updated | YYYY-MM-DD |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Graph owner / API lead | [@name] | [Approved / Approved with comments / Rejected] | YYYY-MM-DD |
| Security reviewer | [@name] | [Decision] | YYYY-MM-DD |
| Client representative | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Record schema-relevant changes, not only document edits. -->

| Version | Date | Author | Change |
|---|---|---|---|
| [0.1] | YYYY-MM-DD | [@author] | Initial draft |
| [0.2] | YYYY-MM-DD | [@author] | Added subscriptions and persisted queries |

## Contents

1. Introduction
2. Schema overview
3. Types
4. Queries
5. Mutations
6. Subscriptions
7. Nullability
8. Pagination
9. Naming conventions
10. Deprecation and evolution
11. Errors
12. Authorisation
13. Query cost and depth limits
14. Persisted queries and transport
15. Federation and composition
16. Performance
17. Observability
18. Schema governance
19. Glossary and references

## 1. Introduction

<!-- Guidance: Why the graph exists, who calls it and what it is not for. -->

### 1.1 Purpose

[*Example:* The bookshop graph gives the web shop and mobile apps one API
for browsing the catalogue, managing baskets and placing orders. It
replaces four REST backends-for-frontends.]

### 1.2 Consumers

| Client | Type | Main operations | Contact |
|---|---|---|---|
| *Example:* Web shop | First-party web | Catalogue browse, basket, checkout | @web-team |
| *Example:* iOS and Android apps | First-party mobile | Same as web, plus order tracking subscription | @mobile-team |
| *Example:* Partner bookstores | Third-party | Read-only catalogue and stock | @partner-success |
| [Client] | [Type] | [Operations] | [@contact] |

### 1.3 Design principles

- Demand-oriented: the schema models what client screens need, expressed in domain language, not the shape of backing services.
- One graph: one schema per organisation area, so clients do not stitch data together themselves.
- Evolve, do not version: additive change plus deprecation; no `/v2` endpoint.
- Secure by default: every field is authorised server-side, regardless of which client asks.

### 1.4 Out of scope

[*Example:* Warehouse operations and finance reporting stay on their own
APIs; the graph does not expose admin mutations.]

## 2. Schema overview

<!-- Guidance: A map of the main types and how they connect. Readers should understand the graph's shape in one minute. -->

The diagram shows the main object types and their relationships.

```mermaid
classDiagram
  class Query
  class Book {
    +ID id
    +String title
    +Money price
  }
  class Author {
    +ID id
    +String name
  }
  class Order {
    +ID id
    +OrderStatus status
  }
  class Viewer {
    +Customer customer
  }
  Query --> Book : book, books
  Query --> Viewer : viewer
  Book "many" --> "many" Author : authors
  Viewer --> Order : orders
  Order --> Book : lines
```

| Area | Root fields | Owning team | Backing service |
|---|---|---|---|
| *Example:* Catalogue | `book`, `books`, `author`, `search` | Team Catalogue | catalogue-service |
| *Example:* Basket | `viewer.basket`, `addToBasket` | Team Checkout | basket-service |
| *Example:* Orders | `viewer.orders`, `placeOrder`, `orderStatusChanged` | Team Orders | order-service |
| [Area] | [Fields] | [Team] | [Service] |

## 3. Types

<!-- Guidance: Describe the type conventions and list the important types. Link the full SDL file instead of pasting it all. -->

Full schema: [link to `schema.graphql`]. The type system uses the kinds
defined by the GraphQL specification: scalars, objects, interfaces, unions,
enums and input objects.

### 3.1 Custom scalars

Every custom scalar MUST declare `@specifiedBy` with a URL to its format.

```graphql
"An instant in time, RFC 3339 format in UTC, e.g. 2026-03-14T09:30:00Z."
scalar DateTime @specifiedBy(url: "https://www.rfc-editor.org/rfc/rfc3339")

"ISO 4217 currency code, e.g. EUR."
scalar CurrencyCode @specifiedBy(url: "https://www.iso.org/iso-4217-currency-codes.html")
```

### 3.2 Core object types and interfaces

Objects that clients may need to refetch implement the `Node` interface
with a globally unique, opaque `id`. Money is always an object with a
decimal string and a currency, never a float. Enums are used for closed
sets of values that clients branch on; clients MUST handle unknown enum
values gracefully because new values are added without a new version.
Every type and field carries a description string, which becomes the
reference documentation.

```graphql
"An object with a globally unique ID that can be refetched with Query.node."
interface Node {
  id: ID!
}

type Money {
  "Decimal amount as a string to avoid floating-point errors, e.g. \"12.90\"."
  amount: String!
  currency: CurrencyCode!
}

type Book implements Node {
  id: ID!
  isbn13: String!
  title: String!
  authors: [Author!]!
  price: Money!
  publishedOn: String
  stock: StockLevel
}

enum StockLevel {
  IN_STOCK
  LOW_STOCK
  OUT_OF_STOCK
}
```

### 3.3 Type register

| Type | Kind | Description | Owner |
|---|---|---|---|
| *Example:* `Book` | Object (implements `Node`) | A sellable edition identified by ISBN-13 | Team Catalogue |
| *Example:* `SearchResult` | Union (`Book`, `Author`) | One hit in a catalogue search | Team Catalogue |
| *Example:* `PaymentMethodInput` | Input object (`@oneOf`) | Exactly one of card token or gift card code | Team Checkout |
| [Type] | [Kind] | [Description] | [Owner] |

## 4. Queries

<!-- Guidance: List root query fields with arguments, authorisation and cost. Prefer a few well-designed entry points over many specialised ones. -->

```graphql
type Query {
  "Refetch any object by its global ID."
  node(id: ID!): Node
  book(id: ID!): Book
  books(first: Int, after: String, filter: BookFilter): BookConnection!
  search(text: String!, first: Int = 20, after: String): SearchResultConnection!
  "The authenticated customer, or null for anonymous callers."
  viewer: Viewer
}
```

| Field | Arguments | Auth | Cost | Notes |
|---|---|---|---|---|
| *Example:* `books` | `first` (max 50), `after`, `filter` | Public | 1 + `first` | Filter by genre, author, price range |
| *Example:* `viewer` | none | Customer token | 1 | Entry point for all personal data |
| [Field] | [Arguments] | [Auth] | [Cost] | [Notes] |

## 5. Mutations

<!-- Guidance: Use one input object and one payload type per mutation. Payloads let you add fields later without breaking clients and carry expected (business) errors as data. -->

Conventions:

- Name mutations verb first: `addToBasket`, `placeOrder`, `cancelOrder`.
- Each mutation takes a single argument `input: <MutationName>Input!` and returns `<MutationName>Payload!`.
- Payloads return the changed object and a list of typed `userErrors` for expected failures.
- Mutations with side effects accept a `clientMutationId` or an idempotency key in the input.

```graphql
input PlaceOrderInput {
  basketId: ID!
  paymentMethod: PaymentMethodInput!
  idempotencyKey: String!
}

input PaymentMethodInput @oneOf {
  cardToken: String
  giftCardCode: String
}

type PlaceOrderPayload {
  order: Order
  userErrors: [PlaceOrderError!]!
}

type PlaceOrderError {
  code: PlaceOrderErrorCode!
  message: String!
  "Path to the input field that caused the error, e.g. [\"paymentMethod\", \"cardToken\"]."
  field: [String!]
}

enum PlaceOrderErrorCode {
  BASKET_EMPTY
  ITEM_OUT_OF_STOCK
  PAYMENT_DECLINED
}

type Mutation {
  placeOrder(input: PlaceOrderInput!): PlaceOrderPayload!
}
```

## 6. Subscriptions

<!-- Guidance: Use subscriptions only for events clients must see quickly. Polling a query is simpler and often enough. State the transport, delivery guarantees and limits. -->

| Subscription | Event source | Auth | Delivery | Limits |
|---|---|---|---|---|
| *Example:* `orderStatusChanged(orderId: ID!)` | `OrderStatusChanged` broker event | Customer token, must own order | At most once; client refetches on reconnect | 5 active per connection |
| [Subscription] | [Source] | [Auth] | [Guarantee] | [Limits] |

```graphql
type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}
```

Transport: [*Example:* GraphQL over WebSocket using the `graphql-transport-ws`
subprotocol; connection authenticated in the `connection_init` payload].

## 7. Nullability

<!-- Guidance: Non-null (!) is a promise. If a non-null field fails to resolve, the null propagates to the nearest nullable parent and can blank out large parts of a response. Decide the policy explicitly. -->

| Rule | Policy |
|---|---|
| Object fields backed by another service | Nullable, so one failing service does not null the parent |
| Identifiers and fields stored with the object | Non-null |
| Lists | `[Item!]!`: the list and its items are non-null; an empty list means none |
| Arguments | Non-null only when required; optional arguments have defaults where sensible |
| Changing nullability | Output: non-null to nullable is breaking for clients; input: nullable to non-null is breaking |

## 8. Pagination

<!-- Guidance: Use cursor-based connections for every list that can grow. The connection pattern (edges, node, cursor, pageInfo) is a community convention published as the Relay cursor connections specification, not part of the GraphQL specification itself. -->

```graphql
type BookConnection {
  edges: [BookEdge!]!
  pageInfo: PageInfo!
  "Approximate; may be omitted for expensive filters."
  totalCount: Int
}

type BookEdge {
  cursor: String!
  node: Book!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
```

- `first` default [20], maximum [50]; larger values return a validation error.
- Cursors are opaque, base64-encoded and valid for at least [24 hours].
- Backward pagination (`last`, `before`) is supported only where [listed].

## 9. Naming conventions

<!-- Guidance: Consistent names make the graph guessable. Lint them. -->

| Element | Convention | Example |
|---|---|---|
| Types, interfaces, unions, enums, inputs | PascalCase | `Book`, `SearchResult`, `PlaceOrderInput` |
| Fields and arguments | camelCase | `publishedOn`, `isbn13` |
| Enum values | SCREAMING_SNAKE_CASE | `OUT_OF_STOCK` |
| Mutations | verb + noun | `cancelOrder` |
| Input types | `<Mutation>Input` | `CancelOrderInput` |
| Payload types | `<Mutation>Payload` | `CancelOrderPayload` |
| Boolean fields | `is` / `has` / `can` prefix | `isGift`, `canCancel` |
| Subscriptions | noun + past-tense verb | `orderStatusChanged` |

Names come from the domain's ubiquitous language, never from backing
service or table names.

## 10. Deprecation and evolution

<!-- Guidance: Evolve continuously, deprecate with a reason, remove only when usage is zero or the removal date has passed. The September 2025 edition allows @deprecated on fields, arguments, input fields and enum values. -->

```graphql
type Book implements Node {
  id: ID!
  "Use price.amount and price.currency."
  priceInCents: Int @deprecated(reason: "Use `price`. Removal after 2026-12-31.")
  price: Money!
}
```

| Schema coordinate | Deprecated on | Replacement | Usage last 30 days | Removal date | Status |
|---|---|---|---|---|---|
| *Example:* `Book.priceInCents` | YYYY-MM-DD | `Book.price` | 1.2% of operations (iOS 5.x) | YYYY-MM-DD | Waiting for app adoption |
| [Type.field] | YYYY-MM-DD | [Replacement] | [Usage] | YYYY-MM-DD | [Status] |

Policy: [*Example:* minimum 90 days after deprecation and two app release
cycles; removal requires zero usage from first-party clients for 14 days].

## 11. Errors

<!-- Guidance: Separate expected business outcomes (modelled in the schema as payload errors or union results) from unexpected failures (the top-level "errors" list of the response). Clients cannot program reliably against free-text messages. -->

| Category | Where it appears | Example |
|---|---|---|
| Validation of the document | Top-level `errors`, no `data` | Unknown field `pricee` |
| Business outcome expected by the client | Payload `userErrors` or union result type | `ITEM_OUT_OF_STOCK` |
| Authorisation failure on a field | Top-level `errors` with `extensions.code = "FORBIDDEN"`, field null | Reading another customer's order |
| Downstream failure | Top-level `errors` with `extensions.code = "UNAVAILABLE"`, partial data | Stock service timeout |

```json
{
  "data": {"book": {"title": "Dune", "stock": null}},
  "errors": [{
    "message": "Stock information is temporarily unavailable.",
    "path": ["book", "stock"],
    "extensions": {"code": "UNAVAILABLE", "traceId": "4bf92f35"}
  }]
}
```

Error messages MUST NOT include stack traces or internal hostnames. The
list of `extensions.code` values is fixed: [UNAUTHENTICATED, FORBIDDEN,
NOT_FOUND, BAD_USER_INPUT, RATE_LIMITED, UNAVAILABLE, INTERNAL].

## 12. Authorisation

<!-- Guidance: Authenticate at the transport, authorise in the domain layer per object and field. Never rely on clients not asking for a field. -->

| Rule | Implementation |
|---|---|
| Authentication | OAuth 2.0 bearer token validated at the gateway; anonymous allowed for catalogue only |
| Object-level checks | Resolvers call the owning service, which checks ownership (`order.customerId == token.sub`) |
| Field-level checks | Sensitive fields (`Customer.email`) require scope `customer:read:pii` |
| Introspection | Disabled for public traffic in production; enabled for authenticated developers |
| `node(id:)` | Applies the same checks as the typed field; never a bypass |

## 13. Query cost and depth limits

<!-- Guidance: GraphQL lets clients ask for a lot in one request. Limit depth, breadth and calculated cost, and tell clients their budget. -->

Cost is calculated statically before execution. Each field costs 1; a
connection multiplies its children by its `first` argument:

$$\text{cost}(f) = 1 + n_f \sum_{c \in \text{children}(f)} \text{cost}(c)$$

where $n_f$ is the page size for connections and 1 otherwise.

| Limit | Value | Response when exceeded |
|---|---|---|
| Maximum depth | [10] | Validation error, `extensions.code = "QUERY_TOO_DEEP"` |
| Maximum cost per operation | [1,000] | Validation error, `QUERY_TOO_COMPLEX` |
| Cost budget per client per minute | [50,000] | `RATE_LIMITED`, with `Retry-After` header |
| Maximum aliases per operation | [30] | Validation error |
| Request body size | [100 KB] | HTTP 413 |

## 14. Persisted queries and transport

<!-- Guidance: First-party clients should send registered operations by ID. It shrinks requests, enables CDN caching and blocks arbitrary queries. -->

| Topic | Decision |
|---|---|
| HTTP transport | POST `application/json` for all operations; GET allowed for persisted queries to enable caching. Follows the GraphQL over HTTP working draft. |
| Persisted queries | First-party clients register operations at build time; server accepts only known hashes from first-party client IDs |
| Third-party clients | Arbitrary operations allowed, subject to section 13 limits |
| Caching | `Cache-Control` set per operation from the lowest field max-age; personalised data is `private` |
| Batching | [Not supported / Up to 5 operations] |

## 15. Federation and composition

<!-- Guidance: Only if the graph is composed from subgraphs. Record which team owns which types and how entities are shared. Name the federation technology and version you use; composition is not part of the GraphQL specification. -->

| Subgraph | Owner | Entities owned | Entities extended | Router |
|---|---|---|---|---|
| *Example:* catalogue | Team Catalogue | `Book` (key `id`), `Author` | none | [Router product and version] |
| *Example:* inventory | Team Stock | none | `Book.stock` | Same |
| *Example:* orders | Team Orders | `Order` | `Customer.orders` | Same |
| [Subgraph] | [Team] | [Entities] | [Extensions] | [Router] |

Composition checks run in CI; a subgraph change that breaks composition or
an active client operation is blocked.

## 16. Performance

<!-- Guidance: The classic GraphQL problem is N+1 calls: one call for a list, then one per item for a nested field. Batch and cache per request. -->

- Resolvers that load by key use a per-request batching loader (the DataLoader pattern) so `books { authors }` makes one author call per request, not one per book.
- Loaders cache only within one request; cross-request caching uses the services' own caches.
- Target latency: p95 [250 ms] for persisted first-party queries, p99 [800 ms].
- Resolver timeouts: [2 s] per downstream call; the field returns null with an `UNAVAILABLE` error when exceeded.

The sequence below shows batched loading for a list query.

```mermaid
sequenceDiagram
  participant C as Client
  participant G as Graph server
  participant CAT as Catalogue service
  participant AUT as Author service
  C->>G: query books(first: 20) { title authors { name } }
  G->>CAT: GET /books?limit=20
  CAT-->>G: 20 books with authorIds
  G->>AUT: POST /authors:batchGet (35 unique IDs)
  AUT-->>G: 35 authors
  G-->>C: data
```

## 17. Observability

<!-- Guidance: Measure per operation and per field, not only per HTTP request. All GraphQL traffic hits one URL. -->

| Signal | What is captured |
|---|---|
| Metrics | Requests, errors and latency per operation name and client name; resolver latency for top fields |
| Traces | One span per operation, child spans per downstream call; trace ID in error extensions |
| Field usage | Field-level usage per client, retained [90 days], used for deprecation decisions |
| Logs | Operation name, hash, client, duration, error codes; never variables containing personal data |

## 18. Schema governance

<!-- Guidance: Who can change the schema and what checks protect clients. -->

| Check | Tool | Blocking |
|---|---|---|
| SDL lint (naming, descriptions on all types and fields) | [Tool] | Yes |
| Breaking change detection against production schema | [Tool] | Yes, unless approved |
| Validation of registered client operations against new schema | [Tool] | Yes |
| Schema review by graph owners | Pull request | Yes for new root fields |

Change process: a schema change starts as a pull request against the SDL
file, owned by the team that owns the affected types. The graph owners
review new root fields, new cross-team relationships and anything that
changes nullability or authorisation. Approved changes are published to the
schema registry, which notifies client teams of new deprecations.

Clients must send an operation name and a client name header
(`[x-client-name]`) so usage can be attributed.

## 19. Glossary and references

<!-- Guidance: Terms specific to this graph and the external documents relied on. -->

| Term | Meaning |
|---|---|
| SDL | Schema definition language, the textual form of a GraphQL schema |
| Schema coordinate | A string identifying a schema element, such as `Book.price` |
| Connection | Cursor-paginated list wrapper with edges and page info |
| Persisted query | An operation registered in advance and called by hash or ID |
| Subgraph | A separately owned schema composed into the supergraph |

References: GraphQL specification September 2025 (https://spec.graphql.org/September2025/);
Relay cursor connections specification; GraphQL over HTTP working draft;
[federation documentation used].

## Standard and licence note

Structure aligned with the GraphQL specification, September 2025 edition
(https://spec.graphql.org/), for the type system, directives (`@deprecated`,
`@specifiedBy`, `@oneOf`), schema coordinates, error and response format.
Pagination follows the Relay cursor connections convention; federation and
the GraphQL over HTTP transport are referenced as separate documents. This
template is original text written for this collection and reproduces no
text from those specifications; consult them for normative requirements.
Template licence: MIT.
