Skip to content
5 min read·Lesson 5 of 10

GraphQL Fundamentals

A schema-first query language for APIs. Types, queries, mutations, resolvers, and what makes GraphQL different.

GraphQL was created at Facebook to solve a specific problem: mobile clients on slow networks needed varied subsets of data, and REST forced them to either over-fetch or make many round trips. The solution was a typed query language and a single endpoint that lets each client describe exactly what it wants.

The Shape of a GraphQL API

One endpoint, one schema, three operation types:

  • Query — read data.
  • Mutation — change data.
  • Subscription — receive real-time updates over WebSockets.

The Schema

type Order {
  id: ID!
  status: OrderStatus!
  amount: Int!
  currency: String!
  customer: Customer!
  lineItems: [LineItem!]!
  createdAt: DateTime!
}

enum OrderStatus { OPEN PAID CANCELLED }

type Customer {
  id: ID!
  email: String!
  orders(limit: Int = 25): [Order!]!
}

type Query {
  order(id: ID!): Order
  orders(status: OrderStatus, limit: Int = 25, cursor: String): OrderConnection!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  cancelOrder(id: ID!): Order!
}
  • ! means non-null.
  • [Type!]! means a non-null list of non-null items.
  • Enums and input types are first-class.

A Client Query

query GetMyOrders {
  orders(status: OPEN) {
    edges {
      node {
        id
        amount
        customer { email }
      }
    }
  }
}

The response mirrors the query exactly:

{
  "data": {
    "orders": {
      "edges": [
        { "node": { "id": "ord_1", "amount": 4999, "customer": { "email": "a@b.com" } } }
      ]
    }
  }
}

Resolvers

Each field in the schema has a resolver function on the server:

const resolvers = {
  Query: {
    order: (_, { id }, ctx) => ctx.db.orders.byId(id),
    orders: (_, args, ctx) => ctx.db.orders.list(args),
  },
  Order: {
    customer: (order, _, ctx) => ctx.db.customers.byId(order.customerId),
    lineItems: (order, _, ctx) => ctx.db.lineItems.byOrder(order.id),
  },
  Mutation: {
    createOrder: (_, { input }, ctx) => ctx.svc.createOrder(input, ctx.user),
  },
};

The runtime walks the query and calls resolvers; resolvers return data; data flows back into the JSON response.

The N+1 Problem

If a client asks for 100 orders and each order resolves customer with a separate database call, that is 101 queries. Solution: DataLoader — batch and cache resolver lookups within a single request.

const customerLoader = new DataLoader(async (ids: string[]) => {
  const rows = await db.customers.byIds(ids);
  return ids.map(id => rows.find(r => r.id === id));
});

// in resolver:
customer: (order, _, ctx) => ctx.loaders.customer.load(order.customerId)

DataLoader is part of every serious GraphQL server.

Pagination

The community-standard pattern is connections with cursors:

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
}
type OrderEdge {
  node: Order!
  cursor: String!
}
type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

Verbose, but it forces every list to be paginated by construction.

Errors

GraphQL responses can have both data and errors at once — partial success is a first-class concept:

{
  "data": { "order": null },
  "errors": [
    { "message": "Not found", "path": ["order"], "extensions": { "code": "NOT_FOUND" } }
  ]
}

Status codes are usually 200 even when errors occur. Clients must check errors.

Subscriptions

Real-time push over WebSockets:

subscription {
  orderUpdated(customerId: "cus_42") {
    id
    status
  }
}

Useful for live dashboards, chat, notifications. Adds significant operational complexity (connection state, scaling).

Tooling You Will Use

  • Servers: Apollo Server, GraphQL Yoga, Mercurius, Hasura, PostGraphile.
  • Clients: Apollo Client, urql, Relay (Meta).
  • Codegen: GraphQL Code Generator — types from your schema, autocompleted everywhere.
  • Federation: Apollo Federation / GraphQL Mesh — compose multiple subgraphs into one.
  • Introspection: query the schema itself; powers playgrounds like Apollo Sandbox.

Strengths

  • One round trip for data spread across resources.
  • No over-fetching; clients pick fields.
  • Strongly typed end to end.
  • Self-documenting via introspection.

Costs

  • HTTP caching is harder (one URL, varying queries).
  • N+1 must be solved explicitly.
  • Query complexity must be limited or attackers will craft expensive queries.
  • Server-side authorisation must be enforced per field, not per endpoint.

Cert Mapping

CertGraphQL scope
AWS SAAAWS AppSync (managed GraphQL)
AWS Data EngineerAPI exposure of curated data products

The next lesson directly compares REST and GraphQL so you can pick well.

Key Takeaways

  • GraphQL exposes one endpoint with a typed schema; clients ask for exactly what they need.
  • The schema is the contract — types, queries, mutations, and subscriptions live there.
  • Resolvers are functions that fetch the data for each field.
  • GraphQL solves over-fetching and N+1 client requests but introduces server complexity.
  • Tooling (Apollo, urql, codegen) makes GraphQL pleasant; without it, less so.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →