A good schema is half the API
https://slides.com/peco/graphql-schema-design
https://github.com/mjrio/mjr-full-stack-graphql
Build API's that are easier to evolve.
Build API's that are easier to reason about
It describes the key concepts of your product,
the relations between these concepts
and the core actions your system supports
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
...Which approach to take
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();// 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>();
}
}// 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
}// 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
}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
}
| 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 |
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
}
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
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
type Product {
id: ID!
name: String
unitPrice: Float
category: Category
}
type Order {
id: ID!
orderDate: String
customer: Customer
products: [Product]
}
Model your 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
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
# 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
}
type Product {
...
tags: [String]
tag: String @deprecated(reason: "Use `tags`.")
...
}
By using the @deprecated directive we can document our changes.
Can be a deal breaker
query {
products {
id
unitPrice
category {
id
name
description
}
}
}GraphQL will need to execute N+1 queries against the database. 🥵
“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.”
Use the power of GraphQL
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]
}
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
}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
| 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 |
type Order {
orderDate: Date
shippingTime: TimeStamp
}
type Customer {
contactEmail: URL
}
input CreateOrderInput {
orderDate: Date!
productID: String
}
type Mutation {
createOrder(input: CreateOrderInput): Order
}Advantages
type Query {
products(limit: Int, offset: Int, orderBy: String): [Product!]!
}query {
products(limit: 10, offset: 0, orderBy: "iPhone-") {
id
name
}
}
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!
}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
}
}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
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
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
query {
products {
unknown
}
}
{
"errors": {
"message": "Cannot query field 'unknown' on type 'Product'",
"locations": [
{ "line": 4, "column": 5}
]
}
}Result
{
"data": {
"products": null,
"status": "OK"
},
"errors": {
"message": "Cannot read property 'name' of undefined",
"locations": [{ "line": 4, "column": 5}],
"path": ["products"]
}
}query {
products {
...
}
status
}
Result
{
"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
type LoginResult {
error: String
errorCode: String
accessToken: String
email: String
}
type Mutation {
login(input: LoginInput): LoginResult
}type SuspendedError {
reason: String
}
type RemovedError {
removalDate: DateTime
}
union LoginResult = SuspendedError | RemovedError | User;
type Mutation {
login(input: LoginInput): LoginResult
}mutation($input: LoginInput) {
login(input: $input) {
__typename
...on User {
id
name
}
...on RemovedError {
removalDate
}
...on SuspendedError {
reason
}
}
}type Query {
user(id: ID!) User
product(id: ID!) Product
order(id: ID!) Order
category(id: ID!) Category
...
}
Typical queries to get single object
type Query {
node(id: ID!) Node
}
Can we build a query to retrieve any type?
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
}
query {
node(id: "dXNlci4xMjM0TY3ODkw") {
id {
... on User {
name
}
}
}
query
Or see you in an upcoming GraphQL training