All articlesArchitecture

GraphQL Schema Design 101

Learn proven best practices and key principles for designing robust GraphQL Schemas.

Petar IvanovPetar Ivanov
6 min read
On this page

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.

GraphQL
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.

TypeScript
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.

TypeScript
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.

GraphQL
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.

GraphQL
type Query {
  #<strong> ⛔ Bad</strong>
  products(first: Int!, <strong>orderBy: ProductsOrdering</strong>)

  #<strong> ✅ Good</strong>
  products(first: Int!, <strong>orderBy: ProductsOrdering = CREATED_AT</strong>)
}

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).

Text
#<strong> ⛔ Bad</strong>
userInvite

#<strong> ✅ Good</strong>
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.

TypeScript
#<strong> ⛔ Bad</strong>
<strong>updatePost</strong>(input: {..., <strong>archived: true }</strong>) {
  ... 
}

#<strong> ✅ Good</strong>
<strong>archivePost</strong>(input: { <strong>postID: "abc"</strong> }) {
  ... 
}

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.

GraphQL
type Mutation {
  # <strong>⛔ Bad</strong>
  createProduct(name: String!): Product

  # <strong>✅ Good</strong>
  createProduct(<strong>input: CreateProductInput!</strong>): <strong>CreateProductPayload</strong>
}

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.

GraphQL
type User {
  # <strong>Prefer nullable associations, especially if backed by external services</strong>
  profilePicture: ProfilePicture

  # <strong>Scalars are usually safer</strong>
  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.

GraphQL
type Query {
  # <strong>⛔ Bad</strong>
  userByNameOrID(<strong>id: ID</strong>, <strong>name: String</strong>): User

  # <strong>✅ Good</strong>
  userByName(<strong>name: String!</strong>): User
  userByID(<strong>id: ID!</strong>): 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.

JSON
{
  "errors": [
    {
      "message": "The current user does not have permissions for: ...",
      "<strong>extensions</strong>": { 
        <strong>"code": "UNAUTHORIZED"</strong>
      }
    }
  ]
}

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.

JSON
{
  "errors": [
    {
      "message": "Invalid input arguments.",
      <strong>"extensions": {</strong> 
        "code": "BAD_USER_INPUT",
        <strong>"errors": [
          { "field": "email", "message": "Email already registered" }
        ]</strong>
      <strong>}</strong>
    }
  ]
}

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

Related articles

Whenever you’re ready, here’s how I can help you:

  1. 1.

    The Conscious React: React architecture, design & clean code — 100+ production tips across 6 chapters, updated for React 19, plus 4 companion repos you can clone and run.

  2. 2.

    The Conscious Node: Node.js architecture, design & clean code — 157 production tips across 10 chapters, from module boundaries to the transactional outbox and zero-downtime deploys.

  3. 3.

    The JavaScript Architect Bundle: Both books + all React companion repos + CLAUDE.md rulesets + both playbooks. The complete path from developer to architect.

  4. 4.

    Free Resources: Architecture playbooks, cheat-sheets, and the JavaScript Architect Roadmap — practical guides for leveling up to senior.

The T-Shaped Dev

Join 30K+ engineers leveling up to architect

One practical tip on JavaScript, React, Node.js, and software architecture every week. No spam, unsubscribe anytime.

Petar Ivanov

Written by

Petar Ivanov

Software engineer, author, and speaker. I help JavaScript developers grow from Mid → Senior → Architect — production-grade React, Node.js, and AI systems.