Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Designing a GraphQL API

Tutorial: Designing a GraphQL API

This tutorial was created by Shopify for internal purposes. We've created a public version of it since we think it's useful to anyone creating a GraphQL API.

It's based on lessons learned from creating and evolving production schemas at Shopify over almost 3 years. The tutorial has evolved and will continue to change in the future so nothing is set in stone.

We believe these design guidelines work in most cases. They may not all work for you. Even within the company we still question them and have exceptions since most rules can't apply 100% of the time. So don't just blindly copy and implement all of them. Pick and choose which ones make sense for you and your use cases.

Intro

Welcome! This document will walk you through designing a new GraphQL API (or a new piece of an existing GraphQL API). API design is a challenging task that strongly rewards iteration, experimentation, and a thorough understanding of your business domain.

Step Zero: Background

For the purposes of this tutorial, imagine you work at an e-commerce company. You have an existing GraphQL API exposing information about your products, but very little else. However, your team just finished a project implementing "collections" in the back-end and wants to expose collections over the API as well.

Collections are the new go-to method for grouping products; for example, you might have a collection of all of your t-shirts. Collections can be used for display purposes when browsing your website, and also for programmatic tasks (e.g. you may want to make a discount only apply to products in a certain collection).

On the back-end, your new feature has been implemented as follows:

  • All collections have some simple attributes like a title, a description body (which may include HTML formatting), and an image.
  • You have two specific kinds of collections: "manual" collections where you list the products you want them to include, and "automatic" collections where you specify some rules and let the collection populate itself.
  • Since the product-to-collection relationship is many-to-many, you've got a join table in the middle called CollectionMembership.
  • Collections, like products before them, can be either published (visible on the storefront) or not.

With this background, you're ready to start thinking about your API design.

Step One: A Bird's-Eye View

A naive version of the schema might look something like this (leaving out all the pre-existing types like Product):

interface Collection {
  id: ID!
  memberships: [CollectionMembership!]!
  title: String!
  imageId: ID
  bodyHtml: String
}

type AutomaticCollection implements Collection {
  id: ID!
  rules: [AutomaticCollectionRule!]!
  rulesApplyDisjunctively: Bool!
  memberships: [CollectionMembership!]!
  title: String!
  imageId: ID
  bodyHtml: String
}

type ManualCollection implements Collection {
  id: ID!
  memberships: [CollectionMembership!]!
  title: String!
  imageId: ID
  bodyHtml: String
}

type AutomaticCollectionRule {
  column: String!
  relation: String!
  condition: String!
}

type CollectionMembership {
  collectionId: ID!
  productId: ID!
}

This is already decently complicated at a glance, even though it's only four objects and an interface. It also clearly doesn't implement all of the features that we would need if we're going to be using this API to build out e.g. our mobile app's collection feature.

Let's take a step back. A decently complex GraphQL API will consist of many objects, related via multiple paths and with dozens of fields. Trying to design something like this all at once is a recipe for confusion and mistakes. Instead, you should start with a higher-level view first, focusing on just the types and their relations without worrying about specific fields or mutations. Basically think of an Entity-Relationship model but with a few GraphQL-specific bits thrown in. If we shrink our naive schema down like that, we end up with the following:

interface Collection {
  Image
  [CollectionMembership]
}

type AutomaticCollection implements Collection {
  [AutomaticCollectionRule]
  Image
  [CollectionMembership]
}

type ManualCollection implements Collection {
  Image
  [CollectionMembership]
}

type AutomaticCollectionRule { }

type CollectionMembership {
  Collection
  Product
}

To get this simplified representation, I took out all scalar fields, all field names, and all nullability information. What you're left with still looks kind of like GraphQL but lets you focus on higher level of the types and their relationships.

Rule #1: Always start with a high-level view of the objects and their relationships before you deal with specific fields.

Step Two: A Clean Slate

Now that we have something simple to work with, we can address the major flaws with this design.

As previously mentioned, our implementation defines the existence of manual and automatic collections, as well as the use of a collector join table. Our naive API design was clearly structured around our implementation, but this was a mistake.

The root problem with this approach is that an API operates for a different purpose than an implementation, and frequently at a different level of abstraction. In this case, our implementation has led us astray on a number of different fronts.

Representing CollectionMemberships

The one that may have stood out to you already, and is hopefully fairly obvious, is the inclusion of the CollectionMembership type in the schema. The collection memberships table is used to represent the many-to-many relationship between products and collections. Now read that last sentence again: the relationship is between products and collections; from a semantic, business domain perspective, collection memberships have nothing to do with anything. They are an implementation detail.

This means that they don't belong in our API. Instead, our API should expose the actual business domain relationship to products directly. If we take out collection memberships, the resulting high-level design now looks like:

interface Collection {
  Image
  [Product]
}

type AutomaticCollection implements Collection {
  [AutomaticCollectionRule]
  Image
  [Product]
}

type ManualCollection implements Collection {
  Image
  [Product]
}

type AutomaticCollectionRule { }

This is much better.

Rule #2: Never expose implementation details in your API design.

Representing Collections

This API design still has one major flaw, though it's one that's probably much less obvious without a really thorough understanding of the business domain. In our existing design, we model AutomaticCollections and ManualCollections as two different types, each implementing a common Collection interface. Intuitively this makes a fair bit of sense: they have a lot of common fields, but are still distinctly different in their relationships (AutomaticCollections have rules) and some of their behaviour.

