Skip to content

Commit

Permalink
Merge branch 'develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
harshtech123 authored Jul 23, 2024
2 parents e99a281 + 81ab8ec commit bc20f17
Show file tree
Hide file tree
Showing 12 changed files with 876 additions and 5 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ jobs:
run: |
npm install
npx prettier --check "**/*.{graphql,yml,json,md,sh,ts,tsx,js}"
cd ./publish-externals
npm run generate
cd ..
npm run typecheck
npm run build
- name: Deploy 🚀
Expand Down
16 changes: 16 additions & 0 deletions blog/graphql-schema-2024-07-11.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ slug: graphql-schema

Designing a robust, scalable GraphQL schema is critical for building production-ready APIs that can evolve with your application's needs. In this comprehensive guide, we'll walk through the process of crafting a GraphQL schema for a real-world application, highlighting best practices and considerations along the way.

If you are thinking how we could possibly cover all of the lovely intricacies associated with this topic in one go, you are right, we can't and so we are not! We have created an amazing series to take you through the nuances of working with GraphQL schemas.

Let's break our job into puzzle pieces. Let's start by simply creating designing a brand new schema!

<!-- truncate -->

<div style={{textAlign: 'center', margin:'16px'}}>

<img src="/images/blog/puzzle-graphql-schema-1.png" alt="puzzle piece to visualise the series" style={{maxWidth: '40%'}} />

</div>

If you're new to GraphQL Schema, check out our [GraphQL Schema Tutorial](https://tailcall.run/graphql/schemas-and-types/) to get up to speed with the basics.

## The Power of GraphQL Schemas
Expand Down Expand Up @@ -474,3 +484,9 @@ Remember, your schema is a living document. As your application evolves, so too
The TechTalent example we've explored here demonstrates many real-world considerations, but every application will have its unique requirements. Always design with your specific use cases in mind, and don't be afraid to iterate as you learn more about how your API is used in practice.

By investing time in thoughtful schema design upfront, you'll create a solid foundation for your GraphQL API, enabling efficient development and a great experience for your API consumers.

Alright greatttt! You have successfully completed the first part of a very intricate series!! Pat yourslef and maybe high five your cat! Here are the links to the next blogs in the series that have already been published.

![cat giving high five](../static/images/blog/cat-high-five-gif.webp)

- [Next Part](/blog/graphql-schema-part-2-1)
345 changes: 345 additions & 0 deletions blog/graphql-schema-part-2-1-2024-07-21.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
---
title: Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 2.1
authors:
- name: Amit Singh
title: Head of Growth and Strategy @ Tailcall
url: https://github.com/amitksingh1490
image_url: https://avatars.githubusercontent.com/u/23661702?v=5
tags: [GraphQL, API, Schema, Design, Best Practices]
description: Learn how to make additive changes to your GraphQL schema without causing disruptions.
image: /images/graphql/graphql-schema-structure.png
hide_table_of_contents: true
slug: graphql-schema-part-2-1
---

import Quiz from "../src/components/quiz/quiz.tsx"

![GraphQL Schema Structure](../static/images/graphql/graphql-schema-structure.png)

In our [previous post](/blog/graphql-schema), we learned scalable GraphQL schema is critical for building production-ready APIs that can evolve with your application's needs.

In this post, we will dive deeper into how to **continuously** evolve your schema to meet your application's changing requirements without hard-coded versioning.

<!-- truncate -->

## Adding Without Breaking: The Art of Additive Changes

You know that feeling when you're working on a project, and suddenly you realize your schema needs to change? Maybe you need to add a new field, modify an existing one, or even remove something entirely. It's enough to make any developer break out in a cold sweat, right?

But fear not! I'm here to show you **how to evolve your schema like a pro**, keeping your API fresh and exciting without causing your clients to tear their hair out.

## The Good, The Bad, and The Ugly of Schema Changes

Not all changes are created equal. In this section, we’ll analyze a few different types of changes and what makes them safe or unsafe.

First things first, let's break down the types of changes we might make to our schema:

