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.