Skip to content

GraphQL — Basics

Query language + runtime for APIs. Single endpoint (typically POST /graphql); client specifies exactly what fields to fetch.

  • Schema — type system (SDL: schema definition language).
  • Query — read.
  • Mutation — write (also returns data).
  • Subscription — long-lived stream (typically over WebSocket).
  • Resolver — function returning value for a field.
  • Context — per-request state (auth, db, dataloaders).
scalar DateTime
type User {
id: ID!
email: String!
posts(limit: Int = 10): [Post!]!
}
type Post {
id: ID!
title: String!
body: String
author: User!
}
input CreatePostInput {
title: String!
body: String
}
type Query {
user(id: ID!): User
feed(cursor: String, limit: Int = 20): FeedPage!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
type Subscription {
postCreated(authorId: ID!): Post!
}

! = non-null. [T!]! = non-null list of non-null T.

Built-in scalars: Int, Float, String, Boolean, ID. Custom scalars common (DateTime, JSON, EmailAddress).

query GetUser($id: ID!) {
user(id: $id) {
id
email
posts(limit: 5) { id title }
}
}

Variables passed alongside; named operations easier to debug/cache.

Fragments for reuse:

fragment UserFields on User { id email name }
query { user(id: "1") { ...UserFields } }
const resolvers = {
Query: {
user: (_, { id }, ctx) => ctx.db.users.find(id),
},
User: {
posts: (parent, { limit }, ctx) => ctx.db.posts.byAuthor(parent.id, limit),
},
Mutation: {
createPost: (_, { input }, ctx) => ctx.db.posts.create({ ...input, authorId: ctx.user.id }),
},
};

Each resolver receives: (parent, args, context, info).

  • Errors don’t fail the whole response — partial data + errors array per field.
  • Standardize error shape: code, message, path. Use extensions field.
{
"data": { "user": null },
"errors": [{ "message": "not found", "path": ["user"], "extensions": { "code": "NOT_FOUND" } }]
}
  • Offset — simple, problematic at scale.
  • Relay cursor — standard: edges, cursor, pageInfo { hasNextPage, endCursor }.
type FeedPage {
edges: [FeedEdge!]!
pageInfo: PageInfo!
}
type FeedEdge { node: Post! cursor: String! }
type PageInfo { hasNextPage: Boolean! endCursor: String }
  • Typically WebSocket (graphql-ws protocol). Server pushes events.
  • Use cases: live updates, chat, dashboards.
  • Backed by pub/sub (Redis, Kafka).
  • Apollo Client, urql, Relay (most strict, used at Meta).
  • Fragment colocation: each component declares its data needs.
  • Persisted queries: send query hash, server stores actual query — saves bandwidth, improves caching.
  • Servers: Apollo Server, GraphQL Yoga, Mercurius (Fastify), graphql-ruby, gqlgen (Go), Strawberry (Python).
  • Federation: Apollo Federation, GraphQL Mesh, Hot Chocolate (.NET).
  • Tooling: GraphQL Code Generator (typed clients), Apollo Studio (schema registry), Hasura/PostGraphile (auto-generated from DB).

Use when:

  • Many clients with different needs (mobile, web, partner).
  • Aggregating multiple downstream services.
  • Rapid frontend iteration without backend changes.

Avoid when:

  • Simple CRUD with one client — REST is less complex.
  • File uploads / streaming bytes.
  • Caching at HTTP layer matters (GraphQL POSTs are uncacheable by default — use persisted queries + GET).
  • Public API where you want HTTP semantics.