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:
| Notation | Meaning |
|---|---|
String | nullable 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 setDisable 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
errorsfield 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.