Skip to content

Commit 3734a7a

Browse files
committed
api changed guide
1 parent 50d485b commit 3734a7a

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

content/blog/api-changes.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
+++
2+
title = "A comprehensive guide to GraphQL API changes"
3+
author = "Andreas Marek"
4+
tags = []
5+
categories = []
6+
date = 2022-02-07T00:00:00+10:00
7+
toc = "true"
8+
+++
9+
10+
__Note: this guide aims to be an update document which will be updated if needed.__
11+
12+
# Breaking changes
13+
14+
They type system of GraphQL gives you confidence what the shape of your API is:
15+
which data to expect from each field, which field can be null or not etc.
16+
This is great, but an API is rarely absolutely fixed, but rather continues to evolve over time.
17+
18+
The beauty of the type system is that not every change is equal, but some are perfectly fine
19+
and others you might want to try to avoid. The most obvious dual classification distinguishes between
20+
a breaking change and a non-breaking change. While this makes sense at first glance, it is to simplistic
21+
to cover all differente changes that can happen.
22+
23+
We will look at the existing definitions and then classify all different changes in detail.
24+
25+
# GraphQL Spec definition of breaking
26+
The [GraphQL spec](https://spec.graphql.org/draft/#sec-Validation.Type-system-evolution) says the following:
27+
28+
> Any change that can cause a previously valid request to become invalid is considered a breaking change.
29+
30+
Every GraphQL request is either considered valid or invalid. If a request is invalid it will not be executed.
31+
This definition of breaking change would cover the following change:
32+
33+
Original schema:
34+
{{< highlight TypeScript "linenos=table" >}}
35+
type Query {
36+
name:String
37+
}
38+
{{< / highlight >}}
39+
<p/>
40+
New Schema:
41+
{{< highlight TypeScript "linenos=table" >}}
42+
type Query {
43+
newName:String
44+
}
45+
{{< / highlight >}}
46+
<p/>
47+
48+
Now the previously valid query `{name}` would become invalid because there is no top-level field `name` anymore.
49+
This is clearly a breaking change.
50+
51+
But what about this:
52+
53+
Original schema:
54+
{{< highlight TypeScript "linenos=table" >}}
55+
type Query {
56+
name:String!
57+
}
58+
{{< / highlight >}}
59+
<p/>
60+
New Schema:
61+
{{< highlight TypeScript "linenos=table" >}}
62+
type Query {
63+
name:String
64+
}
65+
{{< / highlight >}}
66+
<p/>
67+
68+
Here the type of `name` was changed from non-nullable to nullable: `String!` => `String`.
69+
Now `{name}` is still a valid query which would be executed without error. In general this is also
70+
considered a breaking change, because the old schema assured that client that `name` could never be null,
71+
but now it can. Every change like this where the type system guarantees less, is considered a breaking
72+
change.
73+
74+
This example shows that the definition of breaking change in the spec doesn't cover all the cases
75+
we are interested in.
76+
77+
# GraphQL.js definition of breaking change
78+
The next good thing after the spec is the JavaScript reference implementation.
79+
GraphQL.js [contains a function](https://github.com/graphql/graphql-js/blob/main/src/utilities/findBreakingChanges.js)
80+
`findBreakingChanges` which compares an old and a new schema and returns
81+
a list of breaking and dangerous changes.
82+
83+
Unfortunately the functions code itself doesn't make clear what breaking or dangerous means exactly, but
84+
the discussion in the two issues [#992](https://github.com/graphql/graphql-js/pull/992) and
85+
[#968](https://github.com/graphql/graphql-js/issues/968) shed some light on it:
86+
87+
>The above is with the assumption that "dangerous change" means
88+
>"it could affect your client if you built things poorly (i.e. didn't provide a default in a switch statement),
89+
>but won't affect clients built with the concept of GraphQL breaking changes in mind".
90+
>
91+
>Basically: if something breaks due to a "dangerous change" there's some underlying,
92+
>root issue on your client you should dig in and fix so future changes don't cause
93+
>that problem. But not coding defensively can lead to these issues. Whereas if you make a
94+
>"breaking change" it's expected that clients won't be able to handle it correctly.
95+
96+
<p/>
97+
98+
>When building clients, it's best to be defensive against possible future expansions and handle
99+
>those cases. For enums, we typically include an else or switch default clause when branching on
100+
>them to handle cases the client doesn't know about. If your clients aren't programming defensively
101+
>like this, then it's true that expanding the possible response values could cause issues for those
102+
>clients - it's not an entirely safe change.
103+
104+
This basically means that you take client side development considerations into account and
105+
not only the guarantees of the type system itself.
106+
107+
For example as described above: adding a value to an Enum is a non breaking change
108+
if you just look at the schema itself,
109+
but it could lead to problems if the clients are not developed with the possibilities
110+
of new Enum values in mind.
111+
112+
And while the list provided by GraphQL.js is much more detailed than the short
113+
definition in the spec, the nuances are not really obvious, it is only available as code
114+
and sometimes it could be more specific.
115+
116+
For example removing a Directive is not always a breaking change if the Directive
117+
is never used at a Query element or adding an argument which is required is not breaking
118+
if the argument has a default value.
119+
120+
# Comprehensive list of changes with explanation
121+
122+
This sections aims to provide a comprehensive list of changes and
123+
what consequence each change have.
124+
125+
Same naming conventions:
126+
127+
128+
- the type of a type is called kind. It can be Enum,
129+
Scalar, Input Object, Object, Interface or Union.
130+
- Argument means arguments for fields and for Directives if
131+
not explicitly mentioned otherwise.
132+
- Query location means a location for a Directive in a Query (eg. field)
133+
- Schema location means a location for a Directive in SDL (eg. field definition)
134+
- Query Directive is a Directive which is a valid for locations in a Query
135+
- Schema Directive is a Directive which is a valid for locations in a SDL
136+
- Input types are Scalar, Enum, Input Objects.
137+
- Output types are Scalar, Enum, Object, Interfaces and Union.
138+
139+
List of changes:
140+
141+
## 1. A type is removed.
142+
When a type is removed every Query which directly uses the type by name becomes invalid.
143+
For input types this means a variable declaration becomes invalid:
144+
145+
{{< highlight Scala "linenos=table" >}}
146+
query($var: InputTypeWhichIsRemoved) {
147+
# more
148+
}
149+
{{< / highlight >}}
150+
151+
For composite output types every query which uses the type as type condition becomes invalid:
152+
{{< highlight Scala "linenos=table" >}}
153+
{
154+
... on TypeWhichIsRemoved {
155+
}
156+
}
157+
{{< / highlight >}}
158+
159+
When a Scalar or Enum which is used as output type is removed it means the field
160+
which returns this Scalar or Enum is either removed or changed in a breaking way.
161+
162+
## 2. The kind of a type is changed.
163+
164+
Changing the kind of a type means fundamentally changing the guarantee about this type.
165+
166+
Open discussion: changing an Object (which was not used as type in an Union)
167+
is maybe not breaking?
168+
169+
## 3. A type of an Union is removed.
170+
171+
Every request which queried the type via Fragment becomes invalid.
172+
{{< highlight Scala "linenos=table" >}}
173+
{ unionField {
174+
... on TypeWhichIsRemoved {
175+
# more
176+
}
177+
}
178+
}
179+
{{< / highlight >}}
180+
181+
## 4. A value is removed from an Enum which is used as input.
182+
183+
Every request which used the value becomes invalid.
184+
185+
## 5. A required input field or argument is added which doesn't have a default value.
186+
187+
Every request which queried the field becomes invalid because the required
188+
argument or input field is not provided.
189+
190+
## 6. An Interface is removed from an Object or Interface.
191+
192+
Every request which queried the type via Fragment becomes invalid.
193+
194+
## 7. An argument or input field is removed.
195+
196+
Every request which provided the argument or input field becomes invalid.
197+
198+
## 8. An argument type or input field is changed in an incompatible way.
199+
200+
Any change which is not just removing non-nullable constraints is breaking.
201+
202+
TODO: more explanation.
203+
204+
## 9. A Query Directive was removed.
205+
206+
Every request which used the Directive becomes invalid.
207+
208+
## 10. A Query Directive was changed from repeatable to not repeatable.
209+
210+
Every request which provided multiple instances of the Directive on the same element
211+
becomes invalid.
212+
213+
## 11. A Query location for a Query Directive was removed.
214+
215+
Every request which has the Directive on the now removed location becomes invalid.
216+
217+
## 12. A value is added to an Enum.
218+
219+
If a client is not developed in defensive way which expects new Enum values
220+
it can cause problems.
221+
222+
## 13. Default value for argument or input field is changed
223+
224+
Every request which didn't provide any value for this argument
225+
or input field is now using the new default value.
226+
227+
# Characteristics of each change
228+
229+
As discussed above there are different aspects to a change we should consider:
230+
231+
- Will it make previously valid queries invalid
232+
- Does it weaken or fundamentally changes the guarantees of the schema
233+
- Could it be problematic for clients
234+
235+
| Change | Causes invalid queries | Less/incompatible guarantees | Maybe problematic for clients |
236+
|--------------------------------------|------------------------|------------------------------|-------------------------------|
237+
| #1 Type removed | yes | na | na |
238+
| #2 Kind changed | yes | yes | na |
239+
| #3 Union type removed | yes | no | na |
240+
| #4 Input Enum value removed | yes | no | na |
241+
| #5 Required input added | yes | | |
242+
| #6 Interface removed | yes | | |
243+
| #7 Argument/Input field removed | yes | | |
244+
| #8 Argument/Input field type changed | yes | | |
245+
| #9 Query directive removed | yes | | |
246+
| #10 Query directive non-repeatable | yes | | |
247+
| #11 Query directive location removed | yes | | |
248+
| #12 Value added to Enum | no | no | yes |
249+
| #13 Default value changed | no | no | yes |
250+
251+
252+
# Feedback or questions
253+
We use [GitHub Discussions](https://github.com/graphql-java/graphql-java/discussions) for general feedback and questions.
254+
255+
You can also checkout our [Workshops](/workshops) for more possibilities to learn GraphQL Java.
256+
257+
258+
259+

0 commit comments

Comments
 (0)