But from a business model perspective, these differences are also basically an implementation detail. The defining behaviour of a collection is that it groups products; the method of choosing those products is secondary. We could expand our implementation at some point to permit some third method of choosing products (machine learning?) or to permit mixing methods (some rules and some manually added products) and they would still just be collections. You could even argue that the fact we don't permit mixing right now is an implementation failure. All of this to say is that the shape of our API should really look more like this:

type Collection {
  [CollectionRule]
  Image
  [Product]
}

type CollectionRule { }

That's really nice. The immediate concern you may have at this point is that we're now pretending ManualCollections have rules, but remember that this relationship is a list. In our new API design, a "ManualCollection" is just a Collection whose list of rules is empty.

Conclusion

Choosing the best API design at this level of abstraction necessarily requires you to have a very deep understanding of the problem domain you're modeling. It's hard in a tutorial setting to provide that depth of context for a specific topic, but hopefully the collection design is simple enough that the reasoning still makes sense. Even if you don't have this depth of understanding specifically for collections, you still absolutely need it for whatever domain you're actually modeling. It is critically important when designing your API that you ask yourself these tough questions, and don't just blindly follow the implementation.

On a closely related note, a good API does not model the user interface either. The implementation and the UI can both be used for inspiration and input into your API design, but the final driver of your decisions must always be the business domain.

Even more importantly, existing REST API choices should not necessarily be copied. The design principles behind REST and GraphQL can lead to very different choices, so don't assume that what worked for your REST API is a good choice for GraphQL.

As much as possible let go of your baggage and start from scratch.

Rule #3: Design your API around the business domain, not the implementation, user-interface, or legacy APIs.

Step Three: Adding Detail

Now that we have a clean structure to model our types, we can add back our fields and start to work at that level of detail again.

Before we start adding detail, ask yourself if it's really needed at this time. Just because a database column, model property, or REST attribute may exist, doesn't mean it automatically needs to be added to the GraphQL schema.

Exposing a schema element (field, argument, type, etc) should be driven by an actual need and use case. GraphQL schemas can easily be evolved by adding elements, but changing or removing them are breaking changes and much more difficult.

Rule #4: It's easier to add fields than to remove them.

Starting point

Restoring our naive fields adjusted for our new structure, we get:

type Collection {
  id: ID!
  rules: [CollectionRule!]!
  rulesApplyDisjunctively: Bool!
  products: [Product!]!
  title: String!
  imageId: ID
  bodyHtml: String
}

type CollectionRule {
  column: String!
  relation: String!
  condition: String!
}

Now we have a whole new host of design problems to resolve. We'll work through the fields in order top to bottom, fixing things as we go.

IDs and the Node Interface

The very first field in our Collection type is an ID field, which is fine and normal; this ID is what we'll need to use to identify our collections throughout the API, in particular when performing actions like modifying or deleting them. However there is one piece missing from this part of our design: the Node interface. This is a very commonly-used interface that already exists in most schemas and looks like this:

interface Node {
  id: ID!
}

It hints to the client that this object is persisted and retrievable by the given ID, which allows the client to accurately and efficiently manage local caches and other tricks. Most of your major identifiable business objects (e.g. products, collections, etc) should implement Node.

The beginning of our design now just looks like:

type Collection implements Node {
  id: ID!
}

Rule #5: Major business-object types should always implement Node.

Rules and Subobjects

We will consider the next two fields in our Collection type together: rules, and rulesApplyDisjunctively. The first is pretty straightforward: a list of rules. Do note that both the list itself and the elements of the list are marked as non-null: this is fine, as GraphQL does distinguish between null and [] and [null]. For manual collections, this list can be empty, but it cannot be null nor can it contain a null.

Protip: List-type fields are almost always non-null lists with non-null elements. If you want a nullable list make sure there is real semantic value in being able to distinguish between an empty list and a null one.

The second field is a bit weird: it is a boolean field indicating whether the rules apply disjunctively or not. It is also non-null, but here we run into a problem: what value should this field take for manual collections? Making it either false or true feels misleading, but making the field nullable then makes it a kind of weird tri-state flag which is also awkward when dealing with automatic collections. While we're puzzling over this, there is one other thing that is worth mentioning: these two fields are obviously and intricately related. This is true semantically, and it's also hinted by the fact that we chose names with a shared prefix. Is there a way to indicate this relationship in the schema somehow?

As a matter of fact, we can solve all of these problems in one fell swoop by deviating even further from our underlying implementation and introducing a new GraphQL type with no direct model equivalent: CollectionRuleSet. This is often warranted when you have a set of closely-related fields whose values and behaviour are linked. By grouping the two fields into their own type at the API level we provide a clear semantic indicator and also solve all of our problems around nullability: for manual collections, it is the rule-set itself which is null. The boolean field can remain non-null. This leads us to the following design:

type Collection implements Node {
  id: ID!
  ruleSet: CollectionRuleSet
  products: [Product!]!
  title: String!
  imageId: ID
  bodyHtml: String
}

type CollectionRuleSet {
  rules: [CollectionRule!]!
  appliesDisjunctively: Bool!
}

type CollectionRule {
  column: String!
  relation: String!
  condition: String!
}

Protip: Like lists, boolean fields are almost always non-null. If you want a nullable boolean, make sure there is real semantic value in being able to distinguish between all three states (null/false/true) and that it doesn't indicate a bigger design flaw.

Rule #6: Group closely-related fields together into subobjects.

Lists and Pagination

Next on the chopping block is our products field. This one might seem safe; after all we already "fixed" this relation back when we removed our CollectionMembership type, but in fact there's something else wrong here.

The field as currently defined returns an array of products, but collections can easily have many tens of thousands of products, and trying to gather all of those into a single array would be incredibly expensive and inefficient. For situations like this, GraphQL provides lists pagination.

Whenever you implement a field or relation returning multiple objects, always ask yourself if the field should be paginated or not. How many of this object can there be? What quantity is considered pathological?

Paginating a field means you need to implement a pagination solution first. This tutorial uses Connections which is defined by the Relay Connection spec.

In this case, paginating the products field in our design is as simple as changing its definition to products: ProductConnection!. Assuming you have connections implemented, your types would look like this:

type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
}

type ProductEdge {
  cursor: String!
  node: Product!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}

Rule #7: Always check whether list fields should be paginated or not.

Strings

Next up is the title field. This one is legitimately fine the way it is. It's a simple string, and it's marked non-null because all collections must have a title.

Protip: As with booleans and lists, it's worth noting that GraphQL does distinguish between empty strings ("") and nulls (null), so if you need a nullable string make sure there is a legitimate semantic difference between not-present (null) and present-but-empty (""). You can often think of empty strings as meaning "applicable, but not populated", and null strings meaning "not applicable".

IDs and Relations

Now we come to the imageId field. This field is a classic example of what happens when you try and apply REST designs to GraphQL. In REST APIs it's pretty common to include the IDs of other objects in your response as a way to link together those objects, but this is a major anti-pattern in GraphQL. Instead of providing an ID, and forcing the client to do another round-trip to get any information on the object, we should just include the object directly into the graph - that's what GraphQL is for after all. In REST APIs this pattern often isn't practical, since it inflates the size of the response significantly when the included objects are large. However, this works fine in GraphQL because every field must be explicitly queried or the server won't return it.

As a general rule, the only ID fields in your design should be the IDs of the object itself. Any time you have some other ID field, it should probably be an object reference instead. Applying this to our schema so far, we get:

type Collection implements Node {
  id: ID!
  ruleSet: CollectionRuleSet
  products: ProductConnection!
  title: String!
  image: Image
  bodyHtml: String
}

type Image {
  id: ID!
}

type CollectionRuleSet {
  rules: [CollectionRule!]!
  appliesDisjunctively: Bool!
}

type CollectionRule {
  column: String!
  relation: String!
  condition: String!
}

Rule #8: Always use object references instead of ID fields.

Naming and Scalars

The last field in our simple Collection type is bodyHtml. To a user who is unfamiliar with the way that collections were implemented, it's not entirely obvious what this field is for; it's the body description of the specific collection. The first thing we can do to make this API better is just to rename it to description, which is a much clearer name.

Rule #9: Choose field names based on what makes sense, not based on the implementation or what the field is called in legacy APIs.

Next, we can make it non-nullable. As we talked about with the title field, it doesn't make sense to distinguish between the field being null and simply being an empty string, so we don't expose that in the API. Even if your database schema does allow records to have a null value for this column, we can hide that at the implementation layer.

Finally, we need to consider if String is actually the right type for this field. GraphQL provides a decent set of built-in scalar types (String, Int, Boolean, etc) but it also lets you define your own, and this is a prime use case for that feature. Most schemas define their own set of additional scalars depending on their use cases. These provide additional context and semantic value for clients. In this case, it probably makes sense to define a custom HTML scalar for use here (and potentially elsewhere) when the string in question must be valid HTML.

Whenever you're adding a scalar field, it's worth checking your existing list of custom scalars to see if one of them would be a better fit. If you're adding a field and you think a new custom scalar would be appropriate, it's worth talking it over with your team to make sure you're capturing the right concept.

Rule #10: Use custom scalar types when you're exposing something with specific semantic value.

Pagination Again

That covers all of the fields in our core Collection type. The next object is CollectionRuleSet, which is quite simple. The only question here is whether or not the list of rules should be paginated. In this case the existing array actually makes sense; paginating the list of rules would be overkill. Most collections will only have a handful of rules, and there isn't a good use case for a collection to have a large rule set. Even a dozen rules is probably an indicator that you need to rethink that collection, or should just be manually adding products.

Enums

This brings us to the final type in our schema, CollectionRule. Each rule consists of a column to match on (e.g. product title), a type of relation (e.g. equality) and an actual value to use (e.g. "Boots") which is confusingly called condition. That last field can be renamed, and so should column; column is very database-specific terminology, and we're working in GraphQL. field is probably a better choice.

As far as types go, both field and relation are probably implemented internally as enumerations (assuming your language of choice even has enumerations). Fortunately GraphQL has enums as well, so we can convert those two fields to enums. Our completed schema design now looks like this:

type Collection implements Node {
  id: ID!
  ruleSet: CollectionRuleSet
  products: ProductConnection!
  title: String!
  image: Image
  description: HTML!
}

type CollectionRuleSet {
  rules: [CollectionRule!]!
  appliesDisjunctively: Bool!
}

type CollectionRule {
  field: CollectionRuleField!
  relation: CollectionRuleRelation!
  value: String!
}

enum CollectionRuleField {
  TAG
  TITLE
  TYPE
  INVENTORY
  PRICE
  VENDOR
}

enum CollectionRuleRelation {
  CONTAINS
  ENDS_WITH
  EQUALS
  GREATER_THAN
  LESS_THAN
  NOT_CONTAINS
  NOT_EQUALS
  STARTS_WITH
}

Rule #11: Use enums for fields which can only take a specific set of values.

Step Four: Business Logic

We now have a minimal but well-designed GraphQL API for collections. There is a lot of detail to collections that we haven't dealt with - any real implementation of this feature would need a lot more fields to deal with things like product sort order, publishing, etc. - but as a rule those fields will all follow the same design patterns layed out here. However, there are still a few things which bear looking at in more detail.

For this section, it is most convenient to start with a motivating use case from the hypothetical client of our API. Let us therefore imagine that the client developer we have been working with needs to know something very specific: whether a given product is a member of a collection or not. Of course this is something that the client can already answer with our existing API: we expose the complete set of products in a collection, so the client simply has to iterate through looking for the product they care about.

This solution has two problems though. The first, obvious problem is that it's inefficient; collections can contain millions of products, and having the client fetch and iterate through them all would be extremely slow. The second, bigger problem, is that it requires the client to write code. This last point is a critical piece of design philosophy: the server should always be the single source of truth for any business logic. An API almost always exists to serve more than one client, and if each of those clients has to implement the same logic then you've effectively got code duplication, with all the extra work and room for error which that entails.

Rule #12: The API should provide business logic, not just data. Complex calculations should be done on the server, in one place, not on the client, in many places.

Back to our client use-case, the best answer here is to provide a new field specifically dedicated to solving this problem. Practically, this looks like:

type Collection implements Node {
  # ...
  hasProduct(id: ID!): Bool!
}

This field takes the ID of a product and returns a boolean based on the server determining if a product is in the collection or not. The fact that this sort-of duplicates the data from the existing products field is irrelevant. GraphQL returns only what clients explicitly ask for, so unlike REST it does not cost us anything to add a bunch of secondary fields. The client doesn't have to write any code beyond querying an additional field, and the total bandwidth used is a single ID plus a single boolean.

One follow-up warning though: just because we're providing business logic in a situation does not mean we don't have to provide the raw data too. Clients should be able to do the business logic themselves, if they have to. You can’t predict all of the logic a client is going to want, and there isn't always an easy channel for clients to ask for additional fields (though you should strive to ensure such a channel exists as much as possible).

Rule #13: Provide the raw data too, even when there's business logic around it.

Finally, don't let business-logic fields affect the overall shape of the API. The business domain data is still the core model. If you're finding the business logic doesn't really fit, then that's a sign that maybe your underlying model isn't right.

Step Five: Mutations

The final missing piece of our GraphQL schema design is the ability to actually change values: creating, updating, and deleting collections and related pieces. As with the readable portion of the schema we should start with a high-level view: in this case, of just the various mutations we will want to implement, without worrying about their specific inputs or outputs. Naively we might follow the CRUD paradigm and have just create, delete, and update mutations. While this is a decent starting place, it is insufficient for a proper GraphQL API.

Separate Logical Actions

The first thing we might notice if we were to stick to just CRUD is that our update mutation quickly becomes massive, responsible not just for updating simple scalar values like title but also for performing complex actions like publishing/unpublishing, adding/removing/reordering the products in the collection, changing the rules for automatic collections, etc. This makes it hard to implement on the server and hard to reason about for the client. Instead, we can take advantage of GraphQL to split it apart into more granular, logical actions. As a very first pass, we can split out publish/unpublish resulting in the following mutation list:

  • create
  • delete
  • update
  • publish
  • unpublish

Rule #14: Write separate mutations for separate logical actions on a resource.

Manipulating Relationships

The update mutation still has far too many responsibilities so it makes sense to continue splitting it up, but we will deal with these actions separately since they're worth thinking about from another dimension as well: the manipulation of object relationships (e.g. one-to-many, many-to-many). We've already considered the use of IDs vs embedding, and the use of pagination vs arrays in the read API, and there are some similar issues to deal with when mutating these relationships.

For the relationship between products and collections, there are a couple of styles we could broadly consider:

  • Embedding the entire relationship (e.g. products: [ProductInput!]!) into the update mutation is the CRUD-style default, but of course it quickly becomes inefficient when the list is large.
  • Embedding "delta" fields (e.g. productsToAdd: [ID!]! and productsToRemove: [ID!]!) into the update mutation is more efficient since only the changed IDs need to be specified instead of the entire list, but it still keeps the actions tied together.
  • Splitting it up entirely into separate mutations (addProduct, removeProduct, etc.) is the most powerful and flexible but also the most work.

The last option is generally the safest call, especially since mutations like this will usually be distinct logical actions anyway. However, there are a lot of factors to consider:

  • Is the relationship large or paginated? If so, embedding the entire list is definitely impractical, however either delta fields or separate mutations could still work. If the relationship is always small though (especially if it's one-to-one), embedding may be the simplest choice.
  • Is the relationship ordered? The product-collection relationship is ordered, and permits manual reordering. Order is naturally supported by the embedded list or by separate mutations (you can add a reorderProducts mutation) but isn't an option for delta fields.
  • Is the relationship mandatory? Products and collections can both exist on their own outside of the relationship, with their own create/delete lifecycle. If the relationship were mandatory (i.e. products must be in a collection) then this would strongly suggest separate mutations because the action would actually be to create a product, not just to update the relationship.
  • Do both sides have IDs? The collection-rule relationship is mandatory (rules can't exist without collections) but rules don't even have IDs; they are clearly subservient to their collection, and since the relationship is also small, embedding the list is actually not a bad choice here. Anything else would require rules to be individually identifiable and that feels like overkill.

Rule #15: Mutating relationships is really complicated and not easily summarized into a snappy rule.

If you stir all of this together, for collections we end up with the following list of mutations:

  • create
  • delete
  • update
  • publish
  • unpublish
  • addProducts
  • removeProducts
  • reorderProducts

Products we split into their own mutations, because the relationship is large, and ordered. Rules we left inline because the relationship is small, and rules are sufficiently minor to not have IDs.

Finally, you may note that we have mutations like addProducts and not addProduct. This is simply a convenience for the client, since the common use case when manipulating this relationship will be to add, remove, or reorder more than one product at a time.

Rule #16: When writing separate mutations for relationships, consider whether it would be useful for the mutations to operate on multiple elements at once.

Input: Structure, Part 1

Now that we know which mutations we want to write, we get to figure out what their input structures look like. If you've been browsing any of the real production schemas that are publicly available, you may have noticed that many mutations define a single global Input type to hold all of their arguments: this pattern was a requirement of some legacy clients but is no longer needed for new code; we can ignore it.

For many simple mutations, an ID or a handful of IDs are all that is needed, making this step quite simple. Among collections, we can quickly knock out the following mutation arguments:

  • delete, publish and unpublish all simply need a single collection ID
  • addProducts and removeProducts both need the collection ID as well as a list of product IDs This leaves us with only three remaining "complicated" inputs to design:
  • create
  • update
  • reorderProducts

Let's start with create. A very naive input might look kind of like our original naive collection model when we started, but we can already do better than that. Based on our final collection model and the discussion of relationships above, we can start with something like this:

type Mutation {
  collectionDelete(id: ID!)
  collectionPublish(collectionId: ID!)
  collectionUnpublish(collectionId: ID!)
  collectionAddProducts(collectionId: ID!, productIds: [ID!]!)
  collectionRemoveProducts(collectionId: ID!, productIds: [ID!])
  collectionCreate(title: String!, ruleSet: CollectionRuleSetInput, image: ImageInput, description: HTML!)
}

input CollectionRuleSetInput {
  rules: [CollectionRuleInput!]!
  appliesDisjunctively: Bool!
}

input CollectionRuleInput {
  field: CollectionRuleField!
  relation: CollectionRuleRelation!
  value: String!
}

First a quick note on naming: you'll notice that we named all of our mutations in the form collection<Action> rather than the more naturally-English <action>Collection. Unfortunately, GraphQL does not provide a method for grouping or otherwise organizing mutations, so we are forced into alphabetization as a workaround. Putting the core type first ensures that all of the related mutations group together in the final list.

Rule #17: Prefix mutation names with the object they are mutating for alphabetical grouping (e.g. use orderCancel instead of cancelOrder).

Input: Scalars

This draft is a lot better than a completely naive approach, but it still isn't perfect. In particular, the description input field has a couple of issues. A non-null HTML field makes sense for the output of a collection's description, but it doesn't work as well for input for a couple of reasons. First-off, while ! denotes non-nullability on output, it doesn't mean quite the same thing on input; instead it denotes more the concept of whether a field is "required". A required field is one the client must provide in order for the request to proceed, and this isn't true for description. We don't want to prevent clients from creating collections if they don't provide a description (or equivalently, we don't want to force them to provide a useless ""), so we should make description non-required.

Rule #18: Only make input fields required if they're actually semantically required for the mutation to proceed.

The other issue with description is its type; this may seem counter-intuitive since it is already strongly-typed (HTML instead of String) and we've been all about strong typing so far. But again, inputs behave a little differently. Validation of strong typing on input happens at the GraphQL layer before any "userspace" code gets run, which means that realistically clients have to deal with two layers of errors: GraphQL-layer validation errors, and business-layer validation errors (for example something like: you've reached the limit of collections you can create with your current storage). In order to simplify this process, we intentionally weakly type input fields when it might be difficult for the client to validate up-front. This lets the business-logic side handle all of the validation, and lets the client only deal with errors from one spot.

Rule #19: Use weaker types for inputs (e.g. String instead of Email) when the format is unambiguous and client-side validation is complex. This lets the server run all non-trivial validations at once and return the errors in a single place in a single format, simplifying the client.

It is important to note, though, that this is not an invitation to weakly-type all your inputs. We still use strongly-typed enums for the field and relation values on our rule input, and we would still use strong typing for certain other inputs like DateTimes if we had any in this example. The key differentiating factors are the complexity of client-side validation, and the ambiguity of the format. HTML is a well-defined, unambiguous specification, but is quite complex to validate. On the other hand, there are hundreds of ways to represent a date or time as a string, all of them reasonably simple, so it benefits from a strong scalar type to specify which format we expect.

Rule #20: Use stronger types for inputs (e.g. DateTime instead of String) when the format may be ambiguous and client-side validation is simple. This provides clarity and encourages clients to use stricter input controls (e.g. a date-picker widget instead of a free-text field).

Input: Structure, Part 2

Continuing on to the update mutation, it might look something like this:

type Mutation {
  # ...
  collectionCreate(title: String!, ruleSet: CollectionRuleSetInput, image: ImageInput, description: String)
  collectionUpdate(id: ID!, title: String, ruleSet: CollectionRuleSetInput, image: ImageInput, description: String)
}

You'll note that this is very similar to our create mutation, with two differences: an id argument was added which determines which collection to update, and title is no longer required since the collection must already have one. Ignoring the title's required status for a moment our example mutations have four duplicate arguments, and a complete collections model would include quite a few more.

While there are some arguments for leaving these mutations as-is, we have decided that situations like this call for DRYing up the common portions of the arguments, even at the cost of requiredness. This has a couple of advantages:

  • We end up with a single input object representing the concept of a collection and mirroring the single Collection type our schema already has.
  • Clients can share code between their create and update forms (a common pattern) because they end up manipulating the same kind of input object.
  • Mutations remain slim and readable with only a couple of top-level arguments.

The primary cost, of course, is that it's no longer clear from the schema that the title is required on creation. Our schema ends up looking like this:

type Mutation {
  # ...
  collectionCreate(collection: CollectionInput!)
  collectionUpdate(id: ID!, collection: CollectionInput!)
}

input CollectionInput {
  title: String
  ruleSet: CollectionRuleSetInput
  image: ImageInput
  description: String
}

Rule #21: Structure mutation inputs to reduce duplication, even if this requires relaxing requiredness constraints on certain fields.

Output

The final design question we need to deal with is the return value of our mutations. Typically mutations can succeed or fail, and while GraphQL does include explicit support for query-level errors, these are not ideal for business-level mutation failures. Instead, we reserve these top-level errors for failures of the client (e.g. requesting a non-existant field) rather than of the user. As such, each mutation should define a "payload" type which includes a user-errors field in addition to any other values that might be useful. For create, that might look like this:

type CollectionCreatePayload {
  userErrors: [UserError!]!
  collection: Collection
}

type UserError {
  message: String!

  # Path to input field which caused the error.
  field: [String!]
}

Here, a successful mutation would return an empty list for userErrors and would return the newly-created collection for the collection field. An unsuccessful one would return one or more UserError objects, and null for the collection.

Rule #22: Mutations should provide user/business-level errors via a userErrors field on the mutation payload. The top-level query errors entry is reserved for client and server-level errors.

In many implementations, much of this structure is provided automatically and all you will have to define is the collection return field.

For the update mutation, we follow exactly the same pattern:

type CollectionUpdatePayload {
  userErrors: [UserError!]!
  collection: Collection
}

It's worth noting that collection is still nullable even here, since if the provided ID doesn't represent a valid collection there is no collection to return.

Rule #23: Most payload fields for a mutation should be nullable, unless there is really a value to return in every possible error case.

Conclusion

Thank you for reading our tutorial! Hopefully by this point you have a solid idea of how to design a good GraphQL API.

Once you've designed an API you're happy with, it's time to implement it!

@lanceharper

This comment has been minimized.

Show comment
Hide comment
@lanceharper

lanceharper Jun 18, 2018

This is a fantastic write-up. I hope you consider including some variant of this as a blog post somewhere. I really appreciate explanations that take readers progressively from naive implementations that novices will likely attempt to more idiomatic approaches.

lanceharper commented Jun 18, 2018

This is a fantastic write-up. I hope you consider including some variant of this as a blog post somewhere. I really appreciate explanations that take readers progressively from naive implementations that novices will likely attempt to more idiomatic approaches.

@felippenardi

This comment has been minimized.

Show comment
Hide comment
@felippenardi

felippenardi Jul 12, 2018

@swalkinshaw Why is field in userErrors an array of strings?

felippenardi commented Jul 12, 2018

@swalkinshaw Why is field in userErrors an array of strings?

@swalkinshaw

This comment has been minimized.

Show comment
Hide comment
@swalkinshaw

swalkinshaw Jul 13, 2018

@felippenardi to represent a path to potentially nested fields (for input objects).

Owner

swalkinshaw commented Jul 13, 2018

@felippenardi to represent a path to potentially nested fields (for input objects).

@danburgo

This comment has been minimized.

Show comment
Hide comment
@danburgo

danburgo Jul 17, 2018

@swalkinshaw - Great post, quick question how would you model a rule that depended on an aggregation of other objects. For example the parent object property would have a count or sum of a field in the subobjects. So if someone changes the property in the subobject, it must apply to be equal to the total count or sum in the parent object, otherwise it would violate the rule

danburgo commented Jul 17, 2018

@swalkinshaw - Great post, quick question how would you model a rule that depended on an aggregation of other objects. For example the parent object property would have a count or sum of a field in the subobjects. So if someone changes the property in the subobject, it must apply to be equal to the total count or sum in the parent object, otherwise it would violate the rule

@rhnonose

This comment has been minimized.

Show comment
Hide comment
@rhnonose

rhnonose Jul 17, 2018

