Search code examples
graphqlapollographql-jsgraphql-tag

GraphQL response type / fragment struggle


I have some struggles with writing an API in graphql.

Every response in my api should look somewhat the same. So ideally this would be the graphql type:

type Response {
  success
  data {
    ... always different
  }
  errors {
    path
    message
  }
}

But because the data field in here is always different. Every Mutation/Query should have it's own response type (if i'm understanding graphql correctly).

So for Login this is the type I'm creating with a transformer function:

type LoginResponse {
  success
  data {
    user
    token
  }
  errors {
    path
    message
  }
}

Now in my front-end, I want to use the following fragment, because these properties are always present in every response.

fragment Response on LoginResponse {
  success
  errors {
    path
    message
  }
}

So the problem I have with this is already shown here, with a fragment you also define it's parent type. So I have to create as many seperate fragments as seperate response types.

Has someone maybe already struggled with this or is there a best practice for this I'm not seeing


Solution

  • In general, when you have a field that could resolve to one of a number of types, you can utilize a Union. If those types share one or more fields, you may want to utilize an Interface instead.

    A common pattern you see in schemas is the idea of a Node interface. You could have a query to fetch a node by id, for example:

    type Query {
      node(id: ID!): Node
    }
    
    interface Node {
      id: ID!
    }
    
    type Foo implements Node {
      id: ID!
      foo: String!
    }
    
    type Bar implements Node {
      id: ID!
      bar: Int!
    }
    

    Here, a Node could be either Foo or a Bar, so if we were to write a fragment for Node, it might look something like this:

    fragment NodeFields on Node {
      id # id is part of the interface itself
      ... on Bar {
        bar # fields specific to Bar
      }
      ... on Foo {
        foo # fields specific to Foo
      }
    }
    

    If you don't have shared fields, you can utilize a Union instead to the same effect:

    union SomeUnion = Foo | Bar
    

    So, to alleviate some of the repetition in your front-end code, you could make each of your Result types an interface, or better yet, have a single Result type with data being a union. Unfortunately, neither Interfaces or Unions work with Scalars or Lists, which complicates things if data is supposed to be a Scalar or List for some queries.

    At the end of the day, though, it's probably not advisable that you structure your schema this way in the first place. There's a number good reasons to avoid this kind of structure:

    1. GraphQL already returns your query result as a JSON object with data and errors properties.
    2. Returning errors inside of the GraphQL data will require additional logic to capture and format the errors, as opposed to being able to just throw an error anywhere and have GraphQL handle the error reporting for you.
    3. You won't be able to capture validation errors, so you'll potentially end up with errors in two places -- inside the errors array and inside data.errors. That also means your client needs to look for errors in two locations to do proper error handling.
    4. GraphQL is specifically design to allow a response to be partially resolved. That means even if some parts of a response errored out and failed to resolve, others may still be resolved and returned as part of the response. That means the concept of a response being "successful" doesn't really apply in GraphQL. If you absolutely need a success field, it would be far better to utilize something like formatResponse to add it to the response object after the query resolves.

    It will make things significantly simpler to stick with convention, and structure your schema along these lines:

    type Query {
      login: LoginResponse
    }
    
    type LoginResponse {
      token: String
      user: User
    }
    

    The actual response will still include data and errors:

    {
      "data": {
        "login": {
          "token": "",
        }
      },
      "errors": []
    }
    

    If you even need to use fragments, you will still need one fragment per type, but there will be significantly less repetition between fragments.