📣 Catch us at GraphQLConf 2024 • September 10-12 • San Francisco • Know more →
Skip to main content
Back to Blogs

Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 4

· 7 min read
Cover Image for Design a GraphQL Schema So Good, It'll Make REST APIs Cry - Part 4

What Do You Already Know? 🧠💫​

GraphQL Schema Change Quiz!

Question 1/6

What is a critical consideration before removing a field from a GraphQL schema?

Removing Without Breaking: The Subtraction Subterfuge​

In our previous post, we explored how to modify existing schema elements without causing disruptions. Now, we'll tackle the most challenging part: removing schema elements and handling breaking changes.

Recap of Additive Changes and Modifications​

In Part 2, we focused on additive changes, emphasizing the importance of expanding your schema's capabilities without disrupting existing clients. By adding new fields, types, and arguments, you can enhance your API while maintaining backward compatibility.

In Part 3, we delved into modifying existing schema elements, discussing how to handle changes such as updating default values, transforming non-null fields to nullable, and changing field types. We highlighted the need for clear communication, providing transition paths, and leveraging schema design tools to minimize client disruptions.

Safe, Dangerous, and Breaking Changes​

  1. Safe Changes: Additive changes such as adding new fields or types that do not affect existing queries or functionality.
  2. Dangerous Changes: Modifications that might not break the schema immediately but can cause subtle issues, such as changing default values or making non-nullable fields nullable.
  3. Breaking Changes: Changes that will definitely break existing queries and require clients to update their code, such as removing fields or changing field types.

The Subtraction Subterfuge​

Removing things from your schema is almost always a breaking change. If you remove a field, type, or argument, clients that depend on it will break. You can't just take things away without consequences.

But sometimes, it's necessary. Here's how to do it without causing a riot.

The Field Farewell​

Let's say we want to remove a field because it's causing performance issues. Here's the smart way to do it:

  1. Introduce a replacement
  2. Deprecate the old field
  3. Wait (patiently!)
  4. Remove when usage has died down
type Query {
- products(first: Int!): [Product!]!
+ products(first: Int!): [Product!]! @deprecated(reason: “products is deprecated and is getting replaced by the field `topProducts`.”)
+ topProducts(first: Int!): [Product!]!
}

By introducing topProducts and deprecating products, we give our clients time to adapt. And hey, we've even improved our API in the process!

The old field may be removed after a certain period and if the usage for it has gone down. Keep in mind you don’t necessarily have to make the change unless absolutely needed. Additive changes and deprecations are sometimes enough to keep evolving the API.

The Argument Abandonment​

Removing an argument is similar to removing a field. You can deprecate it and

introduce a new field with the desired behavior. Clients will have time to migrate to the new field before the old one is removed.

type Query {
- products(first: Int!, featured: Boolean): String!
+ products(first: Int!, featured: Boolean): String! @deprecated(reason: “products is deprecated, use `allProducts` for products and `featuredProducts` to get products that are featured”)
+ allProducts(first: Int!): String!
+ featuredProducts(first: Int!): String!
}

If you need to make a change to an existing field, because arguments can’t be deprecated just yet, you should indicate that the argument is deprecated through its description.

type Query {
- products(first: Int!, featured: Boolean): String!
+ products(first: Int!,
+ # DEPRECATED: This argument will be removed. Use query `featuredProducts`.
+ featured: Boolean
+ ): String!
}

The Type Deletion Dilemma​

Sometimes, you need to remove an entire type from your schema. This is a major operation and requires careful planning.

  1. First, deprecate all fields that return this type:
type Query {
oldUser(id: ID!): OldUser @deprecated(reason: "Use `user` query with new User type instead")
user(id: ID!): User
}
  1. If the type is part of a union or implements an interface, you'll need to be extra cautious. These can't be easily deprecated, so clear communication is key.
  2. Finally, after a long deprecation period and when usage has dropped to zero, you can remove the type entirely.

Note that you might want to deprecate using that type within your codebase to avoid developers to use that User type for new fields. Removing a type is even trickier when it’s part of union types or implements interfaces. Once again, union members and interface implementations cannot be marked as deprecated. This means that fields like node may stop working correctly if the type you’re removing was reachable through that field.

Your best bet in these cases are to either keep this type as part of unions and through interfaces or to communicate that change very carefully through descriptions and out of band communication like documentation and emails.

Conclusion​

Removing schema elements is a delicate process that requires strategic planning and clear communication to avoid breaking changes. By following the principles and strategies outlined in this article, you can confidently remove fields, arguments, and types while minimizing disruption to your clients.

Remember these key takeaways:

  1. Deprecate Cautiously: Use deprecation notices, schema descriptions, and out-of-band communication to keep your clients informed about upcoming changes.
  2. Provide Transition Paths: When breaking changes are necessary, offer clear migration paths. This might involve introducing new fields alongside deprecated ones or providing new query structures that achieve the same results.
  3. Monitor Usage: Keep an eye on usage metrics to determine when it's safe to remove deprecated elements. Don't rush the process – give your clients time to adapt.
    note

    Tailcall supports a variety of integrations with monitoring tools to help you track usage metrics and make informed decisions about schema changes. you can check out our documentation for more information.

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.

Remember, a great GraphQL schema is never truly finished – it's a living, breathing entity that grows and evolves with your application's needs. Embrace this continuous evolution, and you'll create APIs that stand the test of time.

Posted By

Amit Singh
Head of Growth and Strategy @ Tailcall