Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/components/learn-aggregator/assets/care.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/components/learn-aggregator/learn-pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/pages/learn/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ export default {
performance: "",
security: "",
federation: "",
"robust-applications": "Robust Applications",
"debug-errors": "Common GraphQL over HTTP Errors",
}
121 changes: 121 additions & 0 deletions src/pages/learn/robust-applications.mdx
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.
Loading