diff --git a/src/components/learn-aggregator/assets/care.svg b/src/components/learn-aggregator/assets/care.svg new file mode 100644 index 0000000000..e03e79da80 --- /dev/null +++ b/src/components/learn-aggregator/assets/care.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/learn-aggregator/learn-pages.tsx b/src/components/learn-aggregator/learn-pages.tsx index 7335dc7d75..5d3fdb18aa 100644 --- a/src/components/learn-aggregator/learn-pages.tsx +++ b/src/components/learn-aggregator/learn-pages.tsx @@ -141,6 +141,12 @@ const _items: Record< icon: new URL("./assets/circuit.svg", import.meta.url).href, section: "best-practices", }, + "robust-applications": { + description: + "Build applications that gracefully handle schema evolution — unknown enum values, new union members, and nullable fields.", + icon: new URL("./assets/product-check.svg", import.meta.url).href, + section: "best-practices", + }, "debug-errors": { description: "Learn about common 'graphql-http' errors and how to debug them.", diff --git a/src/pages/learn/_meta.ts b/src/pages/learn/_meta.ts index efcc5f53f6..b1ce2956c5 100644 --- a/src/pages/learn/_meta.ts +++ b/src/pages/learn/_meta.ts @@ -37,5 +37,6 @@ export default { performance: "", security: "", federation: "", + "robust-applications": "Robust Applications", "debug-errors": "Common GraphQL over HTTP Errors", } diff --git a/src/pages/learn/robust-applications.mdx b/src/pages/learn/robust-applications.mdx new file mode 100644 index 0000000000..1e9db98c8e --- /dev/null +++ b/src/pages/learn/robust-applications.mdx @@ -0,0 +1,121 @@ +# Robust Applications + +

Build applications that gracefully handle schema evolution

+ +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.