1. **Safe Changes:** These are the golden children of schema evolution. You can make these changes anytime, and your clients won't even bat an eyelash.
2. **Dangerous Changes:** These are the sneaky ones. They might not break anything outright, but they can cause subtle issues that'll have your clients scratching their heads. We'll need to proceed carefully here.
3. **Breaking Changes:** The name says it all. These changes will send your clients' applications crashing down faster than you can say "**GraphQL**". We want to avoid these like the plague, but sometimes they're necessary. Don't worry, I'll show you how to handle them like a pro.

## Additive Changes

Most of the time, these are safe as houses.

For example, adding fields & adding types is unlikely to cause issues for clients. But, there are a few tricky scenarios to watch out for.

### The Optional Argument Conundrum

Adding optional arguments is generally safe - it's like offering your clients a shiny new toy without forcing them to play with it.

However, there's a catch. Check this out:

```diff
type Query {
- products(category: String): [Product!]!
+ products(category: String, inStock: Boolean): [Product!]!
}
```

See what we did there? We added an optional `inStock` argument. Seems harmless, right?

Let's dive deeper into why changing the behavior of a resolver when an optional argument isn't provided can be problematic:

```graphql
type Query {
products(category: String, inStock: Boolean): [Product!]!
}
```

Imagine you have clients that have been using this query:

```graphql
query {
products(category: "Electronics") {
name
price
}
}
```

If your resolver suddenly starts filtering out out-of-stock products when `inStock` isn't provided, these clients will unexpectedly receive fewer results. This could break their UI or data processing logic.

To avoid this issue, you can implement a strategy to handle the absence of the `inStock` argument gracefully in your resolver, so that the behavior remains consistent for clients.

### The Required Argument Trap

Now, this is where things get spicy 🌶️.

Adding a required argument is almost always a **breaking change**.

But, fear not! There's a way out:

```diff
type Query {
- products(category: String): [Product!]!
+ products(category: String, sortBy: SortOption!): [Product!]!
}
```

This change is **breaking**, but it doesn't have to be.

You can provide a **default value** for the new argument to keep your existing clients happy.

```diff
type Query {
- products(category: String): [Product!]!
+ products(category: String, sortBy: SortOption! = POPULARITY): [Product!]!
}
```

See that `= POPULARITY`? That's your get-out-of-jail-free card. By providing a default value, you've made this addition safe.

Existing clients will use the default, and new clients can take advantage of the sorting option.

### The Interface and Union Twist

Now, let's talk about some trickier additive changes that can catch you off guard if you're not careful.

### Adding New Interface Implementations

Adding a new type that implements an existing interface might seem harmless, but it can cause some unexpected behavior. Check this out:

```graphql
interface Node {
id: ID!
}

type User implements Node {
id: ID!
name: String!
}

type Team implements Node {
id: ID!
name: String!
}

# highlight-start
type Organization implements Node {
id: ID!
name: String!
employees: [User!]!
}
# highlight-end
```

By adding the `Organization` type, we've expanded what could be returned by queries selecting for `Node`. This could break clients that aren't prepared to handle new types. Always encourage clients to use proper type checking.

```graphql
query {
node(id: "1") {
... on User {
name
}
... on Team {
name
}
... on Organization {
name
employees {
name
}
}
}
}
```

Without proper type checking, clients might encounter these issues:

1. **Runtime Errors:** If a client assumes all Node types have only a name field, they might try to access `employees` on a `User` or `Team`, causing errors.
2. **Missing Data:** Clients might not display Organization-specific data if they're not prepared to handle it.
3. **Incorrect Data Processing:** Business logic that assumes only `User` and `Team` types exist might produce incorrect results.

To mitigate these issues:

1. Use TypeScript or Flow on the client-side to catch type errors at compile-time.
2. Implement exhaustive type checking in your client code:

```typescript
function handleNode(node: Node) {
switch (node.__typename) {
case "User":
return handleUser(node)
case "Team":
return handleTeam(node)
case "Organization":
return handleOrganization(node)
default:
const _exhaustiveCheck: never = node
throw new Error(`Unhandled node type: ${(_exhaustiveCheck as any).__typename}`)
}
}
```

This approach ensures that if a new type is added in the future, TypeScript will raise a compile-time error, prompting developers to update their code.

### The Union Expansion Conundrum

