GraphQL

Schemas And The Type System

SDL: The Schema Definition Language

Schemas are usually written in SDL, a compact declarative syntax. Here is the core of our music catalog:

type Artist {
  id: ID!
  slug: String!
  name: String!
  foundedYear: Int
  albums(first: Int = 10): [Album!]!
}

type Album {
  id: ID!
  title: String!
  releaseYear: Int!
  artist: Artist!
  tracks: [Track!]!
  averageRating: Float
}

type Query {
  artist(slug: String!): Artist
  album(id: ID!): Album
}

Each type block declares an object type; each line inside is a field with a type and optionally arguments. Fields returning object types (Album.artist: Artist!) are the edges of your graph.

Scalars, Lists, And Non-Null

Five scalar types are built in: Int, Float, String, Boolean, and ID. ID serializes as a string but signals "opaque identifier — do not parse or do math on this."

Two wrapping types compose with everything:

  • [Album] — a list of albums,
  • Album! — a non-nullable album.

Because wrappers nest, [Album!]! reads as "a list that is always present, whose elements are never null" — an empty list is fine, null is not, and no element may be null. All four combinations ([T], [T]!, [T!], [T!]!) mean different things; being deliberate about which you pick is a large part of schema quality. Nullability is also GraphQL's error-containment mechanism — the execution lesson shows how a null from a failed resolver propagates until it reaches a nullable field — so "non-null everywhere" is not automatically better. A good default: non-null for identity and intrinsic fields (id, title), nullable for anything that comes from a separate system that might be down (averageRating from a stats service).

Schemas can also declare custom scalars (scalar DateTime, scalar EmailAddress) when a value has validation and serialization rules the built-ins cannot express; the server supplies the parsing code, and clients see a documented primitive.

Enums

Enums restrict a value to a fixed set of named constants:

enum ReleaseKind {
  ALBUM
  EP
  SINGLE
  LIVE
}

Use an enum whenever a string field has a known closed set of values — the schema then documents the set, validation rejects typos, and adding a new value later is an explicit, visible schema change.

Interfaces

An interface names a set of fields that several types share, letting fields return "anything with these fields":

interface Playable {
  id: ID!
  title: String!
  durationSeconds: Int!
}

type Track implements Playable {
  id: ID!
  title: String!
  durationSeconds: Int!
  album: Album!
}

type PodcastEpisode implements Playable {
  id: ID!
  title: String!
  durationSeconds: Int!
  showName: String!
}

type Query {
  queue: [Playable!]!
}

Querying queue gives you the shared fields directly; type-specific fields (album, showName) require inline fragments (... on Track). Choose an interface when types are substitutable — code that treats them uniformly makes sense — not merely when they coincidentally share field names.

Unions

A union says "one of these types, which share no required fields":

union SearchResult = Artist | Album | Track

A search field returning [SearchResult!]! forces clients to handle each possibility with inline fragments. Interfaces say "these types are the same kind of thing"; unions say "this field can hand you several different kinds of thing." Unions also power the result-object error pattern covered in the error-handling lesson.

Input Types And The Root Types

Input object types (input PostReviewInput { ... }) were covered in the mutations lesson; recall they are the only object-like types allowed in argument position.

The schema's entry points are up to three special object types — Query, Mutation, Subscription. They are ordinary object types with one distinction: their fields are the operations clients can start from. If the schema declares no Mutation type, the API is read-only, and that is a legitimate design.

Documentation And Deprecation Live In The Schema

SDL supports doc strings and a built-in deprecation directive:

type Album {
  """Year of first commercial release, in the artist's local market."""
  releaseYear: Int!

  rating: Float @deprecated(reason: "Use averageRating, which excludes spam reviews.")
  averageRating: Float
}

SDL also has plain comments (# like this), and the distinction matters: # comments are for schema authors and vanish from introspection, while """doc strings""" are for schema consumers and travel with the schema. Because tooling reads doc strings through introspection, they are not comments — they are the API reference your consumers actually see. Write them for the client developer, including units, timezones, and edge-case behavior. @deprecated is how schemas evolve without versioning: the old field keeps working, tooling warns anyone still selecting it, and you remove it only after usage drains.

Schema-First Or Code-First

Teams either write SDL by hand and bind resolvers to it (schema-first), or define types in PHP/host-language code and generate SDL from that (code-first). Both are valid; what matters is that the SDL — however produced — is treated as the reviewed, versioned contract. The governance lesson later builds on that idea.

What To Check

Before moving on, make sure you can:

  • read and write object types with typed, argument-bearing fields in SDL
  • explain all four list/non-null combinations and pick deliberately between them
  • say when a custom scalar is warranted over String
  • choose among enum, interface, and union for a given modeling problem
  • explain what makes Query, Mutation, and Subscription special
  • use doc strings and @deprecated as consumer-facing schema features

What You Should Be Able To Do

After this lesson, you should be able to model a small domain end to end in SDL — entities, relationships, closed value sets, polymorphic fields, and entry points — with nullability chosen consciously and documentation written into the schema itself.