GraphQL Schema Design 101
Learn proven best practices and key principles for designing robust GraphQL Schemas. (5 min)
GraphQL Schema is the contract between your server and your clients.
It defines what data your clients can request, how they will receive it, and how they can modify it.
Good schema design makes your API easy to use, predictable, and maintainable.
In the past, I’ve designed several production-ready GraphQL schemas that lasted in time.
I saw and learned what worked well and what didn’t.
In today’s article, I’ll discuss the key principles, rules, patterns, and design considerations that worked well for me in the past.
1. General Principles
1.1 YAGNI (You Aren’t Gonna Need It)
Keep your API minimal.
Expose fields and use cases only when needed.
Deprecate the fields that are not being used anymore.
Fewer fields means less surface are for bugs, simpler documentation, and easier client development.
1.2 Completeness
Keep your schema minimal, yet complete.
Clients should be able to achieve their use cases completely.
A complete schema saves clients from multiple APIs, requests, and keeps data fetching efficient.
1.3 Embed Domain Knowledge
Encode as mush as possible and as clearly as possible the domain knowledge in the schema.
Your names should match the domain knowledge and language.
Domain-Driven schemas are much easier to deal with, especially for newer developers to understand the business domain.
1.4 No Implementation Details
The API should focus on the Domain and Client use cases.
Implementation details must be hidden.
2. Naming Conventions
2.1 camelCase For Fields & Inputs
Field names and inputs should use camelCase.
Same as we do in JavaScript.
type User {
firstName: String!
lastName: String!
}
2.2 PascalCase For Types & Enums
Type names should use pascalCase.
This matches how classes are defined in JavaScript.
type OrderDetail { ... }
enum PaymentMethod { CREDIT_CARD, PAYPAL }
2.3 ALL_CAPS For Enum Values
Enum values should use ALL_CAPS, because they are similar to constants.
enum Role { ADMIN, EDITOR, VIEWER }
2.4 Use Create/Update/Delete In Mutations
Use Create/Update/Delete in mutations instead of they synonyms, unless you have a specific reason to use another verb.
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload
deletePost(input: DeletePostInput!): DeletePostPayload
}
2.5 Avoid Abbreviations
Prefer to be explicit about names.
3. Fields And Mutations
3.1 Default Values
Use default values to make an API more predictable and communicate intent.
type Query {
# ⛔ Bad
products(first: Int!, orderBy: ProductsOrdering)
# ✅ Good
products(first: Int!, orderBy: ProductsOrdering = CREATED_AT)
}
3.2 Explicit Side-Effects
Avoid implicit and surprise side-effects in fields and mutations.
The name should convey what it does at runtime.
3.3 Mutation Naming: verbEntity
Name mutations using verbEntity (and not entityVerb).
# ⛔ Bad
userInvite
# ✅ Good
inviteUser
3.4 Single-Purpose Fields
Do one thing, and do it well.
Prefer specific solutions rather than overly clever and generic fields.
3.5 Beyond CRUD
Think beyond CRUD.
Use action or behavior specific fields and mutations to help clients use your API.
# ⛔ Bad
updatePost(input: {..., archived: true }) {
...
}
# ✅ Good
archivePost(input: { postID: "abc" }) {
...
}
3.6 Input & Payload Types
Use Input and Payload types for mutations.
Use specific object types for mutation inputs and outputs.
Use *Input suffix for input and *Payload suffix for the result.
type Mutation {
# ⛔ Bad
createProduct(name: String!): Product
# ✅ Good
createProduct(input: CreateProductInput!): CreateProductPayload
}
3.7 Don’t Reuse Types Across Mutations
Don’t reuse input and payload types across multiple mutations.
This prevents accidental coupling.
3.8 Use Non-Null With Care
Use Non-Null mostly on scalar fields and use them with care on associations.
Non-Nullable mean that the field must never be null.
If it is null for any reason, GraphQL will return error without the data.
type User {
# Prefer nullable associations, especially if backed by external services
profilePicture: ProfilePicture
# Scalars are usually safer
name: String!
}
You can read more about nullability here.
3.9 Prefer Required Arguments
Prefer a strong schema rather than runtime checks.
Use more specific fields if needed.
type Query {
# ⛔ Bad
userByNameOrID(id: ID, name: String): User
# ✅ Good
userByName(name: String!): User
userByID(id: ID!): User
}
4. Error Handling
4.1 GraphQL Errors For Global Issues
Use GraphQL Errors for developer-facing global errors.
Example where to use such errors: internal (unexpected) server errors, timeouts, auth, schema validation, etc.
4.2 Error Extensions
Use GraphQL Error extensions to encode additional information.
Always provide a unique “code”.
This helps the client to decide what to do based on the “code”, instead of the error message.
{
"errors": [
{
"message": "The current user does not have permissions for: ...",
"extensions": {
"code": "UNAUTHORIZED"
}
}
]
}
4.3 User-Facing Errors in Schema
Use user errors, or errors-in-schema, for user-facing errors.
Use the schema instead of top-level errors for errors that are meant to be displayed to the user.
{
"errors": [
{
"message": "Invalid input arguments.",
"extensions": {
"code": "BAD_USER_INPUT",
"errors": [
{ "field": "email", "message": "Email already registered" }
]
}
}
]
}
5. Identification
5.1 Global IDs
Every type that implements the Node interface must define a unique and global ID.
Those IDs are opaque so that it is clear they are not intended to be decoded on the client.
5.2 Rich ID Encoding
Encode enough information in IDs for global fetching.
Prefer encoding more that not enough for evolution.
This might mean including the parent object id, the shard key, etc.
📌 TL;DR
General Principles
YAGNI (You Aren’t Gonna Need It)
Completeness
Embed Domain Knowledge
No Implementation Details
Naming Conventions
camelCase For Fields & Inputs
PascalCase For Types & Enums
ALL_CAPS For Enum Values
Use Create/Update/Delete In Mutations
Avoid Abbreviations
Fields And Mutations
Use Default Values
Prefer Explicit Side-Effects
Mutation Naming: verbEntity
Prefer Single-Purpose Fields
Think Beyond CRUD
Input & Payload Types
Don’t Reuse Types Across Mutations
Use Non-Null With Care
Prefer Required Arguments
Error Handling
GraphQL Errors For Global Issues
Error Extensions
User-Facing Errors in Schema
Identification
Use Global IDs
Rich ID Encoding
That's all for today. I hope this was helpful. ✌️
How I can help you further?
Learn how to build clean, maintainable, and testable React Applications! 🚀
👋 Let’s connect
You can find me on LinkedIn, Twitter(X), Bluesky, or Threads.
I share daily practical tips to level up your skills and become a better engineer.
Thank you for being a great supporter, reader, and for your help in growing to 26.8K+ subscribers this week 🙏
You can also hit the like ❤️ button at the bottom to help support me or share this with a friend. It helps me a lot! 🙏
3.3 description and examples are contradicting.
"Name mutations using verbEntity (and not entityVerb)."
But examples are the other way around.
```
# ⛔ Bad
inviteUser
# ✅ Good
userInvite
```
Nice explanation Petar