Very good content, thank you for posting this.

rhnonose commented Jul 17, 2018

Very good content, thank you for posting this.

@albertorestifo

This comment has been minimized.

Show comment
Hide comment
@albertorestifo

albertorestifo Jul 18, 2018

Great content! Just one question:

When speaking of the mutations return payload, you write:

In many implementations, much of this structure is provided automatically and all you will have to define is the collection return field.

Can you provide an example of such implementation?

albertorestifo commented Jul 18, 2018

Great content! Just one question:

When speaking of the mutations return payload, you write:

In many implementations, much of this structure is provided automatically and all you will have to define is the collection return field.

Can you provide an example of such implementation?

@tungsheng

This comment has been minimized.

Show comment
Hide comment
@tungsheng

tungsheng Jul 19, 2018

This is super helpful!! Thank you for sharing!!

tungsheng commented Jul 19, 2018

This is super helpful!! Thank you for sharing!!

@danielbentes

This comment has been minimized.

Show comment
Hide comment
@danielbentes

danielbentes Jul 19, 2018

Great writeup! Appreciated!

danielbentes commented Jul 19, 2018

Great writeup! Appreciated!

@stephencorwin

This comment has been minimized.

Show comment
Hide comment
@stephencorwin

stephencorwin Jul 19, 2018

Awesome writeup! I think this is a fantastic baseline to follow. There are a couple rules though that are mentioned as a bit absolute, but I feel can be broken for the sake of performance or use case.

Namely:

Rule #8: Always use object references instead of ID fields.

If I have a deeply nested object or static (does not change) object, then it feels like a basic optimization to fetch this data ahead of time and reuse what has already been retrieved.

In my example, I have something to the effect of:

type Klass {
  uid: ID!
  imageUrl: String
  description: String
  health: Int
  mana: Int
  power: Int
  block: Int
}

type Hero {
  uid: ID!
  name: String!
  klassUid: ID!
}

type User {
  uid: ID!
  heroes: [Hero]
  parties: [String]
}

In this case -- I have 20 heroes associated with my User and have the need to lookup other Users, I can dramatically reduce the payload size of each query by loading the Klass metadata ahead of time and doing a client-side join/reference in JS.

render() {
  const klass: IKlass = metadata.klasses[this.props.klassUid]

  return {
    <div>
      <h1>{hero.name}</h1>
      <img src={klass.image} />
      <p>{klass.description}</p>
    </div>
  }
}

Similarly, I have an Apollo GraphQL Server resolving Redis transactions for the in-game stateful information. I definitely don't want to store all of this information in a volatile Redis instance where I am paying a high dollar for in-memory storage. I would rather store in cheaply in MongoDB. The game server is on a separate cluster than the general API because I anticipate that I will need to have many more instances of the game server (Redis + Websocket over GraphQL Subscriptions) than I will of the API.

Since there is this logistical separation, I can apply the same optimization as before for the Hero data coming from Redis. No need to return the expanded Hero query with the Klass object embedded. I can just reference it client-side with information I already have.

Given this wall of text preface...
Would it then be suitable to either:
A. Only have the klassUid field on Hero
B. Embed the Klass object on EVERY Hero
C. Include both the klassUid and the Klass embed as siblings on the Hero and leave it up to the client to only request the klassUid if the optimization is being performed client-side.

Note: I just thought of C as I was writing this response.

One issue I can see with option C though is that in the case of the game server example above, I don't have an active connection to Mongo -- only Redis.

thoughts?

stephencorwin commented Jul 19, 2018

Awesome writeup! I think this is a fantastic baseline to follow. There are a couple rules though that are mentioned as a bit absolute, but I feel can be broken for the sake of performance or use case.

Namely:

Rule #8: Always use object references instead of ID fields.

If I have a deeply nested object or static (does not change) object, then it feels like a basic optimization to fetch this data ahead of time and reuse what has already been retrieved.

In my example, I have something to the effect of:

type Klass {
  uid: ID!
  imageUrl: String
  description: String
  health: Int
  mana: Int
  power: Int
  block: Int
}

type Hero {
  uid: ID!
  name: String!
  klassUid: ID!
}

type User {
  uid: ID!
  heroes: [Hero]
  parties: [String]
}

In this case -- I have 20 heroes associated with my User and have the need to lookup other Users, I can dramatically reduce the payload size of each query by loading the Klass metadata ahead of time and doing a client-side join/reference in JS.

render() {
  const klass: IKlass = metadata.klasses[this.props.klassUid]

  return {
    <div>
      <h1>{hero.name}</h1>
      <img src={klass.image} />
      <p>{klass.description}</p>
    </div>
  }
}

Similarly, I have an Apollo GraphQL Server resolving Redis transactions for the in-game stateful information. I definitely don't want to store all of this information in a volatile Redis instance where I am paying a high dollar for in-memory storage. I would rather store in cheaply in MongoDB. The game server is on a separate cluster than the general API because I anticipate that I will need to have many more instances of the game server (Redis + Websocket over GraphQL Subscriptions) than I will of the API.

Since there is this logistical separation, I can apply the same optimization as before for the Hero data coming from Redis. No need to return the expanded Hero query with the Klass object embedded. I can just reference it client-side with information I already have.

Given this wall of text preface...
Would it then be suitable to either:
A. Only have the klassUid field on Hero
B. Embed the Klass object on EVERY Hero
C. Include both the klassUid and the Klass embed as siblings on the Hero and leave it up to the client to only request the klassUid if the optimization is being performed client-side.

Note: I just thought of C as I was writing this response.

One issue I can see with option C though is that in the case of the game server example above, I don't have an active connection to Mongo -- only Redis.

thoughts?

@swalkinshaw

This comment has been minimized.

Show comment
Hide comment
@swalkinshaw

swalkinshaw Jul 19, 2018

@albertorestifo graphql-ruby has one: http://graphql-ruby.org/mutations/mutation_classes.html#example-mutation-class

Those fields get added to an automatically generated MutationNamePayload type which is returned from the mutation field.

Owner

swalkinshaw commented Jul 19, 2018

@albertorestifo graphql-ruby has one: http://graphql-ruby.org/mutations/mutation_classes.html#example-mutation-class

Those fields get added to an automatically generated MutationNamePayload type which is returned from the mutation field.

@swalkinshaw

This comment has been minimized.

Show comment
Hide comment
@swalkinshaw

swalkinshaw Jul 19, 2018

@stephencorwin option C seems good (at first glance). It actually kind of fits in with our Rule #13.

Owner

swalkinshaw commented Jul 19, 2018

@stephencorwin option C seems good (at first glance). It actually kind of fits in with our Rule #13.

@inbararan

This comment has been minimized.

Show comment
Hide comment
@inbararan

inbararan Jul 23, 2018

@stephencorwin How about enabling the whole class data in the api and in the client using only the uid?
I mean using (inside the user query) klass { uid }?

(Option B as much as the api is concerned)

inbararan commented Jul 23, 2018

@stephencorwin How about enabling the whole class data in the api and in the client using only the uid?
I mean using (inside the user query) klass { uid }?

(Option B as much as the api is concerned)

@jshowalter

This comment has been minimized.

Show comment
Hide comment
@jshowalter

jshowalter Jul 25, 2018

"follow the same design patterns layed out here" should be "follow the same design patterns laid out here"

jshowalter commented Jul 25, 2018

"follow the same design patterns layed out here" should be "follow the same design patterns laid out here"

@stephencorwin

This comment has been minimized.

Show comment
Hide comment
@stephencorwin

stephencorwin Jul 29, 2018

@swalkinshaw @inbararan I ended up opting for option C and it seems to be working great! I am already storing only the klassUid on the Hero anyway in the database, which means that I can provide this property in addition to the embedded Klass as siblings.

What has been truly great though, is that with only a minor amount of effort, I implemented Apollo's @client directive. If I am doing this optimization anywhere in the client, I can write a client-side resolver which resolves this fragment of the data from a previously cache result. In this case, I have a MetadataProvider which queries all the Klasses ahead of time.

Below are the results of this optimization for a User with 15 Heroes:

Test Name Duration
Vanilla Embed 23918966 (~0.023)
@client directive 1075378 (~0.001)

embedded
with-client

I think the thing I like most about this approach is that the API still provides both the Klass and User queries separately and any consumer can utilize it as they would expect (embed approach) while the client can keep the same structure and pivot with only adding the @client directive.

Note: obviously, this is a small contrived example, but I believe the more Heroes requested, the more benefit you gain from this optimization.

stephencorwin commented Jul 29, 2018

@swalkinshaw @inbararan I ended up opting for option C and it seems to be working great! I am already storing only the klassUid on the Hero anyway in the database, which means that I can provide this property in addition to the embedded Klass as siblings.

What has been truly great though, is that with only a minor amount of effort, I implemented Apollo's @client directive. If I am doing this optimization anywhere in the client, I can write a client-side resolver which resolves this fragment of the data from a previously cache result. In this case, I have a MetadataProvider which queries all the Klasses ahead of time.

Below are the results of this optimization for a User with 15 Heroes:

Test Name Duration
Vanilla Embed 23918966 (~0.023)
@client directive 1075378 (~0.001)

embedded
with-client

I think the thing I like most about this approach is that the API still provides both the Klass and User queries separately and any consumer can utilize it as they would expect (embed approach) while the client can keep the same structure and pivot with only adding the @client directive.

Note: obviously, this is a small contrived example, but I believe the more Heroes requested, the more benefit you gain from this optimization.

@smolinari

This comment has been minimized.

Show comment
Hide comment
@smolinari

smolinari Aug 4, 2018

If business decides it needs a new field, does a developer have to go in and program it...everywhere? What I am asking is, for every change in the business, does a developer need to dig into the schema? Couldn't all this be automated in some way?

Scott

smolinari commented Aug 4, 2018

If business decides it needs a new field, does a developer have to go in and program it...everywhere? What I am asking is, for every change in the business, does a developer need to dig into the schema? Couldn't all this be automated in some way?

Scott

@willcosgrove

This comment has been minimized.

Show comment
Hide comment
@willcosgrove

willcosgrove Aug 4, 2018

I'm not sure I understand your question @smolinari. You add a field to a type definition. That field then becomes available everywhere that type is being returned. So adding a field only requires adding it in one place.

willcosgrove commented Aug 4, 2018

I'm not sure I understand your question @smolinari. You add a field to a type definition. That field then becomes available everywhere that type is being returned. So adding a field only requires adding it in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment