# GraphQL — Part 1: Schema, Queries, and Resolvers

This topic assumes the REST-based model built up across the rest of this group (`01-rest_fundamentals` through `11-api_documentation`). GraphQL is presented here as an alternative to that model, not a superset — this part covers the schema/type system, how a query becomes data, and the single-endpoint model. Part 2 covers the N+1 query problem and DataLoader batching, over/under-fetching compared to REST, subscriptions, and schema evolution.

## 1. One Endpoint, a Query Language Instead of Many URLs

A REST API exposes many URLs, each returning a fixed shape (`GET /orders/482` always returns the same fields). GraphQL exposes exactly **one** endpoint (conventionally `POST /graphql`), and the client's request body — the *query* — specifies exactly which fields it wants back:

```graphql
query {
  order(id: 482) {
    id
    status
    customer {
      name
    }
  }
}
```

```json
{
  "data": {
    "order": { "id": 482, "status": "confirmed", "customer": { "name": "Ana" } }
  }
}
```

The server never sends fields the client didn't ask for, and the client never has to make a second request to fetch `customer.name` — both problems that REST's fixed-shape responses run into (over-fetching and under-fetching, covered in depth in Part 2).

## 2. The Schema: a Strongly-Typed Contract

Every field the API can ever return is declared up front, in the **Schema Definition Language (SDL)**:

```graphql
type Order {
  id: ID!
  status: OrderStatus!
  total: Int!
  customer: Customer!
  items: [OrderItem!]!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  CANCELLED
}

type Query {
  order(id: ID!): Order
  orders(customerId: ID!): [Order!]!
}
```

`!` marks a field **non-nullable** — the server guarantees it's never `null` in a valid response (an "Order" always has a `status`, an `Order` field on `Query` may legitimately return `null` if no order with that id exists). This schema is the entire contract: a client can only request fields that exist on it, and a query requesting a field that doesn't exist fails **before execution**, at validation time — this is GraphQL's equivalent of REST's OpenAPI schema (`backend/api-design/11-api_documentation`), except it's enforced by the GraphQL engine itself at request time, not just documented separately.

## 3. Queries vs. Mutations

Reads and writes are distinct root types, mirroring (loosely) REST's GET-is-safe vs POST/PUT/DELETE-changes-state distinction:

```graphql
type Mutation {
  cancelOrder(id: ID!): Order!
  createOrder(input: CreateOrderInput!): Order!
}

input CreateOrderInput {
  customerId: ID!
  itemIds: [ID!]!
}
```

```graphql
mutation {
  cancelOrder(id: 482) {
    id
    status
  }
}
```

A mutation's response can (and usually should) select fields back from the *result* of the write, in the same request — a client cancelling an order gets the updated `status` back in the same round trip, without a follow-up `GET`.

## 4. Resolvers: Where Fields Actually Get Their Data

Every field in the schema is backed by a **resolver function** — the code that actually produces that field's value, given its parent object:

```ts
const resolvers = {
  Query: {
    order: async (_parent, args: { id: string }, ctx: Context) => {
      return ctx.db.orders.findById(args.id); // resolves the Query.order field
    },
  },
  Order: {
    customer: async (parent: OrderRow, _args, ctx: Context) => {
      return ctx.db.customers.findById(parent.customerId); // resolves Order.customer, given the Order already resolved
    },
  },
};
```

The GraphQL engine walks the query, calling `Query.order`'s resolver first (producing an `Order` row), then — because the query also asked for `customer.name` — calling `Order.customer`'s resolver with that row as its `parent` argument, and so on recursively until every requested field has a resolver-produced value. This resolver-per-field execution model is the root cause of Part 2's N+1 problem: each nested resolver call is, by default, independent and unaware of its siblings.

## Common Pitfalls

1. **Treating the schema as optional documentation.** Unlike a REST OpenAPI spec (which can silently drift from the code, per `backend/api-design/11-api_documentation`), a GraphQL schema is enforced live — a query for a non-existent field is rejected before any resolver runs, which is a real guarantee, not just a convention.
2. **Confusing `null` with "field doesn't exist."** A non-nullable (`!`) field that a resolver returns `null` for is a **server error**, not a valid empty result — GraphQL engines reject the entire response subtree in that case.
3. **Writing a resolver per field without considering the whole request.** Section 4's naive per-field resolution is exactly what causes N+1 query storms — Part 2 covers the fix.
4. **Putting side effects in a Query resolver.** `Query` fields are meant to be read-only, mirroring REST's GET-is-safe convention — a "query" that secretly writes breaks client assumptions about retries and caching.

## Quick Reference

```
ONE endpoint (POST /graphql), client's QUERY selects the exact fields
Schema (SDL)  -- type Order { field: Type! }, enforced at request time
Query          -- reads, mirrors GET
Mutation        -- writes, mirrors POST/PUT/DELETE, can select result fields in the same request
Resolver        -- one function per field, given (parent, args, context)
! suffix         -- non-nullable; a null there from a resolver is a SERVER ERROR
```

Continue to `api_graphql_theory_2.md` for the N+1 problem, DataLoader batching, over/under-fetching vs REST, subscriptions, and schema evolution.
