Versioning GraphQL APIs — Schema Evolution Without Breaking Clients
In this tutorial, you'll learn about Versioning Graphql. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
GraphQLs single-version approach handles evolution through schema deprecation, nullable fields, and client-driven selection rather than traditional API version numbers.
What You'll Learn
By the end of this lesson, you will manage GraphQL schema evolution without breaking clients, use deprecation directives, add fields as nullable, handle field removal, and migrate clients from deprecated fields.
Why It Matters
GraphQL clients specify exactly what they need, making many changes backward-compatible. Understanding this lets you evolve your schema without version bumps.
Real-World Use
Durga Antivirus Pro evolved its GraphQL schema from scan { id status } to scan { id state result { threats } } over 6 months, keeping the old status field as deprecated until all clients migrated.
GraphQL Schema Evolution
flowchart LR
Schema[GraphQL Schema]-->Add[Add Field Nullable]
Schema-->Deprecate[Deprecate Old Field]
Schema-->Rename[Add Alias Field]
Schema-->Remove[Remove After Migration]
Add-->Client[Client Uses @skip]
Deprecate-->Client2[Client Sees @deprecated]
Rename-->Client3[Client Uses New Name]
Schema Evolution Manager
Track schema changes and deprecations.
from typing import Dict, List, Optional, Set
from datetime import datetime
class GraphQLSchemaEvolution:
def __init__(self):
self.fields: Dict[str, Dict] = {}
self.deprecated: Dict[str, Dict] = {}
def add_field(self, type_name: str,
field_name: str,
field_type: str,
nullable: bool = True,
description: str = ""):
key = f"{type_name}.{field_name}"
self.fields[key] = {
"type": field_type,
"nullable": nullable,
"description": description,
"added": datetime.utcnow(),
}
def deprecate_field(self, type_name: str,
field_name: str,
reason: str,
replacement: str = ""):
key = f"{type_name}.{field_name}"
if key in self.fields:
self.deprecated[key] = {
"reason": reason,
"replacement": replacement,
"deprecated_at": datetime.utcnow(),
}
def remove_field(self, type_name: str,
field_name: str):
key = f"{type_name}.{field_name}"
self.fields.pop(key, None)
self.deprecated.pop(key, None)
def get_schema_sdl(self, include_deprecated: bool = True
) -> str:
sdl = ""
types = set(
k.split(".")[0] for k in self.fields
)
for type_name in sorted(types):
sdl += f"type {type_name} {{\n"
for key, field in sorted(self.fields.items()):
if key.startswith(f"{type_name}."):
field_name = key.split(".")[1]
nullable = field["nullable"]
ftype = field["type"]
if nullable:
ftype = ftype
sdl += f" {field_name}: {ftype}"
if key in self.deprecated:
dep = self.deprecated[key]
sdl += f' @deprecated(reason: "{dep["reason"]}")'
sdl += "\n"
sdl += "}\n\n"
return sdl
def get_active_queries(self) -> List[Dict]:
return [
{
"type": k.split(".")[0],
"field": k.split(".")[1],
"type_name": v["type"],
"deprecated": k in self.deprecated,
}
for k, v in sorted(self.fields.items())
]
evolution = GraphQLSchemaEvolution()
evolution.add_field("ScanResult", "id", "ID!", nullable=False)
evolution.add_field("ScanResult", "status", "String", nullable=True)
evolution.add_field("ScanResult", "state", "String", nullable=True)
evolution.deprecate_field("ScanResult", "status",
"Use state instead", "state")
sdl = evolution.get_schema_sdl()
print(sdl)
Client Query Compatibility
Check if client queries are compatible with the current schema.
from typing import Dict, List, Set, Optional
import re
class QueryCompatChecker:
def __init__(self, schema_fields: Set[str]):
self.schema_fields = schema_fields
def extract_fields(self, query: str) -> Set[str]:
fields = set()
pattern = r'\b(\w+)\s*(?:\(|$)'
for match in re.finditer(pattern, query):
field = match.group(1)
if field not in {"query", "mutation", "subscription",
"type", "on", "fragment", "..."}:
fields.add(field)
return fields
def check_compatibility(self, query: str) -> Dict:
used_fields = self.extract_fields(query)
missing = used_fields - self.schema_fields
return {
"compatible": len(missing) == 0,
"missing_fields": sorted(missing),
"used_fields": sorted(used_fields),
}
def suggest_migration(self, query: str,
rename_map: Dict[str, str]
) -> str:
for old, new in rename_map.items():
query = query.replace(old, new)
return query
schema_fields = {"id", "state", "result", "threats"}
checker = QueryCompatChecker(schema_fields)
client_query = "{ scan { id status result } }"
result = checker.check_compatibility(client_query)
print(f"Compatible: {result['compatible']}")
print(f"Missing: {result['missing_fields']}")
Deprecation Directive Implementation
Use GraphQLs @deprecated directive to mark fields.
from typing import Dict, Optional
class DeprecationDirective:
def __init__(self):
self.directives: Dict[str, str] = {}
def mark_deprecated(self, field: str,
reason: str,
replacement: Optional[str] = None):
full_reason = reason
if replacement:
full_reason += f" Use {replacement} instead."
self.directives[field] = full_reason
def build_sdl_directive(self, field: str) -> str:
reason = self.directives.get(field)
if reason:
return f' @deprecated(reason: "{reason}")'
return ""
def get_deprecation_reason(self, field: str
) -> Optional[str]:
return self.directives.get(field)
def should_show_in_introspection(self, field: str
) -> bool:
return True
directives = DeprecationDirective()
directives.mark_deprecated(
"status", "Field is deprecated",
"state"
)
sdl_directive = directives.build_sdl_directive("status")
print(f"SDL: {sdl_directive}")
Common Mistakes
Mistake 1: Removing Fields Immediately
Mark fields as deprecated first. Give clients at least 6 months before removing.
Mistake 2: Making Non-Nullable Fields Required
Adding a non-nullable field to an existing type breaks all queries that do not request it.
Mistake 3: Ignoring Introspection Deprecation
GraphQL introspection shows @deprecated. Rely on clients to check this instead of versioning.
Mistake 4: Versioning in the Endpoint
GraphQL does not need /v1/graphql and /v2/graphql. One endpoint evolves over time.
Mistake 5: Not Monitoring Deprecated Field Usage
Without usage tracking, you do not know when it is safe to remove deprecated fields.
Practice Questions
- Why does GraphQL not need traditional versioning?
- How does the @deprecated directive work in GraphQL?
- What is the safest way to add a new field to a GraphQL type?
- How do you handle field removal in GraphQL?
- What is the role of nullable fields in schema evolution?
Challenge
Build a GraphQL schema evolution system that tracks field additions and deprecations, generates SDL with @deprecated directives, checks client queries for deprecated field usage, and reports migration completion stats.
FAQ
Mini Project
Build a GraphQL schema evolution system that manages field lifecycle (add, deprecate, remove), generates SDL with @deprecated directives, checks client queries against the schema for compatibility, provides migration suggestions for deprecated fields, and tracks which fields are actively used.
What's Next
Learn about Versioning gRPC APIs for protobuf-based API Versioning, or explore Versioning REST APIs for REST API strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro