I will explain the best practice I have found by creating high maintainable GraphQL Schemas for the projects I'm working on.
updated: Dec 16, 2012
General Advice
When designing your GraphQL schema, one of the common mistakes is to replicate your database tables and fields.
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 Conventions
- Query Names:
camelCase
- Mutation names:
camelCase
- Field names:
camelCase
- Type names:
PascalCase
- Enum names:
PascalCase
- Enum values:
ALL_CAPS
- Input names:
PascalCase
camelCase Many GraphQL clients are written in JavaScript, Java, Kotlin, or Swift, all of which recommend camelCase for variable names.
PascalCase This matches how classes are defined in the languages mentioned above.
ALL_CAPS
Enum values should use ALL_CAPS , because they are similar to constants.
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:
type Product {
id: ID!
category: String!
image: String
visits: Int!
}
✅ Good example:
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
orgetAllProducts
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. e.g
productById
,productBySlug
- The return type name is the capitalized query name with a
Payload
postfix to make your queries more flexible. e.g.ProductByIdPayload
,ProductBySlugPayload
❌ Bad query examples:
type Query {
product(id: ID, slug: String): Product
getProduct(id: ID!): Product
getProducts: [Product]
}
✅ Good query examples:
type Query {
productById(id: ID!): ProductByIdPayload!
productBySlug(id: ID!): ProductBySlugPayload!
products: ProductPayload!
}
type ProductByIdPayload {
product: Product
}
type ProductBySlugPayload {
product: Product
}
type ProductPayload {
nodes: [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.
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.
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:
Filters
The convention I use here is uppercase camelCase for the filters and allowing always to pass an array of IDs to make the filters more flexible (only if needed). Remember to keep only the filters that you need.
input ProductFilters {
productIds: [ID]
excludeProductIds: [ID]
categoryIds: [ID]
orderBy: 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
- Mutation are named as verbs
createProduct
,updateProduct
,deleteProduct
- There must be a single argument input
- The input type name is the capitalised mutation name with a
Input
postfix e.gCreateProductInput
,UpdateProductInput
- The return type name is the capitalized mutation name with a
Payload
postfix to make your mutations more flexible, e.g.CreateProductPayload
,UpdateProductPayload
CreateProduct
type Mutation {
createProduct(input: CreateProductInput!): CreateProductPayload!
}
input CreateProductInput {
name: String!
categoryId: Int!
description: String
}
type CreateProductPayload {
product: Product!
}
UpdateProduct
type Mutation {
updateProduct(input: UpdateProductInput!): UpdateProductPayload!
}
input UpdateProductInput {
productId: Int!
name: String
description: String
categoryId: Int
}
type UpdateProductPayload {
product: Product!
}
DeleteProduct
type Mutation {
deleteProduct(input: DeleteProductInput!): DeleteProductPayload!
}
input DeleteProductInput {
productId: Int!
}
type DeleteProductPayload {
product: Product
}
Union Types usage
Interfaces, Custom Scalars, etc
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.
ref:
GitHub GraphQL Schema: https://docs.github.com/en/graphql/overview/explorer