Graphql Schema Design
A good schema is half the API
https://slides.com/peco/graphql-schema-design
The slides
Demo Project
https://github.com/mjrio/mjr-full-stack-graphql
Graphql

Architecture

Graphql query

Why we need design
-
Build API's that are easier to evolve.
-
Build API's that are easier to reason about
Graphql should not be a interface to your database
Graphql should not match your existing REST resources
GraphQL should not be a 1:1 mapping of your UI
Graphql lets us model an interface to our domain.
-
It describes the key concepts of your product,
-
the relations between these concepts
-
and the core actions your system supports

Northwind Db Model
Northwind REST API
GET /api/orders
GET /api/orders/:id
GET /api/orders/:id/details
GET /api/products
GET /api/customers
GET /api/categories
GET /api/suppliers
POST /api/orders
POST /api/products
...Thinking in Graphs
With GraphQL, you model your business domain interface as a graph
Northwind Business Model

Schema or Code First
Which approach to take
Schema First (JS)
const typeDefs = `
type Query {
book(id: String): Book
}
type Book {
name: String
description: String
}
`;
const resolvers = {
Query: {
book: (root, args) => fetchBookById(args.id),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen();Schema First (C#)
// Book.cs
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
// Query.cs
public class Query {
public Task<Book> GetBookAsync(string id) {
// Omitted code for brevity
}
}
// Startup.cs
public class Startup {
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouting()
.AddGraphQLServer()
.AddDocumentFromString(@"
type Query {
book(id: String): Book
}
type Book {
title: String
author: String
}
")
.BindComplexType<Query>();
}
}Code First (C#)
// Query.cs
public class Query
{
public Task<Book> GetBookAsync(string id) {
// Omitted code for brevity
}
}
// QueryType.cs
public class QueryType : ObjectType<Query>
{
protected override void Configure(IObjectTypeDescriptor<Query> descriptor) {
descriptor.Field(f => f.GetBook(default)).Type<BookType>();
}
}
// Book.cs
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
// BookType.cs
public class BookType : ObjectType<Book>
{
protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
{
descriptor.Field(f => f.Title).Type<StringType>();
descriptor.Field(f => f.Author).Type<StringType>();
}
}
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddRouting()
.AddGraphQLServer()
.AddQueryType<QueryType>();
}
// Omitted code for brevity
}Code First (C#)
// QueryType.cs
public class QueryType : ObjectType<Query>
{
protected override void Configure(IObjectTypeDescriptor<Query> descriptor) {
descriptor.Field(f => f.GetBook(default)).Type<BookType>();
}
}
// BookType.cs
public class BookType : ObjectType<Book>
{
protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
{
descriptor.Field(f => f.Title).Type<StringType>();
descriptor.Field(f => f.Author).Type<StringType>();
}
}
type Query {
book(id: String): Book
}
type Book {
title: String
author: String
}Schema First
- A design language between teams
- Mostly results in a
better-designed API - Teams can work simultaneously
Code First
- Code is the single source of truth
- The API design will be influenced by the implementation.
- Backward-incompatible changes can slip in more easily
👍
Lets start a design
Northwind GraphQL API
Step 1 - Types
type Product {
id: ID!
name: String
unitPrice: Float
}
type Category {
id: ID!
name: String
description: String
}
type Customer {
id: ID!
companyName: String
}
type Supplier {
id: ID!
companyName: String
}
type Order {
id: ID!
orderDate: String
}
Scalar types
| Type | Desc |
|---|---|
| Int | A signed 32-bit integer |
| Float | A signed double precision floating point value |
| String | A signed double precision floating point value |
| Boolean | Represents true/false |
| ID | Unique identifier |
Step 2 - More Types
type Address {
street
city
region
postalCode
country
}
type Customer {
id: ID!
companyName: String
address: Address
}
type Supplier {
id: ID!
companyName: String
address: Address
}
type Order {
id: ID!
orderDate: String
shippingSddress: Address
}
Step 3 - Queries
type Query {
category(id: ID!): Category
categories: [Category!]!
product(id: ID!): Product
products: [Product!]!
customer(id: ID!): Customer
customers: [Customers!]!
}
Notice the arguments and required fields
Step 4 - Improve Queries
type Query {
category(id: ID!): Category
categories: [Category!]!
product(id: ID!): Product
products(limit: Int, offset: Int, orderBy: String): [Product!]!
customer(id: ID!): Customer
customers(limit: Int = 20, offset: Int = 0): [Customers!]!
}
Add paging & sorting when needed
Step 5 - Relations
type Product {
id: ID!
name: String
unitPrice: Float
category: Category
}
type Order {
id: ID!
orderDate: String
customer: Customer
products: [Product]
}
Model your relations
Step 6 - More Relations
type OrderDetail {
product: Product
unitPrice: Float
quantity: Int
discount: Float
}
type Order {
id: ID!
orderDate: String
customer: Customer
details: [OrderDetail]
}
Create sub types when needed
Step 7 - Mutations
input CreateOrderInput {
customerID: ID!
productID: ID!
unitPrice: Float!
quantity: Int!
discount: Float!
shippingAddress: Address
}
type Mutation {
addCategory(name: String!, description: String!): Category
addproduct(name: String!, unitPrice: Float!): Product
createOrder(input: CreateOrderInput!): Order
}
Notice the input type
Step 8 - Documentation
# Mutations
input CreateOrderInput {
"the price or the product on the moment of the order"
unitPrice: Float!
" the given discount on the product"
discount: Float!
}
type Mutation {
"""
Create the order, decrement the stock
and start the shipping process
"""
createOrder(input: CreateOrderInput!): Order
}
Step 9-10 - Evolve
type Product {
...
tags: [String]
tag: String @deprecated(reason: "Use `tags`.")
...
}
By using the @deprecated directive we can document our changes.
Performance
Can be a deal breaker
An example
query {
products {
id
unitPrice
category {
id
name
description
}
}
}GraphQL will need to execute N+1 queries against the database. 🥵
The N+1 problem is GraphQL’s archenemy, something to avoid at all costs
The DataLoader to the rescue 😀
“DataLoader is a generic utility to be used as part of your application’s data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching.”
Schema design patterns
Use the power of GraphQL
Use consistent naming
- Fields should be named in camelCase
-
Types & Enum should be named in PascalCase.
-
Use all capital letters for ENUM values, as they are basically constants
-
Type should have id field of type ID!
enum Language {
NL,
FR
}
type BlogPost {
id: ID!
name: String
}
type Query {
blogPosts: [BlogPost]
}
Avoid confusion
type Query {
# bad
product(id: ID, slug: String) Product
# good
productById(id: ID!) Product
productBySlug(slug: String!) Product
}
type Mutation {
# bad
updateCart(cart: Cart) Cart
# good
addProductsToCart(cartId: ID, productIds: [ID!]!) Cart
clearCart(cartId: ID) Cart
}Unique ID's
- Remember the client. His caching is based on the ID and type
type Code {
id: ID!
name: String!
}
type Customer {
id: ID!
name: string
languageCode: Code
countryCode: Code
}
{
"id": "ANUI23232",
"name": "MyCorp",
"languageCode": {
id: "1",
name: "Dutch"
},
"countryCode": {
"id": "1",
"name": "Belgium"
}
}Bad
Extend your Scalar types
| Scalar Type | Example |
|---|---|
| Date (ISO 8601) | 2020-02-01 |
| TimeStamp (ISO 8601) | 1994-11-05T13:15:30Z |
| name@no-reply.com | |
| CreditCard (Luhn Algorithm) |
4111 1111 1111 1111 |
| PhoneNumber (E.164 format) |
+44207183875044 |
| VAT number | BE1234567890 |
| IBAN | BE68 5390 0754 7034 |
Extend your Scalar types
type Order {
orderDate: Date
shippingTime: TimeStamp
}
type Customer {
contactEmail: URL
}
input CreateOrderInput {
orderDate: Date!
productID: String
}
type Mutation {
createOrder(input: CreateOrderInput): Order
}- Improves type definition
- Improves field correctness
- Adds input validation
Advantages
Pagination (offset based)
type Query {
products(limit: Int, offset: Int, orderBy: String): [Product!]!
}query {
products(limit: 10, offset: 0, orderBy: "iPhone-") {
id
name
}
}
Problem of offset based pagination

Pagination (cursor based)
type Query {
products(first: Int, after: String): ProductsConnection!
}
type ProductsConnection {
totalCount: Int!
edges: [ProductEdge!]!
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String!
endCursor: String!
}
type ProductEdge {
node: Product!
cursor: String!
}Mutation (not so good)
type Mutation {
addProduct(name: String!, unitPrice: Float!, tag: String): Product
}
mutation AddProductMutation($name: String!, $unitPrice: Float!, $tag: String ) {
addBlogPost(name: $name, unitPrice: $unitPrice, tag: $tag ) {
id
name
}
}- Lengthy arguments to mutations
- Result is not extendable
Mutation (improved)
input AddProductInput {
name: String!
unitPrice: Float!
tag: String
}
type AddProductPayload {
product: Product
status: String
}
type Mutation {
addProduct(input: AddProductInput): AddProductPayload!
}
Schema
mutation AddProductMutation($input: AddProductInput ) {
addProduct(input: $input ) {
product {
id
name
}
status
}
}Mutation (improved)
Mutation
REST API
- Token based
- Token verified on incoming request
-
Global Security
- 401 HTTP Error
- Authorization
- 403 HTTP Error
GRAPHQL API
- Token based
- Token verified on incoming request
-
Global Security
- 401 HTTP Error
- Authorization
- Defined by schema
- GraphQL error
Authentication / Authorization
Authorization
type Product {
name: String
price: Float
itemsInStock: Int @isAuthenticated
}
type Query {
products: [Prod!]!
}
type Mutation {
# add a new product
addProd(input: ProdInput): Product @hasRole(roles: ["admin"])
}by using GraphQL Directives
Authorization
type User {
name: String
email: String
roles: [String]
}
type Query {
viewer User! @isAuthenticated
# or
me User! @isAuthenticated
}
Best to provide query to retrieve current user
Error Handling - validation
query {
products {
unknown
}
}
{
"errors": {
"message": "Cannot query field 'unknown' on type 'Product'",
"locations": [
{ "line": 4, "column": 5}
]
}
}Result
Error Handling - resolver
{
"data": {
"products": null,
"status": "OK"
},
"errors": {
"message": "Cannot read property 'name' of undefined",
"locations": [{ "line": 4, "column": 5}],
"path": ["products"]
}
}query {
products {
...
}
status
}
Result
Error Handling - Business Logic
{
"data": {
"login": null
},
"errors": [{
"path": [ "login" ],
"locations": [ { "line": 2, "column": 3 } ],
"extensions": {
"message": "Account is suspended",
"type": 2
}
}]
}mutation login($input: LoginInput) {
login(input: $input) {
...
}
}Result
Error Handling - improved
type LoginResult {
error: String
errorCode: String
accessToken: String
email: String
}
type Mutation {
login(input: LoginInput): LoginResult
}- You need to query the error
- What if you need specific error props
Error Handling - more improved
type SuspendedError {
reason: String
}
type RemovedError {
removalDate: DateTime
}
union LoginResult = SuspendedError | RemovedError | User;
type Mutation {
login(input: LoginInput): LoginResult
}Error Handling - improved
mutation($input: LoginInput) {
login(input: $input) {
__typename
...on User {
id
name
}
...on RemovedError {
removalDate
}
...on SuspendedError {
reason
}
}
}Node Interface
type Query {
user(id: ID!) User
product(id: ID!) Product
order(id: ID!) Order
category(id: ID!) Category
...
}
Typical queries to get single object
Node Interface
type Query {
node(id: ID!) Node
}
Can we build a query to retrieve any type?
Node Interface
interface Node {
id: ID!
}
type User extends Node {
id: ID!
name: String
}
type Product extends Node {
id: ID!
title: String
}
type Query {
node(id: ID!) Node
}
- Convention to fetch an object
- Support client side caching
- Id must be globally unique:
Global ID
Requirement
Advantage
Node Interface
query {
node(id: "dXNlci4xMjM0TY3ODkw") {
id {
... on User {
name
}
}
}
query
Resources
Now start building some kickass GraphQL servers
Or see you in an upcoming GraphQL training
Graphql Schema Design
By Peter Cosemans
Graphql Schema Design
- 39