skip to content
Samuel Edwin's Website

How to design your first GraphQL schema in 10 minutes

/ 6 min read

Having a properly designed GraphQL schema determines whether you’ll enjoy using GraphQL or start to question your life choices.

This is the guide I wish to have when designing my first GraphQL schema.

Who this guide is for

Front end developers, back end developers, mobile developers, doesn’t matter.

If you’re implementing a GraphQL server or consuming a GraphQL API, this guide is for you.

Having a database design or object oriented design experience is preferred since the knowledge is transferable.

Prerequisites

Basic knowledge of GraphQL and its syntax.

If you’re totally new to GraphQL, here’s a quick guide for you to get started:

Aside from that, having a basic understanding of SQL and Object Oriented Programming will help but not required.

Step 0: Always design based on product requirements

The ultimate goal of our GraphQL server is eventually to support running the business logic.

It is crucial to model our schema based on the product requirements to make development runs smoothly.

So the very first step you need to do is: talk to the product manager and understand the product or business requirements.

Step 1: Define the types

A typical software implementation will always have entities of some sort.

  • In database design, entities are the tables.
  • In object oriented design, entities are the classes.
  • In GraphQL, entities are the types.

A good first step is to define the types used in the schema.

Here’s how a schema for a simple e-commerce app looks like:

type User {
id: ID!
username: String!
}
type Shop {
# this is mandatory, doesn't make sense for ID
# to be nullable
id: ID!
name: String!
description: String!
# this is optional
address: String
}
type Product {
id: ID!
name: String!
description: String!
price: Float!
stock: Int
}

Nullable or not?

Note that some fields end up with a ! which means that the field is mandatory or non-nullable.

In order to find out which types are nullable or non-nullable, we need to look at the requirements.

Let’s say in the example above, the product manager requested that the shop’s name and description are required, but the address field is optional.

Step 2: Define the queries and add missing relations

A query is an operation that retrieves data from the server.

It is the entry point of the GraphQL server to get the data that the client needs.

Think of it like a select statement in SQL, or a get method in OOP.

I’ll give some example of queries that are needed from the product perspective.

A user can see the detail of a product, its shop name and description

The query should look like this:

product(id: "1") {
id
name
description
price
stock
shop {
id
name
description
}
}

In order to support this operation, we need to add the missing pieces to our schema:

  • A query to get the product by id
  • A way to get the shop from the Product type.
schema.graphql
type Query {
product(id: ID!): Product
}
type Product {
id: ID!
name: String!
description: String!
price: Float!
stock: Int
shop: Shop!
}

Note that the product(id: ID!) query returns a nullable Product not Product!, because there’s no guarantee that the product will always exist.

As for the product’s shop field, we mark it as non-nullable because a product must always have a shop based on the feature requirements.

A user can see the detail of another user and their shop if they have one

This is the query that we want to support:

user(id: "1") {
id
username
shop {
id
name
description
}
}

Here are the missing pieces we need to add to our schema:

  • A query to get the User by id
  • A way to get the shop from the User type.
schema.graphql
type Query {
user(id: ID!): User
}
type User {
id: ID!
username: String!
shop: Shop
}

A user can see the detail of a shop and its owner

This is the query that we want to support:

shop(id: "1") {
id
name
owner {
id
username
}
}

And this are the things we need to define in our schema:

schema.graphql
type Query {
shop(id: ID!): Shop
}
type Shop {
id: ID!
name: String!
owner: User!
}

A user can search for products within a shop with some filters applied

This is the query that we want to support:

shop(id: "1") {
id
products(name: "Laptop", minPrice: 1000, maxPrice: 2000) {
id
name
price
}
}

To support this operation, we need to define the products field in the Shop type.

schema.graphql
type Shop {
id: ID!
# ... other fields
products(
name: String,
minPrice: Float,
maxPrice: Float
): [Product!]!
}

Note that the products field can receive filter parameters.

This is similar to OOP where objects can define their own methods.

class Shop {
getProducts(
name: String,
minPrice: Float,
maxPrice: Float
): Product[]
}

A user can search for products from any shop, with some filters applied. Show the shop name for each product.

Similar to the previous query, but we want to search for products from any shop.

searchProducts(name: "Laptop", minPrice: 1000, maxPrice: 2000) {
id
name
price
shop {
id
name
}
}

We just need to add a new query for this since we already have the shop field in the Product type previously.

schema.graphql
type Query {
searchProducts(
name: String,
minPrice: Float,
maxPrice: Float
): [Product!]!
}

And so on…

I hope you get the point from the examples above.

After defining the types, add the queries you want to support and the missing relations required by the queries.

Step 3: Define the mutations

Mutations are operations that modify data on the server.

Think of it like an insert, update, or delete statement in SQL.

Some examples of mutations required in this project:

  • Sign up a new user
  • Create a new shop
  • Add a new product to a shop
  • Update a product’s stock
  • Purchase a product

Now let’s define the mutations we want to support in our schema.

schema.graphql
type Mutation {
# returns the user's access token
signUp(
username: String!,
password: String!
): User
createShop(
name: String!,
description: String!,
address: String
): Shop
addProductToShop(
name: String!,
description: String!,
price: Float!,
stock: Int
): Shop
updateProductStock(productId: ID!, stock: Int!): Product
purchaseProduct(productId: ID!, quantity: Int!): Product
}

Wrapping up

This is the final schema that we designed based on the requirements, combining the types, queries, and mutations above.

schema.graphql
type Mutation {
# returns the user's access token
signUp(username: String!, password: String!): String!
createShop(
name: String!,
description: String!,
address: String
): Shop!
addProductToShop(
name: String!,
description: String!,
price: Float!,
stock: Int
): Product!
updateProductStock(productId: ID!, stock: Int!): Product!
purchaseProduct(productId: ID!, quantity: Int!): Product!
}
type Query {
product(id: ID!): Product
user(id: ID!): User
shop(id: ID!): Shop
searchProducts(
name: String,
minPrice: Float,
maxPrice: Float
): [Product!]!
}
type User {
id: ID!
username: String!
shop: Shop
}
type Shop {
id: ID!
name: String!
description: String!
address: String
products(name: String, minPrice: Float, maxPrice: Float): [Product!]!
}
type Product {
id: ID!
name: String!
description: String!
price: Float!
stock: Int
shop: Shop!
}

The are a lot of details that I didn’t cover, such as pagination.

But this should give you a good starting point on designing your own GraphQL schema.

To summarize, the steps are:

  1. Define the types
  2. Define the queries and missing relations
  3. Define the mutations