GraphQL Schema Best Practices
Tags: graphql, webdev, javascript, node
Published: 2021-02-08 07:19:13

Are you creating or managing GraphQL schemas, but you don't know the best practice for creating maintainable schemas that your team or even you can follow to gain confidence in your project?

In this post, I will explain the best practice I have found by creating high maintainable GraphQL Schemas for the projects I'm working on.

### General Advice

When designing your GraphQL schema, one of the common mistakes is to replicate your database tables and fields.

Think in the GraphQL Schema DX (Developer Experience); you want to simplify the use of your schema for your team.

My advice here will be to create your Schema in the exact way that you need to consume them. Avoid adding fields that you will not need to use or overcomplicate to get the information for your UI.

Start with the minimum and expand them as you need. Taking too many decisions in advance could lead to feature problems and force you to refactor the code.

---

### Naming Fields

Naming fields are very important because they can impact future schema changes; being very explicit early on will make things easier in the future.

❌ Bad example:

```graphql
type Product {
	id: ID!
	category: String!
	image: String
	visits: Int!
}
```

✅ Good example:

```graphql
type Product {
	id: ID!
	image: Image
	stats: ProductStats! // or just visitsCount: Int!
}

type ProductStats {
	visitsCount: Int!
	purchasesCount: Int!
}

type Image {
	id: ID!
	url(size: ImageSize): String
	description: String
}

enum ImageSize {
	XS,
	SM,
	MD,
	LG,
	ORIGINAL
}
```

---
## Queries

Avoid writing queries named like `getProduct` or `getAllProducts` These queries will always return something. I consider starting with the word **get** is redundant and makes your schema difficult to read.

Don't force your queries to do more than one thing, instead create different queries that are self-explanatory.

❌ Bad query examples:

```graphql
type Query {
	product(id: ID, slug: String): Product
	getProduct(id: ID!): Product
}
```

✅ Good query examples:

```graphql
type Query {
	productById(id: ID!): Product
	productBySlug(slug: ID!): Product
}
```
---

## Pagination

Returning multiple results in GraphQL could end in a very complicated schema design, but you can opt for a simple solution depending on your project.

### **Offset Pagination**

Best for page based paginations, users can jump to a specific page. This option may be the right fit for most of the cases. If you are using an ORM it would be easy to implement.

But It has some disadvantages if your data change often; some results could be potentially skipped or returned duplicated.

```graphql
type Query {
	products(page: Int, limit: Int, filters: ProductFilters): ProductConnection!
}

type ProductConnection {
	nodes: [Product!]
	pageInfo: PageInfo!
	totalCount: Int!
}

type PageInfo {
	hasNextPage: Boolean!
	hasPreviousPage: Boolean!
	currentPage: Int!
	perPage: Int!
	lastPage: Int!
}
```

### **Cursor Pagination (Relay way)**

Best for infinite scroll or Load more results. (Facebook, Airbnb uses this style), In this solution, there is no concept of pages.

This one will scale well for large datasets, but it's the most complicated to implement. With this style, you can prevent the problems of offset pagination.

```graphql
type Query {
   products(first: Int, after: ID, last: Int, before: ID, 
 filters: ProductFilters): ProductConnection!
}

type ProductConnection {
   edges: ProductEdges!
   pageInfo: PageInfo!
   totalCount: Int!
}

type ProductEdges {
   nodes: Product!
   cursor: ID!
}

type PageInfo {
   hasNextPage: Boolean!
   hasPreviousPage: Boolean!
   startCursor: ID
   endCursor: ID
}
```

Related links:

- [https://relay.dev/graphql/connections.htm](https://relay.dev/graphql/connections.htm)
- [https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564](https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564)
- [https://www.apollographql.com/blog/understanding-pagination-rest-graphql-and-relay-b10f835549e7/](https://www.apollographql.com/blog/understanding-pagination-rest-graphql-and-relay-b10f835549e7/)

**Filters**

The convention I use here is uppercase for the filters and allowing always to pass an array of IDs to make the filters more flexible. Remember to keep only the filters that you need.

```graphql
type Query {
   products(..., filters: ProductFilters): ProductConnection!
}

input ProductFilters {
    PRODUCT_IDS: [ID]
    EXCLUDE_PRODUCTS_IDS: [ID]
    CATEGORY_IDS: [ID]
    ORDER_BY: ProductOrderBy
    SEARCH: String
}

enum ProductOrderBy {
   CREATED_AT_ASC
   CREATED_AT_DESC
   RANKING_ASC
   RANKING_DESC
}
```
---

## Mutations

We can summarise the mutation naming conventions into 5 rules

1. Mutation are named as verbs `CreateProduct` , `UpdateProduct` , `DeleteProduct`
2. There must be a single argument input
3. The input type name is the capitalised mutation name with a `Input` postfix e.g `CreateProductInput` , `UpdateProductInput`
4. The returned value is a new custom type that can contain various fields.
5. The return type name is the capitalized mutation name with a `Payload` postfix e.g. `CreateProductPayload` , `UpdateProductPayload`

**CreateProduct**

```graphql
type Mutation {
   CreateProduct(input: CreateProductInput!): CreateProductPayload!
}

input CreateProductInput {
   name: String!
   categoryId: ID!
   description: String
}

type CreateProductPayload {
   product: Product!
}
```

**UpdateProduct**

```graphql
type Mutation {
   UpdateProduct(id: ID, input: UpdateProductInput!): UpdateProductPayload!
}

input UpdateProductInput {
   name: String
   description: String
   categoryId: ID
}

type UpdateProductPayload {
   product: Product!
}
```

**DeleteProduct**

```graphql
type Mutation {
   DeleteProduct(id: ID): DeleteProductPayload!
}

type DeleteProductPayload {
   isDeleted: Boolean!
}
```

---
## Conclusions

These best practices are what worked for me, and I hope they are useful for you.

Just remember to make your schema self-explanatory, even if it requires you to add more queries or mutations.