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.