🔥 0
0
Lesson 2 of 6 20 min +200 XP

Schema, Types, Queries & Mutations

The schema is the heart of every GraphQL API. It defines what data exists, how it's structured, and what operations clients can perform.

Think of it as a contract: the schema guarantees what the API can do, and both client and server must honor it.

The Type System

GraphQL is strongly typed. Every piece of data has a defined type.

Scalar Types (Primitives)

GraphQL has 5 built-in scalar types:

Int

Signed 32-bit integer

Float

Double-precision decimal

String

UTF-8 text

Boolean

true or false

ID

Unique identifier

Object Types

Object types are the building blocks of your schema. They represent entities in your domain:

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

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  likes: Int!
  published: Boolean!
}

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

Nullability: The ! Modifier

NULLABLE (DEFAULT)

avatar: String

Can return null or a string value

NON-NULL (!)

name: String!

Must always return a string, never null

Lists

Square brackets denote arrays:

# Nullable list of nullable strings
tags: [String]        # null, [], ["a", null, "b"] are all valid

# Non-null list of nullable strings
tags: [String]!       # [] or ["a", null] valid, but not null itself

# Nullable list of non-null strings
tags: [String!]       # null or ["a", "b"] valid, but not ["a", null]

# Non-null list of non-null strings
tags: [String!]!      # Must be array, all items must be strings

Defining the Schema

A complete schema has Query (read), Mutation (write), and optionally Subscription (real-time) root types:

type Query {
  # Fetch a single user by ID
  user(id: ID!): User

  # Fetch all users, with optional pagination
  users(limit: Int, offset: Int): [User!]!

  # Fetch a single post
  post(id: ID!): Post

  # Search posts by title
  searchPosts(query: String!): [Post!]!
}

type Mutation {
  # Create a new user
  createUser(input: CreateUserInput!): User!

  # Update an existing user
  updateUser(id: ID!, input: UpdateUserInput!): User

  # Create a new post
  createPost(input: CreatePostInput!): Post!

  # Delete a post
  deletePost(id: ID!): Boolean!
}

# Input types for mutations
input CreateUserInput {
  name: String!
  email: String!
  avatar: String
}

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

input CreatePostInput {
  title: String!
  content: String!
  published: Boolean
}

Writing Queries

Queries read data. The client specifies exactly which fields to return:

# Simple query - get user's name and email
query GetUser {
  user(id: "123") {
    name
    email
  }
}

# Response
{
  "data": {
    "user": {
      "name": "Sarah Chen",
      "email": "sarah@example.com"
    }
  }
}

Nested Queries

Query related data in a single request:

query GetUserWithPosts {
  user(id: "123") {
    name
    posts {
      title
      likes
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

Query Variables

Use variables for dynamic values:

# Query definition
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# Variables (passed separately)
{
  "userId": "123"
}

In JavaScript:

const GET_USER = gql`
  query GetUser($userId: ID!) {
    user(id: $userId) {
      name
      email
    }
  }
`;

// Apollo Client
const { data } = await client.query({
  query: GET_USER,
  variables: { userId: "123" }
});

Writing Mutations

Mutations modify data. They can return the created/updated object:

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    content
    author {
      name
    }
  }
}

# Variables
{
  "input": {
    "title": "GraphQL is Awesome",
    "content": "Here's why...",
    "published": true
  }
}

Multiple Mutations

Mutations execute sequentially (unlike queries which can parallelize):

mutation CreateUserAndPost {
  # First: create user
  createUser(input: { name: "Alex", email: "alex@example.com" }) {
    id
    name
  }

  # Then: create post (runs after user is created)
  createPost(input: { title: "My First Post", content: "Hello!" }) {
    id
    title
  }
}

Enums and Interfaces

Enums

Define a fixed set of values:

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type Post {
  id: ID!
  title: String!
  status: PostStatus!
}

# Query usage
query {
  posts(status: PUBLISHED) {
    title
  }
}

Interfaces

Share fields across types:

interface Node {
  id: ID!
  createdAt: String!
}

type User implements Node {
  id: ID!
  createdAt: String!
  name: String!
  email: String!
}

type Post implements Node {
  id: ID!
  createdAt: String!
  title: String!
  content: String!
}

Schema Design Best Practices

Practice Example
Use Input types for mutations createUser(input: CreateUserInput!)
Return the created/updated object createPost(...): Post!
Use descriptive field names publishedAt not pub_at
Design for the client's needs Think about what views need

Complete Example Schema

# Scalars
scalar DateTime

# Enums
enum Role {
  USER
  ADMIN
  MODERATOR
}

# Types
type User {
  id: ID!
  name: String!
  email: String!
  role: Role!
  avatar: String
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  tags: [String!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# Inputs
input CreatePostInput {
  title: String!
  content: String!
  published: Boolean = false
  tags: [String!]
}

# Root types
type Query {
  me: User
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
  posts(published: Boolean): [Post!]!
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: CreatePostInput!): Post
  deletePost(id: ID!): Boolean!
}

Key Takeaways

  • Schema = Contract - Defines what data and operations exist
  • Strong typing - Every field has a defined type (scalars, objects, enums)
  • ! means non-null - Field must always return a value
  • Queries read, Mutations write - Clear separation of operations
  • Use Input types - Group mutation arguments into Input types

Next up: Resolvers - Learn how to implement the functions that fetch data for your schema.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is the purpose of the GraphQL schema?

2

Which is NOT a built-in scalar type in GraphQL?

3

What does the exclamation mark (!) mean in 'name: String!'?