-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add best practice about robust applications #2434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+187
−0
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # Robust Applications | ||
|
|
||
| <p className="learn-subtitle">Build applications that gracefully handle schema evolution</p> | ||
|
|
||
| GraphQL schemas grow over time — new types, new enum values, new union and interface subtypes — and a well-written application expects this and handles it gracefully. How much of this is handled for you depends on your environment. In languages with compile-time schema awareness — such as TypeScript with a codegen tool, Swift with Apollo iOS, or Kotlin with Apollo Kotlin — a typed client can generate catch-all variants for enums, surface nullable fields as optional types, and warn at build time when new cases are unhandled. In dynamic languages or environments without a schema-aware client, these are application-level concerns that need to be addressed explicitly in your code. | ||
|
|
||
| The three areas below are where applications most commonly fail to account for schema evolution. | ||
|
|
||
| ## Plan for unknown enum values | ||
|
|
||
| GraphQL schemas can add new enum values at any time. A `Status` enum that starts with `ACTIVE` and `INACTIVE` might later gain `PENDING` or `ARCHIVED`. If your application treats every enum value as exhaustively known, a new value from the server can cause crashes or silent data loss. | ||
|
|
||
| **Avoid exhaustive switches without a fallback:** | ||
|
|
||
| ```ts | ||
| // Fragile: crashes or falls through if a new value is added | ||
| switch (status) { | ||
| case "ACTIVE": | ||
| return showActive() | ||
| case "INACTIVE": | ||
| return showInactive() | ||
| // No default — new values are silently ignored or throw | ||
| } | ||
| ``` | ||
|
|
||
| **Always include a default branch:** | ||
|
|
||
| ```ts | ||
| // Robust: handles any future value the server might return | ||
| switch (status) { | ||
| case "ACTIVE": | ||
| return showActive() | ||
| case "INACTIVE": | ||
| return showInactive() | ||
| default: | ||
| return showUnknown(status) | ||
| } | ||
| ``` | ||
|
|
||
| In typed languages, if your codegen tool marks enums as exhaustive, configure it to generate a catch-all variant (often called `UNKNOWN` or `%future added value`) so the compiler enforces that you handle it. | ||
|
|
||
| ## Plan for unknown union and interface subtypes | ||
|
|
||
| Unions and interfaces in GraphQL schemas can gain new member types over time. A `SearchResult` union that starts as `Article | User` might later gain `Product`. Likewise, a `Node` interface implemented by `User` and `Post` might later be implemented by `Comment`. Applications that only match known types and ignore unrecognized ones are robust; applications that crash on unexpected `__typename` values are not. | ||
|
|
||
| **Query `__typename` on union and interface fields and handle unrecognized types:** | ||
|
|
||
| ```graphql | ||
| query Search($query: String!) { | ||
| search(query: $query) { | ||
| __typename | ||
| ... on Article { | ||
| title | ||
| url | ||
| } | ||
| ... on User { | ||
| name | ||
| avatarUrl | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| In your application code, handle the case where `__typename` is something you don't recognize: | ||
|
|
||
| ```ts | ||
| for (const result of data.search) { | ||
| if (result.__typename === "Article") { | ||
| renderArticle(result) | ||
| } else if (result.__typename === "User") { | ||
| renderUser(result) | ||
| } else { | ||
| // A new type was added — degrade gracefully instead of crashing | ||
| renderUnknownResult(result) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This is especially important in long-lived native mobile apps, where a user may be running an old version of the app against a schema that has since been extended. | ||
|
|
||
| ## Do not force-unwrap nullable fields | ||
|
|
||
| GraphQL fields are nullable by default. When a field is nullable, the schema is explicitly communicating that it may not always return a value — either because the data is genuinely optional, or because the server may omit it when an error occurs on that field without failing the entire response. | ||
|
|
||
| Force-unwrapping a nullable field (using `!` in Swift, `!!` in Kotlin, or a non-null assertion in TypeScript) bypasses this contract and turns a graceful partial response into a crash. | ||
|
|
||
| **Avoid non-null assertions on nullable fields:** | ||
|
|
||
| ```ts | ||
| // Dangerous: crashes if `user` or `profile` is null | ||
| const email = data.user!.profile!.email! | ||
| ``` | ||
|
|
||
| **Use safe access patterns instead:** | ||
|
|
||
| ```ts | ||
| // Safe: degrades gracefully when any field is absent | ||
| const email = data.user?.profile?.email ?? "No email provided" | ||
| ``` | ||
|
|
||
| In Swift, prefer `guard let` or optional chaining over force-unwrapping: | ||
|
|
||
| ```swift | ||
| // Dangerous | ||
| let email = data.user!.profile!.email! | ||
|
|
||
| // Safe | ||
| guard let email = data.user?.profile?.email else { | ||
| showPlaceholder() | ||
| return | ||
| } | ||
| showEmail(email) | ||
| ``` | ||
|
|
||
| If you find yourself force-unwrapping fields that you believe will always be present, consider working with the schema maintainer to mark those fields as non-null — that way the guarantee is encoded in the schema itself, and the server is responsible for upholding it. | ||
|
|
||
| ## Recap | ||
|
|
||
| - **Unknown enum values**: Always include a default/fallback branch when switching on enum values. Configure codegen tools to generate a catch-all variant. | ||
| - **Unknown union and interface subtypes**: Always query `__typename` on union and interface fields and handle unrecognized types gracefully instead of crashing. | ||
| - **Nullable fields**: Use safe access patterns (optional chaining, `guard let`, null-coalescing) rather than force-unwrapping. If a field must always be present, encode that in the schema. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So - we should have an admonition here.. as
%future added valueis fake in some clients