Skip to content

Schema Design

You now know how to read a GraphQL schema. This module is about writing one well. A schema is not a side effect of your database — it is a public contract between your server and every client that will ever talk to it. Once a field ships, someone depends on it. Good schema design is the art of choosing types and fields that describe your domain so clearly that the contract stays honest as the product grows.

When you design a REST endpoint, the URL and the JSON it returns are loosely coupled — you can quietly add a field and nobody notices. A GraphQL schema is the opposite: it is explicit and strongly typed. Every type, every field, every argument is written down and validated. Clients build queries against that written contract, tools generate code from it, and your server promises to honour it.

That promise is the whole point. A well-designed schema lets a frontend team work against types they can see, lets tooling catch mistakes before runtime, and lets you reason about change. The cost is discipline: you have to decide, up front, what the world looks like.

Design around the domain, not the database

Section titled “Design around the domain, not the database”

The single biggest mistake new schema designers make is to mirror their database tables one-to-one. Tables are shaped for storage — foreign keys, join tables, snake_case columns, denormalized flags. Your schema should be shaped for the questions clients ask.

Consider a simple library application. The database might store a books table, an authors table, and a book_authors join table. A client does not care about the join table. It cares about a Book that has authors, and an Author that has books. The schema expresses that relationship directly:

type Book {
id: ID!
title: String!
authors: [Author!]!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}

The join table vanishes from the contract. The relationship that clients actually traverse — book to author and back — becomes a first-class field. Resolvers will later bridge the gap to storage, but the schema speaks the language of the domain.

flowchart LR
  Query["Query (entry point)"]
  Book["type Book"]
  Author["type Author"]
  Scalars["Scalars: ID, String"]
  Query -->|"book(id)"| Book
  Book -->|"authors"| Author
  Author -->|"books"| Book
  Book --> Scalars
  Author --> Scalars
A schema is a graph of domain types and the relationships between them.

This module walks through every building block you need to design a clean schema, lesson by lesson:

  1. Schema Design (you are here) — the contract mindset and designing around the domain.
  2. Types and Fields — object types, field arguments, descriptions, and how types link together.
  3. Scalars and Enums — built-in and custom scalars, and choosing an enum over a free-form string.
  4. Interfaces and Unions — modelling shared shapes and one-of alternatives, selected with fragments.
  5. Inputs and Nullability — input object types for arguments, the ! non-null marker, lists, and defaults.

By the end you will be able to take a fuzzy product idea and turn it into a schema that is precise, expressive, and pleasant to query.

The example below models the library above, wires up resolvers, and runs a query that crosses from a Book into its Author and back. Press Run to execute it with the real GraphQL engine.

JavaScript

Notice how the query reads like a sentence about the domain: a book, its authors, and the books those authors wrote. No join tables, no foreign keys — just the relationships a human cares about. That readability is what good schema design buys you.

Why is a GraphQL schema described as a contract?
What is the most common schema-design mistake this lesson warns against?
In the library example, what happened to the book_authors join table in the schema?