DEV Community

Stefan  🚀
Stefan 🚀

Posted on • Originally published at wundergraph.com on

Why you should avoid exhaustive switch case in API clients

Exhaustive switch case in API clients should be avoided as it prevents your client from being forward compatible. In addition, this problem highlights three hidden problems when it comes to designing APIs. You might be breaking clients although you're confident that your changes are backward compatible.

Are you looking for an Open Source Graph Manager? Cosmo is the most complete solution including Schema Registry, Router, Studio, Metrics, Analytics, Distributed Tracing, Breaking Change detection and more.

I'm going to demonstrate this problem using GraphQL as an example as it's quite easy to accidentally run into this issue using the Query language, but the same problem can occur with any API style, like REST.

What is an exhaustive switch case?

An exhaustive switch case is a switch case that handles all possible cases. In the case of GraphQL, we have two situations where we might use a switch case:

  1. When we're querying abstract types like interfaces or unions, we can switch on the __typename field to determine the concrete type.
  2. When we're querying an enum, we can switch on the enum value to determine the concrete value.

Why exhaustive switch cases are problematic in API clients

Let's assume we have a GraphQL API that returns a list of users. Each user has a role field that is an enum with the values ADMIN, MODERATOR, and USER. We might be using a switch case to determine the role of each user:

const users = await client.query({
  query: gql`
    query {
      users {
        id
        name
        role
      }
    }
  `,
});

users.forEach((user) => {
  switch (user.role) {
    case "ADMIN":
      // ...
      break;
    case "MODERATOR":
      // ...
      break;
    case "USER":
      // ...
      break;
    default:
      throw new Error("Unknown role");
  }
});

Enter fullscreen mode Exit fullscreen mode

You wanted to be a good citizen and handle all possible cases, so you added the default case. But what happens if the API adds a new role, like SUPER_ADMIN? Your application will throw an error and crash.

Even though you didn't intend to do so, you gave the API the power to break your application. This is a problem because it prevents you from being forward compatible.

Let's take a look at another example where we're querying an interface.

const users = await client.query({
  query: gql`
    query {
      users {
        id
        name
        role
        ... on Admin {
          adminField
        }
        ... on Moderator {
          moderatorField
        }
        ... on User {
          userField
        }
      }
    }
  `,
});

users.forEach((user) => {
  switch (user.__typename) {
    case "Admin":
      // ...
      break;
    case "Moderator":
      // ...
      break;
    case "User":
      // ...
      break;
    default:
      throw new Error("Unknown user type");
  }
});

Enter fullscreen mode Exit fullscreen mode

Again, we're handling all possible cases. But what happens if the API adds a new type that implements the User interface? Again, your application will throw an error and crash.

It's a tiny little mistake in your client that prevents you from being forward compatible.

How to avoid exhaustive switch cases in API clients

The solution is quite simple: Don't use exhaustive switch cases in API clients. Instead, use a default case that handles all unknown cases, but don't throw an error.

const users = await client.query({
  query: gql`
    query {
      users {
        id
        name
        role
      }
    }
  `,
});

users.forEach((user) => {
  switch (user.role) {
    case "ADMIN":
      // ...
      break;
    case "MODERATOR":
      // ...
      break;
    case "USER":
      // ...
      break;
    default:
      // Handle unknown roles gracefully
  }
});

Enter fullscreen mode Exit fullscreen mode

What does exhaustive switch casing in clients tell us about API design?

The problem with exhaustive switch cases in API clients is that they prevent us from being forward compatible. Being aware of this problem, we might redefine our understanding of backward compatibility.

Before knowing about this problem, I was under the impression that adding a new enum value or a new type that implements an interface is backward compatible. But it's not.

You might be thinking that you're not responsible for how API users implement their clients. That's true, but if you're aware of this problem, you can take steps to prevent it.

Let's take a look at how we can prevent this problem from occurring in the first place.

Inform and educate your API users about the problem

The first and simplest step is to inform and educate your API users about the problem. If you're using GraphQL, you can add a note to each enum value and each type that implements an interface. Make your API users aware that they should not use exhaustive switch cases in their clients. It costs very little, but it can save your API users a lot of time and frustration.

Avoid using enums, interfaces, and unions in your API

You could also avoid using enums, interfaces, and unions in your API, but let's be honest, these are very useful concepts to design great APIs. So, avoiding them is not the solution, but we can do something similar.

You can be more conservative in your API design when adding new enum values, interface implementations, or union types. Think ahead and ask yourself what interface implementations or union types you might add in the future. There are many ways to do this.

When introducing a new enum value, you could think more thoroughly about its possible values. Instead of treating this lightly, you can take a step back and maybe add some more values, even though you're not returning them yet.

It's a common pattern for GraphQL APIs to model possible errors using interfaces or unions. The awareness of the switch case problem might lead you to think more about possible errors so you don't have to add them later.

Introduce a new field to avoid breaking clients

An alternative solution is to introduce a new field with a new type instead of adding a new enum value or a new type that implements an interface. This might not always be possible, and it might also lead to duplicate or overlapping fields with similar functionality, but it's a solution that can be considered.

Use a versioned API

Another approach is to version your API. If you're concerned about breaking clients, you can introduce a new version of your API instead of changing the existing one. This is definitely the safest approach, but it's also the most expensive one. Maintaining multiple versions of your API might bring a lot of overhead.

Adding to that, at least for GraphQL APIs, it's not common practice to version your API. Instead, you could introduce a new field and deprecate the old one. This way, you move the "versioning" into the schema itself.

Track API usage and clients to identify and communicate (breaking) changes

In addition to the previous approaches, there's another way to improve the situation for your API users. You can use analytics to track API usage of your API users. If you know which clients are using which fields, you can identify if a client is affected by a change you're about to make. You can then communicate the change to the API user and help them to update their client.

WunderGraph Cosmo is an Open Source solution for (Federated) GraphQL APIs to help you with this. Cosmo Router analyzes the traffic and sends schema usage reports to Cosmo Studio. Cosmo Studio then aggregates the reports and provides you with a dashboard to analyze the usage of your API.

In addition, we check the schema for breaking changes against recent API usage. Cosmo Studio also allows you to register clients, so you can track their API usage and notify them about breaking changes.

Conclusion

Exhaustive switch cases in API clients should be avoided as they prevent your client from being forward compatible. In addition, this problem highlights three hidden problems when it comes to designing APIs. Adding enum values, union members, or interface implementations might break clients.

Ideally, you're aware of how your API users are using your API. You can then inform and educate them about the problem, and keep them up to date about changes that might affect them.

Top comments (0)