This article is a part of free GraphQL Language course. To fully understand the content you need to be familiar with basic GraphQL concepts like SDL language, GraphQL document syntax or GraphQL object types and scalars. If you do not have this knowledge, you can take a look at our older articles on our older articles on atheros.ai.
Introduction
In this article we will go through various ways and basic patterns to help design schema in the best possible way. This is definitely not a complete guide; this is just a short list of best practices and instructions how to avoid the biggest pitfalls that regularly come up. For more advanced info on designing your GraphQL schemas check out our free GraphQL language course. It is useful to approach schema design by making a check-list of what your schema should provide to guide you through the design process.
Using input object type for mutations
It is extremely important to use just one variable for your mutations and use the input object type to simplify the structure of your GraphQL documents. In our schema the example is our createPlanet mutation. In SDL this is written as follows:
type Planet implements Node {id: ID!createdAt: DateTime!updatedAt: DateTimename: Stringdescription: StringplanetType: PlanetTypeEnum}input CreatePlanetInput {name: String!galaxyId: ID!description: String}type Mutation {createPlanet(input: CreatePlanetInput!): Planet!}
We can see that we only have one argument input - this is typed as an input object type. We are applying a non-null modifier to ensure that the input payload cannot be null. To get more details on this pattern including why we should use it and how to implement it, you can follow this article on input object types and this one on GraphQL lists and Non Null modifiers.
Returning affected objects as a result of mutations
When working with mutations it is considered good design to return mutated records as a result of the mutation. This allows us to update the state on the frontend accordingly and keep things consistent. To illustrate the concept let’s look at the following mutation:
type Mutation {createPlanet(input: CreatePlanetInput!): Planet!updatePlanet(input: UpdatePlanetInput!): Planet!}
We can see that we return Planet! as a result of this mutation. This is then used by the cache and we can use it to update the store. In the createPlanet mutation we append the object to the list of planets. This normally needs to be done in the code. In the second example for updatePlanet, however, we can update the Planet automatically by its id.
Using paginated lists by default
Paginated results are really important for security reasons and for ability to limit amount of records we would like to retrieve from the server. It is a good practice to structure paginated results as follows:
type PageInfo {endCursor: StringhasNextPage: Boolean!hasPreviousPage: Boolean!startCursor: String}type ConstellationConnection {nodes: [Constellation]pageInfo: PageInfo!totalCount: Int!}
This pagination is based on the so-called cursor based pagination. Whenever you fetch lists, I suggest you use paginated lists by default. You will avoid breaking changes of the schema in the future and it is almost always a much more scalable solution. You can also easily implement amount limiting and protect yourself against resource exhaustion attacks, where someone can query an overwhelming number of records from your database at once.
Nesting your objects in queries
When building GraphQL schemas I often see faulty patterns which affect caching and hinder the efficiency of GraphQL queries on the client. If we rewrite our planet type with this bad practice we would write something like this:
type Planet implements Node {id: ID!createdAt: DateTime!updatedAt: DateTimename: Stringdescription: StringplanetType: PlanetTypeEnumgalaxyId: ID!}
The problem here is the galaxyId. For the cache on the frontend there will be inconsistencies. You need to think about the schema, reuse the types, and substitute these ids with the proper type. If you want to query for the planet’s constellation you need to do two queries. First call the planet with the galaxyId, and then pass the galaxyId to an additional query. This is really inconvenient and does not maximize the power of GraphQL. In GraphQL it is much better to nest the output types. This way we can call everything with one single request and also perform caching and batching with data loader.
type Planet implements Node {id: ID!createdAt: DateTime!updatedAt: DateTimename: Stringdescription: StringplanetType: PlanetTypeEnumgalaxy: Galaxy!}
When we query for the planet and we want to include its constellation we can just call this GraphQL query:
query getPlanets {planets {nodes {idnameconstellation {idnamecreatedAt}}}}
Elevating schema design with interfaces
The interfaces and unions are powerful tools for abstracting different concepts, reducing complexity and simplifying our schema. The best example for interfaces is the Node Interface. The Node interface is enforced by Relay.js, but I would recommend implementing it to other schemas as well. Even if they do not use Relay.js on the frontend, it will still help to reduce complexity. To gain a more in-depth understanding of interfaces you can check out this article or our GraphQL language course.
Think about future schema changes
When implementing your initial schema, try to think about all possible future changes. In general just invest in the schema. Think about where you can expect to add additional fields in the future, and add output object types and input object types there so that you will not have to introduce breaking changes later on. If you are building a more complex app, just do not use GraphQL schema generators blindly. GraphQL schema generators are definitely useful in certain cases like prototyping or for instant high performance APIs from the database, but in general I would suggest to think about the schema design and tailor it to your frontend needs.
Use consistent naming in your schema
It's really important to keep naming conventions consistent not just in GraphQL schema design. It is common good practice to use camelCase for your fields and pascalCase for the names of types. I find also useful to name for example input types like this:
(action)(type)Input
(action) is Create, Update or Delete and (type) is the name of the type or entity, which is being updated. We also need to ensure that some common patterns like pagination are named in the same manner. We use the PageInfo type, and connection for the paginated list with nodes. Another good practice for enums is to use all capital letters for ENUM values, as they are basically constants.
I hope that you liked this short list of best practices for GraphQL schema design. Feel free to ask any question and feedback on this article or GraphQL Mastery in general to david@atheros.ai.