Query language and runtime for APIs. Single endpoint (usually POST /graphql); the client describes exactly the shape of data it wants.

Operations

Three operation types: query (read), mutation (write), subscription (stream).

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
  }
}

Sent as JSON:

{
  "query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
  "variables": { "id": "42" },
  "operationName": "GetUser"
}

Fields, arguments, aliases

{
  # Argument
  user(id: "42") {
    name
  }
 
  # Alias — rename a field in the response
  admin: user(id: "1") {
    name
  }
  guest: user(id: "2") {
    name
  }
}

Fragments (DRY)

fragment UserCore on User {
  id
  name
  avatarUrl
}
 
query {
  me {
    ...UserCore
  }
  team {
    members {
      ...UserCore
    }
  }
}

Inline fragments (polymorphism)

{
  search(text: "foo") {
    __typename
    ... on User {
      name
    }
    ... on Post {
      title
    }
    ... on Comment {
      body
    }
  }
}

Variables and directives

query Posts($limit: Int = 10, $withAuthor: Boolean!) {
  posts(limit: $limit) {
    id
    title
    author @include(if: $withAuthor) {
      name
    }
    draft @skip(if: $withAuthor)
  }
}

Built-in directives: @include(if:), @skip(if:), @deprecated(reason:).

Mutations

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      slug
    }
    errors {
      field
      message
    }
  }
}

Mutations at the top level run serially; query fields run in parallel.

Subscriptions

subscription OnMessage($roomId: ID!) {
  messageAdded(roomId: $roomId) {
    id
    text
    sender {
      name
    }
  }
}

Transport: WebSocket (graphql-ws protocol) or SSE.

Schema Definition Language (SDL)

"User of the system."
type User implements Node {
  id: ID!
  name: String!
  email: String
  posts(first: Int = 10, after: String): PostConnection!
  role: Role!
}
 
interface Node {
  id: ID!
}
 
union SearchResult = User | Post | Comment
 
enum Role {
  ADMIN
  EDITOR
  VIEWER
}
 
input CreatePostInput {
  title: String!
  body: String!
  tags: [String!] = []
}
 
type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}
 
type Query {
  user(id: ID!): User
  posts(first: Int, after: String): PostConnection!
}
 
type Subscription {
  messageAdded(roomId: ID!): Message!
}
 
schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

Scalars

Built-in: Int, Float, String, Boolean, ID. Custom scalars (e.g. DateTime, URL, JSON) are common but transport as strings.

Type modifiers:

NotationMeaning
Stringnullable string
String!non-null string
[String]nullable list of nullable strings
[String!]nullable list of non-null strings
[String]!non-null list of nullable strings
[String!]!non-null list of non-null strings

Pagination (Relay-style cursors)

{
  posts(first: 10, after: "cursor") {
    edges {
      cursor
      node {
        id
        title
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Errors

Standard response shape:

{
  "data": { "user": null },
  "errors": [
    {
      "message": "Not found",
      "path": ["user"],
      "locations": [{ "line": 2, "column": 3 }],
      "extensions": { "code": "NOT_FOUND" }
    }
  ]
}

data and errors can both be present (partial results).

Introspection

Every GraphQL server exposes its own schema:

{
  __schema {
    types {
      name
      kind
    }
  }
}
{
  __type(name: "User") {
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}
{
  __typename
} # works inside any selection set

Disable in production for closed APIs.

Querying from the CLI

# Basic POST
curl -s https://api.example.com/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ me { id name } }"}'
 
# With variables
curl -s https://api.example.com/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"query($id:ID!){user(id:$id){name}}","variables":{"id":"42"}}'
 
# GET (read-only, query in URL)
curl -G https://api.example.com/graphql \
  --data-urlencode 'query={ me { id name } }'

Tooling: graphql-cli, gql, Insomnia, Postman, Apollo Sandbox, GraphiQL.

Best practices

  • Use persisted queries in production (client sends a hash, server has the text) — cuts payload size and locks the surface.
  • Add a query depth/complexity limit server-side to prevent abusive nested queries.
  • Prefer input types for mutation arguments — easier to evolve.
  • Return payload types from mutations with a errors field for typed user errors (don’t conflate with transport errors).
  • Use @deprecated(reason:) instead of removing fields; clean up later.
  • Don’t expose internal IDs as ID — opaque global IDs (base64-encoded) are convention.