Codementor Events

Graphql Schema Design

Published Apr 21, 2019

logo.png

Wow, has Graphql picked up a lot of steam over the last couple of years. And not without good reason, it allows you to execute some pretty cool data management features. Namely, the best feature in my opinion, is the ability to query data, line by line vs. REST, here take the whole block and filter out what you don't need. Which as your data needs expand, the more taxing REST can become. Graphql is not without its drawbacks though, namely, it's written in it's own language (yay for learning a whole new language!), and again my opinion, is heavy on the repetition, light on the utility it provides to combat that repetition. So, your forced to learn a new language to really take advantage of some of the specialized utilities (graphqli for instance), and you're going to be repeating yourself a lot. No loops or arrays, and no "class" structure to help with reuseabilitiy. It does have it's own class structure, but it's only helpful in type definition of similar fields, and it can also be a drawback to use, depending on the API you'll have to interface with to call data from it. Practically speaking, graphql looks great on paper, executing in the real world, can lead to huge time sinks and less than optimal queries, which can end up negating the benefits of gql.

Namely, do you have to integrate existing services that will remain live, with graphql? Expect to spend a lot of time adjusting queries to match CRUD operations, as they will often fail for any number of mix-match reasons, the cross environment support just isn't robust enough at this time. Lot of great strides, but still a lot of headaches. You'll get the best bang for the buck, if you're gql stack, can be brandnew, and standalone, or comboed with another very similar stack of gql. Mixing ORM's, backends, frames, and API's can really have a lot of unforseen impact into data operations. Let's go over some basics of schema design, these are by no means exhaustive, or the only solution, just solutions to common problems I've encoutered and had to overcome using gql.

Generally speaking, if you have to integrate with a live REST api, a lot of them and the framework their built on, do not support true batched calls. This is important, if your API submits singular calls (even though it sends a GET_MANY request, if it sends each request one at a time, but "batched" up), try to avoid assigning many_to_many relationships, or one_to_many relationships, you'll thank me later. So in gql:

Shop: {
  id: !ID @unique
    item: String
    location: Location @relation(name: "ShopLocation")
}
//vs
...
  location: [Location!]! @relation//

The main reason to avoid it, is you create non-nullable fields, which are very difficult for standard REST API routes to adequately deal with, even when wrapped in GQL clients, or provider helpers. And the change between mutation, update, list (query), are different, the only drawback to this, is calls for update or upsert, require an additional id, that is located outside of data pool. Going from

provider(UPDATE, 'Resource', { data: values })
//to
provider(UPDATE, 'Resource', { id: values.id, data: values })

The id cannot just be lumped in. That is also without reference. Connecting references requires getting the id, then showing that id, in relation to the resource id, you are connecting it to. Usually results in something like

provider(UPDATE, 'Resource', { id: values.id, data: { id: values.id, reference: { id: values.reference.id }}})

And yes I find, most often I have to reference the id twice, additionally, some update methods require the original values.

provider(UPDATE, 'Resource', { id: values.id, previousData: values, data: {id: values.id, ...values}})

back to the schema. It can be tempting to create types that unify common data, rather than be repetive. Excercise caution here! Again, if your API doesn't support batch calls, or if your API is particularly week with deep nested references, or references in general, limit this type of grouping to types that are the lowest on the food chain, i.e. types that are building blocks, and do not posses upwards and downwards references. For instance, in a shop, the shop is a bad idea to do this with, however, at the singular item level, since it should only reference items to it, and other things should not rely on those references, it would be ok there. Such as:

Shop {
  id: ID! @unique
    store: String
    location: Location @relation
    lineItems: LineItem
}
LineItem {
  id: ID! @unique
    quantity: INT
    options: [Option!]! @relation
    items: [Item!]! @relation
}
Option {
  id: ID! @unique
    product: Product @relation(name: 'optionway')
    variant: String
    inStock: Boolean
}
Item {
  id: ID! @unique
    product: Product @relation(name: 'oneway')
    inStock: Boolean
}
Product {
  id: ID! @unique
    sku: String!
    color: String
    features: String
    price: Float!
    ages: String
    warranty: String
}

Since the Item and Option are base units, and do not break any further down, using the product relation is fine, and you notice, we minimize the one_to_many areas, by utilizing a lineItem container, rather than creating that directly within the shop. You can see though, it is rather verbose. But again, if the API doesn't support true batched calls, this is best, because you can communicate 1 record at a time, which is what the API will expect. If your API has very limited reference
support, you would want to absorb the product into item, and repeat it, within option type as well. Little things, like this in your schema, can save hours off; one off querys or mutations, and the re-writes + error hunting.

Another great tool, after schema creation, is to utilize an introspection in your query building for your provider. The trade off, is additional load time intially. But another useful method can be applied directly during the introspection. Namely, fragmenting and pushing the fragmented query, along with the returned introspection back through a query builder function, and greatly increasing the functionality of LIST calls. GQL is great with this, at the client generation try:

import querybuilderFactory from 'Provider'
import introspection from 'graphql'
import gql from 'graphql-tag'
import get from 'lodash/get'

const getManyThing = {
Shop: {
['GET_LIST']:  gql`
  fragment shoplist on Shop {
    	id
        store
        location {
        	id
            address
        }
        lineItem {
        	id
            quantity
            option {
            	id
                variant
                product {
                	id
                    sku
                    color
                    warranty
                }
          }
          item {
          	id
            product {
            	id
                sku
                color
                warranty
            }
         }
       }
     }
    `
  }
}

const customQuery = introspection => ({type,resource,param}) => (
  const frag = get(getManyThing, `${resource}.${type}`)
    return buildqueryFactory(introspection, type, resource, param, frag)
)

and thats the general idea, in including queries, that leverage fragments, which are a great offset to API's that struggle with references or batches, since the parsed return, will mostly be accessable using 'record.shop.lineItem' type notation, with only one area within lineItem to deal with two arrays, the rest are objects.

Hopefully this brief overview, gives you a few ideas to help shave some time off your integration efforts utilizing graphql. And highlights a few areas to look out for as you migrate!

Discover and read more posts from Robert Nicely
get started
post commentsBe the first to share your opinion
Show more replies