Life Inside an IDE

GraphQL Schema Design Best Practices

Thoughts from 19th December 2019

Alternative Title: I read tons of GraphQL blog articles and these are my notes.

Build your schema based on existing requirements and your domain model

Don't try to build a one-size-fits-all schema

# Do!
userById(id: ID!): User!
userByName(name: String!): User!

# Don't!
user(id: ID, name: String): User!
# Do!
union Shipping = Pickup | Mail

# Don't!
type Shipping {
  isPickup: Boolean!
}

Use consistent naming conventions

enum Region {
  EUROPE
  NORTH_AMERICA
}

type MarketingCampaignConnection {
  edges: [MarketingCampaignEdge!]!
}

type MarketingCampaignEdge {
  node: MarketingCampaign
}

type MarketingCampaign {
  trackingUrl: String!
  region: Region!
}

type Query {
  marketingCampaigns(filter: MarketingCampaignFilter): MarketingCampaignConnection!
}

type Mutation {
  deleteMarketingCampaign(input: DeleteMarketingCampaignInput!): DeleteMarketingCampaignPayload!
}

type Subscription {
  deleteMarketingCampaignEvent: DeleteMarketingCampaignPayload!
}

Use Object types instead of simple types whenever possible

# Do!
type Customer {
  location: {
    city: String!
    zipCode: String!
  }
}

# Don't!
type Customer {
  locationCity: String!
  locationZipCode: String!
}
# Do!
type Product {
  image: {
    url (width: Int!, height: Int!): String!
    title: String!
  }
}

# Don't!
type Product {
  image: String!
}

Nest your types, do not reference their IDs

# Do!
type Book {
  author: Author!
}

# Don't!
type Book {
  authorId: ID!
}

Use an input object type for mutations

# Do!
type UpdateAuthorInput {
  author: {
    id: ID!
    firstName: String!
    lastName: String!
  }
}

type Mutation {
  updateAuthor (input: UpdateAuthorInput!): UpdateAuthorPayload!
}

# Don't!
type Mutation {
  updateAuthor (id: ID!, firstName: String!, lastName: String!): UpdateAuthorPayload!
}

Return affected objects as payloads for mutations

# Do!
type UpdateAuthorPayload {
  author: Author!
}

type DeleteAuthorPayload {
  id: ID!
}

type Mutation {
  updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload!
}

# Don't!
type Mutation {
  updateAuthor(input: UpdateAuthorInput!): ID!
}

Don't forget about computed fields

Use connections for pagination, and use pagination for everything that is a list

type Product {
  recommendedProducts (first: Int, last: Int, after: String, before: String): ProductConnection!
}

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

type ProductEdge {
  cursor: String!
  node: Product!

  # Optional: Additional data for the relationship
  boughtTogetherPercentage: Float!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# Usage:
product (id: 1) {
  title
  recommendedProducts (first: 5, after: "4e025") {
    edges {
      cursor
      boughtTogetherPercentage
      node {
        title
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor

      # Additional optional fields you could implement:
      pageCount
      hasNextPages (amount: Integer!)
      hasPreviousPages (amount: Integer!)
    }
  }
}

Consider adding filter & sort to connections

products(filter: "createdAt < 2019", sort: {field: "createdAt", direction: "ASC"}, first: 5) {
  edges {
    node {
      title
    }
  }
}

Provide top level queries for "get from ID" and for "get from filters"

product(id: ID!): Product!
products(filter: FilterString): ProductConnection!

(Optional) Global object identification