Schema Definition and Types

Module 26: Advanced Backend & API Development

Introduction to GraphQL Schemas

A GraphQL schema is the blueprint of your GraphQL API. It defines what queries, mutations, and subscriptions are available, as well as the structure of the data that can be requested or modified.

Think of a GraphQL schema as the contract between your client and server. It explicitly declares:

Analogy: Blueprint for a Building

A GraphQL schema is like a detailed blueprint for a building:

  • The blueprint shows what rooms exist and how they connect (like types and their relationships)
  • It defines doorways and access points (like query entry points)
  • It specifies utility connections and modifications (like mutations)
  • Building inspectors can verify compliance against the blueprint (like type checking)
  • Renovations require updating the blueprint first (like schema evolution)

Schema Definition Language (SDL)

GraphQL schemas are typically written using the Schema Definition Language (SDL), which provides a human-readable way to define types and their relationships.


# A basic schema defining a blog
type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
  createdAt: String!
}
      

Key aspects of the SDL syntax:

Scalar Types

GraphQL comes with built-in scalar types that represent primitive values:

Scalar Type Description Example Values
Int Signed 32‐bit integer 1, 42, -7
Float Signed double-precision floating-point value 3.14159, -2.5, 6.02e23
String UTF‐8 character sequence "Hello", "GraphQL"
Boolean True or false true, false
ID Unique identifier, serialized as a string "123", "abc123"

Custom Scalar Types

You can also define custom scalar types for specialized data formats:


scalar Date
scalar Email
scalar URL
scalar JSON

type User {
  id: ID!
  email: Email!
  dateOfBirth: Date
  profileUrl: URL
  metadata: JSON
}
      

When creating custom scalar types, you need to define how the data is serialized, parsed, and validated in your GraphQL server implementation.

Object Types and Relationships

Object types are the most common types in GraphQL schemas. They represent entities with named fields.

graph LR User --> Posts Posts --> Comments User --> Comments class User,Posts,Comments type; classDef type fill:#f5f5f5,stroke:#333,stroke-width:1px;

Relationships between types can be:


type User {
  id: ID!
  name: String!
  profile: Profile!         # One-to-one
  posts: [Post!]!           # One-to-many
  likedPosts: [Post!]!      # Many-to-many
}

type Post {
  id: ID!
  title: String!
  author: User!             # Many-to-one
  likedBy: [User!]!         # Many-to-many
}

type Profile {
  id: ID!
  bio: String
  user: User!               # One-to-one
}
      

These relationships form the backbone of your data graph and determine how clients can traverse and access related data.

Input Types

Input types are special object types used for arguments in mutations and queries. They're defined using the input keyword:


input CreateUserInput {
  name: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  name: String
  email: String
  password: String
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}
      

Input types have some restrictions:

Real-World Example: E-commerce Order Creation


input AddressInput {
  street: String!
  city: String!
  state: String!
  zipCode: String!
  country: String!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
  options: JSON
}

input CreateOrderInput {
  customerId: ID!
  shippingAddress: AddressInput!
  billingAddress: AddressInput
  items: [OrderItemInput!]!
  couponCode: String
  paymentMethodId: ID!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
}
        

Enumeration Types

Enums represent a finite set of possible values. They're useful for fields that can only take one of a predefined set of options.


