graphql-schema-design-patterns icon indicating copy to clipboard operation
graphql-schema-design-patterns copied to clipboard

GraphQL schema design patterns

A list of GraphQL schema design patterns.

  1. Argument as query field
  2. Argument as formatter
  3. Different fields for the semantically same value
  4. Interface with multiple implementations
  5. Explicit type field
  6. Generic object with type field
  7. Union with weak interface
  8. Any Object (or JSON)
  9. Relay pagination
  10. Wrap scalar in object
  11. Error types
  12. Top level groups
  13. Specific return type for each Mutation
  14. One specific input type for each Mutation

Argument as query field

The argument of a field serves as a query parameter to select the field.

Example

The id of an user is used to select the user object or a search for multiple users based on the name.

type Query {
  user(id: ID): User
  userSearch(name: String): [User]
}
type User {
  id: ID
  ...
}

Or based on

Discussion

The argument acts as a query parameter to select one (or more) values.

Argument as formatter

The argument of an field serves as a formatter.

Example

Instead of returning a fixed date format the format argument decides on the returned format.

type User {
  dateOfBirth(format: String): String
}

It also possible to use this pattern for non Scalars:

type WebPage {
  content(contentFormat: WebPageFormat): RenderedWebPage   
}
enum WebPageFormat {
  HTML,
  TEXT
}
interface RenderedWebPage {
  value:String
}
type Html implements RenderedWebPage {
...
}
type Text implements RenderedWebPage {
...
}

Discussion

The difference to Argument as query field is that the argument doesn't really specify what you want, but rather how you want it. The content and the dateOfBirth are already selected implicitly by the User or WebPage, the question is only the format.

Different fields for the semantically same value

Using different fields for the (semantically) same value.

Example

type User {
  dateOfBirthUTC: String
  dateOfBirthLocale: String
}

Discussion

This is the alternative to Argument as formatter where you can select the format by providing an argument. If both formats are needed as the same time this pattern allows for it directly:

...on User {
  dateOfBirthUTC,
  dateOfBirthLocal
}

while Argument as formatter requires the usage of aliases.

Interface with multiple implementations

Multiple objects implementing a interface and sharing some fields.

Example

interface Item {
  title: String
}
type Book implements Item{
  title: String
  pageCount: Int
}

type Movie implements Item{
  title: String
  director: String
}

Explicit type field

Having an explicit type field instead of relying on __typename.

Example

interface Item {
  title: String
  # static value for each Item type
  type: String
}

type Book implements Item {
  title: String
  type: String
  pageCount: Int
}

type Movie implements Item {
  title: String
  type: String
  director: String
}

Discussion

Every type name can be queried with the introspection field __typename. This is automatically the name of the current object. If for some reason this automatic behavior is not suitable a explicit type field can be added.

Generic object with type field

A generic objects which actually represents multiple types. It has a type field to recognize the actual type. Not every field makes sense for every actual type.

Example

A generic Item object which can be a book or a movie.

type Item {
  type: ItemType
  title: String
  # only available for Movies
  director:String
  # only available for Books
  pageCount: Int
}
enum ItemType {
  BOOK,
  MOVIE
}

Discussion

One difference to Interface with multiple implementations is the way it can be queried:

...on Item {
  title
  director
  pageCount
}

This allows for simpler queries without fragments for both types.

Union with weak interface

A weak interfaces implemented by a lot of objects and a union type restricting the possible implementations.

Example

type Node {
  id: ID
}

type Human implements Node {
  id: ID
  friends: [HumanFriend]
  ...
}

union HumanFriend = Dog | Human

type Dog implements Node {
  id: ID
  ...
}  

type House implements Node {
  id:ID
  ...
}

Discussion

It is useful to restrict the possible return types if the common interface is to broad. In the above example Node is to generic and a Human and Dog don't have enough in common to extract another interface.

Any Object (or JSON)

Opting out of the GraphQL type system by returning or accepting a general Object type (or JSON type as often described).

Relay pagination

Using the relay.js way of doing pagination: https://facebook.github.io/relay/graphql/connections.htm

Wrap scalar in object

Using a object type to wrap a single scalar value.

Example

Using a complex country type instead of a string.

type Address{
  country: Country 
}

type Country {
  code: String
  # possible more fields in the future
}

Discussion

This allows for further non-breaking changes of the schema on the costs of having more complex queries in the beginning. This is also an alternative to have custom scalars: Instead of having a Country scalar you have Country type. But scalars can be used as input and output types. This means a second input CountryInput ... input type is maybe needed.

Error types

Instead of using the normal GraphQL error handling, define explicit Error types.

Top level groups

Having top level fields without any meaning except grouping the underlying fields.

Example

type Query {
  humans: Humans
  animals: Animals
}
type Humans {
  human(id:ID): Human
  food: [HumanFood]
}

type Animals {
  animal(id: ID): Animal
  food: [AnimalFood]
} 
...

Discussion

Especially useful for larger APIs. The same pattern is not possible for Mutations, because only the top level mutation fields are guaranteed to be executed sequentially.

Specific return type for each Mutation

Each mutation field returns a specific type, just for this Mutation.

Example

type Mutation {
  addUser(input: AddUserInput): AddUserInputPayload
}
type AddUserInputPayload {
  user: User
  clientMutationId: String
}

One specific input type for each Mutation

Mutation becomes one single input argument.

Example

type Mutation {
  addUser(input: AddUserInput): AddUserInputPayload
}
input AddUserInput {
  name: String
  ...
}

Discussion

Related to Specific return type for each Mutation, but for the input type.