Similar to interfaces, adding new members to a union can cause runtime surprises. Consider this:

```diff
- union SearchResult = User | Post
+ union SearchResult = User | Post | Comment
```

Surprise! Your clients might suddenly receive a type they weren't expecting. It's like opening a box of chocolates and finding a pickle - not necessarily bad, but definitely unexpected. Make sure to document how clients should handle these surprise types.

Let's delve into why union expansions can be tricky and how to handle them gracefully:

When you add `Comment` to the `SearchResult` union, existing clients might break in subtle ways:

1. **Incomplete UI:** If the client only has UI components for `User` and `Post`, `Comment` results won't be displayed.
2. **Runtime Errors:** Code that assumes only `User` and `Post` types exist might throw errors when encountering a `Comment`.

To handle this gracefully:

1. Implement a fallback UI component for unknown types:

```tsx
function SearchResultItem({result}) {
switch (result.__typename) {
case "User":
return <UserResult user={result} />
case "Post":
return <PostResult post={result} />
case "Comment":
return <CommentResult comment={result} />
default:
return <UnknownResultType type={result.__typename} />
}
}
```

2. Encourage clients to use introspection queries to stay updated on schema changes:

```graphql
query {
__type(name: "SearchResult") {
kinds
possibleTypes {
name
}
}
}
```

By implementing these strategies, clients can gracefully handle new union members without breaking existing functionality.

### The Enum Evolution

Adding new enum values seems innocent enough, but it can impact client-side logic. Let's look at an example:

```diff
enum OrderStatus {
PENDING
COMPLETED
+ CANCELED
+ REFUNDED
}
```

Clients that were using exhaustive switches might now have incomplete logic. Encourage clients to use default cases to handle new enum values.

```typescript
switch (order.status) {
case "PENDING":
return "Order is pending"
case "COMPLETED":
return "Order is completed"
default:
return "Order status unknown"
}
```

## Quiz Time! 🎉

<Quiz
title="GraphQL Schema Change"
questions={[
{
id: 1,
text: "Adding a new field to a GraphQL schema is generally a:",
options: ["Safe change", "Dangerous change", "Breaking change", "Requires deprecation"],
correctAnswer: 0,
},
{
id: 2,
text: "What is a potential issue when adding a new optional argument to a resolver?",
options: [
"It requires clients to update their queries",
"It can change the default behavior if not handled properly",
"It always breaks existing queries",
"It is not allowed in GraphQL",
],
correctAnswer: 1,
},
{
id: 3,
text: "Which strategy can make adding a required argument safe?",
options: [
"Introducing a new field",
"Providing a default value",
"Deprecating the old argument",
"Using introspection queries",
],
correctAnswer: 1,
},
{
id: 4,
text: "What is a common risk when adding a new type to an existing interface?",
options: [
"Clients will receive runtime errors if not properly type-checked",
"The schema becomes invalid",
"Existing types get overridden",
"It forces all clients to update immediately",
],
correctAnswer: 0,
},
{
id: 5,
text: "What should clients implement to handle new union members?",
options: [
"Fallback UI components for unknown types",
"Always use non-null fields",
"Deprecation notices",
"Schema descriptions",
],
correctAnswer: 0,
},
]}
/>

## Conclusion

Evolving a GraphQL schema through additive changes allows you to expand your API's capabilities while maintaining backward compatibility. By following the principles and strategies outlined in this article, you can confidently add new fields, types, and arguments without causing disruptions to your clients.

Remember these key takeaways:

1. **Favor Additive Changes**: Whenever possible, add new fields, types, or arguments instead of modifying existing ones. This approach maintains backward compatibility while allowing your schema to grow.

2. **Provide Transition Paths**: Introduce new features alongside existing ones to allow gradual client adoption.

By treating your GraphQL schema as a product with its own lifecycle and evolution strategy, you can build APIs that are both powerful and adaptable. This approach allows you to innovate rapidly while providing a stable and reliable service to your clients.

Stay tuned for the [next part](/blog/graphql-schema-part-2-2) of this series, where we will dive into removing schema elements and handling breaking changes!
Loading

0 comments on commit bc20f17

Please sign in to comment.