GraphQL API Monitoring
GraphQL answers 200 whether the query worked or not. The failure sits in the errors array in the body, where a status-code monitor never looks. Assert sends the query, reads the body, and fails the check on what it finds there.
The Problem
Five ways a GraphQL endpoint fails while still reporting success:
- 200 with errors: the server reports success and populates
errors - Partial responses: some fields resolve, the rest come back null
- N+1 queries: the resolver gets slower every week and never errors
- Schema changes: a removed field breaks clients while the status code holds at 200
- Resolver failures: one field fails on its own and nothing downstream notices
How Assert Helps
Execute Real Queries
Assert sends the query itself, with its variables:
Monitor: User Profile Query
Query:
query GetUser($id: ID!) {
user(id: $id) {
id
email
profile {
name
avatar
}
}
}
Variables: {"id": "user_123"}
Detect GraphQL Errors
The one assertion every GraphQL monitor needs is that errors never shows up:
Assertions:
✓ $.errors does not exist
✓ $.data.user exists
✓ $.data.user.email contains "@"
Monitor Mutations
Mutations get the same treatment. Run one against a test account and check what comes back:
Monitor: Create Post Mutation
Query:
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
status
}
}
Variables: {"input": {"title": "Test", "content": "..."}}
Assertions:
✓ $.errors does not exist
✓ $.data.createPost.id exists
✓ $.data.createPost.status == "draft"
GraphQL-Specific Assertions
No Errors
This response is a 200:
// Bad response (200 OK but has errors):
{
"data": null,
"errors": [
{
"message": "User not found",
"path": ["user"]
}
]
}
Assertion: $.errors must not exist
Data Structure
Validate the expected data structure:
// Expected response:
{
"data": {
"users": [
{"id": "1", "name": "Alice"},
{"id": "2", "name": "Bob"}
]
}
}
Assertions:
$.data.usersmust be array$.data.users.lengthmust be > 0$.data.users[0].idmust exist
Partial Failures
Some fields might fail while others succeed:
// Partial failure:
{
"data": {
"user": {
"id": "1",
"posts": null // Failed to resolve
}
},
"errors": [
{"message": "Failed to fetch posts", "path": ["user", "posts"]}
]
}
Assertions:
$.data.user.postsmust exist (catches null)$.errorsmust not exist
Real-World Examples
Monitor a Public GraphQL API
Monitor: GitHub GraphQL API
URL: https://api.github.com/graphql
Method: POST
Headers:
Authorization: Bearer $GITHUB_TOKEN
Body:
query: |
query {
viewer {
login
repositories(first: 5) {
nodes {
name
}
}
}
}
Assertions:
✓ $.errors does not exist
✓ $.data.viewer.login exists
✓ $.data.viewer.repositories.nodes.length > 0
✓ Response time < 2000ms
Monitor an E-commerce GraphQL API
Monitor: Product Catalog Query
Query:
query Products($first: Int!) {
products(first: $first) {
edges {
node {
id
name
price
inventory {
quantity
}
}
}
}
}
Variables: {"first": 10}
Assertions:
✓ $.errors does not exist
✓ $.data.products.edges.length > 0
✓ $.data.products.edges[0].node.price > 0
✓ $.data.products.edges[0].node.inventory.quantity >= 0
Monitor Authentication
Monitor: Auth Query
Query:
query Me {
me {
id
email
role
permissions
}
}
Headers:
Authorization: Bearer $TEST_USER_TOKEN
Assertions:
✓ $.errors does not exist
✓ $.data.me.id exists
✓ $.data.me.role in ["user", "admin"]
Common GraphQL Monitoring Patterns
Schema Health Check
Query introspection to verify schema is available:
query IntrospectionQuery {
__schema {
types {
name
}
}
}
Assertions:
$.data.__schema.types.length> 0
Resolver Performance
Point a monitor at the resolvers you already know are slow:
query SlowResolver {
analytics {
dailyStats(days: 7) {
date
value
}
}
}
Assertions:
- Response time < 3000ms
$.data.analytics.dailyStats.length== 7
Subscription Health (via HTTP)
If you expose subscription health:
GET /graphql/subscriptions/health
Assertions:
✓ $.connected == true
✓ $.activeSubscriptions >= 0
Best Practices
Monitor Critical Paths
Focus on queries that impact users:
| Priority | Query Type | Example |
|---|---|---|
| Critical | Authentication | me, currentUser |
| Critical | Core data | products, orders |
| High | Search | searchProducts |
| Medium | Analytics | dashboardStats |
Use Realistic Variables
Test with production-like data:
// Good: Realistic variables
{"userId": "user_abc123", "limit": 10}
// Bad: Minimal testing
{"userId": "1", "limit": 1}
Monitor from Multiple Regions
A query's latency depends on CDN caching, database proximity and resolver complexity, and all three change by region. Assert runs each check from six regions and wants them to agree before it opens an incident.
Test Error Handling
Point a monitor at a request that should fail, and assert on the error you expect back:
Monitor: Invalid ID Handling
Query: user(id: "invalid_id") { id }
Assertions:
✓ Status code == 200
✓ $.errors[0].message contains "not found"
If someone changes that message, this check finds out before a client does.
Track Response Times
A dashboard query and a me query have nothing in common, so give them separate budgets:
Assertions:
✓ Response time < 500ms (simple queries)
✓ Response time < 2000ms (complex queries)
✓ Response time < 5000ms (analytics queries)
Alert Configuration
However you route them, Assert sends one alert per incident with the failing response attached, and closes it on its own when the next check passes.
Critical
Condition: $.errors exists on core queries
Action: Page on-call
Reason: Users can't access data
Performance
Condition: Response time > 3000ms
Action: Slack alert
Reason: User experience degrading
Schema Issues
Condition: Introspection query fails
Action: Email engineering
Reason: Schema may be broken
Getting Started
- List the queries users hit first, starting with authentication and core data
- Build one monitor per query, with realistic variables
- Assert
$.errorsdoes not exist on every one of them - Assert on the shape of
$.dataunderneath it - Give each query a response time budget and assert on that too
Related Features
- Response Validation: what you can assert on
- Multi-Region Monitoring: the six regions and how consensus works