enum UserRole {
  ADMIN
  EDITOR
  VIEWER
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

type User {
  id: ID!
  name: String!
  role: UserRole!
}

type Order {
  id: ID!
  status: OrderStatus!
  items: [OrderItem!]!
}
      

Enums provide several benefits:

Interface and Union Types

Interfaces

Interfaces are abstract types that include a set of fields implementing types must include.


interface Node {
  id: ID!
}

interface Content {
  title: String!
  createdAt: String!
}

type Post implements Node & Content {
  id: ID!
  title: String!
  createdAt: String!
  body: String!
  author: User!
}

type Comment implements Node & Content {
  id: ID!
  title: String!
  createdAt: String!
  text: String!
  author: User!
}
      

Interfaces allow you to:

Unions

Unions represent a type that could be one of several object types, but don't specify common fields.


union SearchResult = User | Post | Comment

type Query {
  search(term: String!): [SearchResult!]!
}
      

When querying a union type, you need to use inline fragments to specify which fields to select from each possible type:


query {
  search(term: "GraphQL") {
    ... on User {
      name
      email
    }
    ... on Post {
      title
      body
    }
    ... on Comment {
      text
      author { name }
    }
  }
}
      

Schema Entry Points

Every GraphQL schema has entry points through special object types:


schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
  posts: [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

type Subscription {
  userCreated: User!
  postCreated: Post!
}
      
graph TD A[GraphQL Schema] --> B[Query] A --> C[Mutation] A --> D[Subscription] B --> B1[user] B --> B2[users] B --> B3[post] B --> B4[posts] C --> C1[createUser] C --> C2[updateUser] C --> C3[deleteUser] D --> D1[userCreated] D --> D2[postCreated]

Schema Directives

Directives provide a way to annotate schema elements with additional metadata or behavior.


directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

directive @auth(
  requires: Role = USER
) on FIELD_DEFINITION

enum Role {
  ADMIN
  USER
  GUEST
}

type User {
  id: ID!
  email: String!
  hashedPassword: String! @auth(requires: ADMIN)
  oldField: String @deprecated(reason: "Use newField instead")
  newField: String
}
      

Common use cases for directives:

GraphQL comes with built-in directives like @deprecated, but you can define custom directives for your specific needs.

Schema Documentation

One of GraphQL's strengths is self-documenting schemas. You can add descriptions to types and fields using string literals or comments:


"""
A user in the system.
Users can create posts and comments.
"""
type User {
  "Unique identifier for the user"
  id: ID!
  
  "Full name of the user"
  name: String!
  
  "Email address used for login"
  email: String!
  
  "Collection of posts authored by this user"
  posts: [Post!]!
}
      

These descriptions are available through introspection, enabling tools like GraphiQL and GraphQL Playground to provide interactive documentation.

Documentation Best Practices

  • Document all types, fields, arguments, and enums
  • Include examples where helpful
  • Explain business logic constraints
  • Indicate related fields or alternatives
  • Specify units for numeric values
  • Use consistent terminology throughout

Real-World Schema Example

Let's examine a more comprehensive schema for a book review application:


type Query {
  book(id: ID!): Book
  books(
    genre: Genre
    searchTerm: String
    first: Int
    skip: Int
  ): [Book!]!
  
  author(id: ID!): Author
  authors(first: Int, skip: Int): [Author!]!
  
  review(id: ID!): Review
  reviews(bookId: ID, userId: ID): [Review!]!
  
  me: User
}

type Mutation {
  signup(input: SignupInput!): AuthPayload!
  login(email: String!, password: String!): AuthPayload!
  
  createBook(input: CreateBookInput!): Book!
  updateBook(id: ID!, input: UpdateBookInput!): Book!
  deleteBook(id: ID!): Boolean!
  
  createReview(input: CreateReviewInput!): Review!
  updateReview(id: ID!, input: UpdateReviewInput!): Review!
  deleteReview(id: ID!): Boolean!
}

type AuthPayload {
  token: String!
  user: User!
}

type User {
  id: ID!
  name: String!
  email: String!
  reviews: [Review!]!
}

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

type Book {
  id: ID!
  title: String!
  summary: String!
  coverImage: String
  pageCount: Int
  publishedDate: String
  genre: Genre!
  author: Author!
  reviews: [Review!]!
  averageRating: Float
}

type Review {
  id: ID!
  rating: Int!
  text: String
  book: Book!
  user: User!
  createdAt: String!
  updatedAt: String!
}

enum Genre {
  FICTION
  NON_FICTION
  SCIENCE_FICTION
  FANTASY
  MYSTERY
  THRILLER
  ROMANCE
  BIOGRAPHY
  HISTORY
  SELF_HELP
}

input SignupInput {
  name: String!
  email: String!
  password: String!
}

input CreateBookInput {
  title: String!
  summary: String!
  coverImage: String
  pageCount: Int
  publishedDate: String
  genre: Genre!
  authorId: ID!
}

input UpdateBookInput {
  title: String
  summary: String
  coverImage: String
  pageCount: Int
  publishedDate: String
  genre: Genre
}

input CreateReviewInput {
  bookId: ID!
  rating: Int!
  text: String
}

input UpdateReviewInput {
  rating: Int
  text: String
}
      

This schema demonstrates many of the concepts we've covered, including types, relationships, enums, inputs, and queries/mutations.

Schema Design Best Practices

Analogy: Schema as a Living Organism

Think of your GraphQL schema as a living organism that evolves over time:

  • It grows by adding capabilities (new fields and types)
  • It adapts to changing requirements (new features)
  • It doesn't discard existing parts (backward compatibility)
  • It maintains its core identity while evolving (consistent design)

Practical Exercise

Design a GraphQL Schema for a Restaurant Ordering System

Your task is to design a GraphQL schema for a restaurant ordering system with the following requirements:

Your schema should include:

Bonus: Add authentication and authorization to your schema

Conclusion and Key Takeaways

In the next lecture, we'll explore queries, mutations, and resolvers—the mechanisms that make your schema functional.

Additional Resources