Link Search Menu Expand Document

Schema Design Principles

Remember that any information included in the schema (even in descriptions) should be considered public. These docs are also public.

Shopify’s GraphQL design tutorial makes good reading, though we do some things (e.g. pagination) very differently from their recommendations.

This article on nullability on Expedia’s blog is also well worth reading, as is a lot of the other content on that blog.

Formatting rules

  • All field names should be camelCased.
  • Enum values should be in ALL_CAPS_WITH_UNDERSCORES.
  • Types should be CamelCasedWithALeadingCapital, a.k.a. Pascal case.
  • Use descriptions where sensible. Avoid # comments as it’s not clear how they carry through to the introspected schema.

These rules are, where possible, enforced by schema linting.

Language

We have standardized the use of British English in this schema. This is mainly to avoid mixing color and colour in different places. When querying the schema, you may use aliases to get color, for example:

query {
  socialLoginProviders {
    code
    name
    loginUrl
    color:colour
  }
}

Field names

  • Timestamps should be named along the lines of createdAt, updatedAt etc.

Pagination and filters

Pagination is normally handled as in the example below.

type Bar {
   # some data fields
}

type Bars {
    bars: [Bar!]!
    total: Int!
    hasMore: Boolean!
}

input BarFilter {
    barId: ID
    servesFood: Boolean
    servesCaskBeer: Boolean
}

extend type City {
    bars(filter: BarFilter, offset: Int! = 0, limit: Int! = 10): Bars!
}

The offset parameter must be zero or more (negative values will error) and gives the index of the first record to be included, starting at zero. The limit parameter must also be zero or more, though a zero value will not be very useful as no records will be returned, and sets the maximum number of records to be returned. total gives the total number of records matching the filter. hasMore is true unless the last record returned is the final record available, or there are no records available at all. If offset is greater than total, no records will be returned but the system will not error.

Filters can be used to narrow down the records returned. All records returned will match the value provided in each non-null filter field, i.e. they are combined with an and rather than an or. To return records matching different filters, use field aliasing. A null filter parameter and a non-null filter with all fields null are equivalent and do not affect the results.

Sub-queries

Sometimes additional data may be needed from a root query in reaction to the presence of a particular GraphQL type (resolved from a union or interface). In this case, these types may have a field of type Query to allow further data to be queried.

An example of this would be with Widgets. The client will want to request Query.socialLinks ONLY when the SocialLinksWidget is present. Unfortunately, they couldn’t normally make that decision efficiently, as they would need to wait for the response to be returned and then make a second GraphQL query. We can solve that by giving the SocialLinksWidget a field of type Query. Then the client can just query as follows:

query HeaderSocialWidgets {
  header {
    widgets {
      ... on SocialLinksWidget {
        query {
          socialLinks {
            name
            url
          }
        }
      }
    }
  }
}

These sub-queries can be neatly pulled out as fragments and re-used if needed.

Potentially Failing Queries

If a query field makes a call to a downstream microservice, and that call may fail, but the field should be Non-null as far as the domain is concerned, we have the issue of a non-null field returning null, and thus causing the response to be inconsistent with the schema. Instead, if a query makes a call to a microservice or may fail, that field should be nullable to avoid this.

For example:

type Query {
  customer: Customer # Nullable because it comes from an Account Service
}

type Customer {
  email: String! # Non-null, because if the account service call for the customer succeeded, 
                 # we already have the email

  creditAccounts: [CreditAccount!] # Nullable as this causes a new call to Account Service which might fail
}

type CreditAccount {
  currency: Currency! # Non-null, because if the account service call for credit accounts succeeded
                      # We already have the currency
}

This does leak some of our internal architecture but, as covered in the Expedia blog post from the top of this page, this is realistically unavoidable.


2024 © The Hut.com Ltd.