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:
- What data can be requested
- What operations can be performed
- What format the responses will take
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:
typekeyword defines an object type- Field names are followed by their types
- The
!exclamation mark indicates a non-nullable field - Square brackets
[]represent a list of the specified type - Comments use the
#symbol
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.
Relationships between types can be:
- One-to-one: A user has one profile
- One-to-many: A user has many posts
- Many-to-many: Users can like many posts, posts can be liked by many users
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:
- Cannot include fields that are object types
- Can only reference other input types, not object types
- Commonly used to group related arguments
- Help with versioning and schema evolution
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:
- Self-documenting—clients know exactly what values are allowed
- Type-safe—prevents invalid values from being provided
- Improves IDE auto-completion and validation
- Makes the API more intuitive and predictable
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:
- Query fields on multiple types uniformly
- Enforce consistent fields across types
- Use polymorphism in your schema
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!
}
- Query: For data fetching operations (required)
- Mutation: For operations that change data (optional)
- Subscription: For real-time updates via WebSockets (optional)
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:
- Access control and authorization
- Field-level validation
- Deprecating fields or enum values
- Adding caching behavior
- Custom formatting or transformations
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
- Evolve, don't version: Add new fields rather than changing existing ones
- Design for the consumer: Organize types based on how they'll be used, not your database structure
- Use descriptive names: Avoid abbreviations and be consistent in naming conventions
- Keep mutations focused: Each mutation should do one thing well
- Use pagination: For lists that could return many items
- Consider nullability carefully: Only mark fields as non-nullable when they're truly required
- Provide meaningful errors: Error messages should guide the client on how to fix issues
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:
- Restaurants have a name, address, cuisine type, and operating hours
- Menus have categories (appetizers, main courses, desserts, drinks)
- Menu items have a name, description, price, and dietary information
- Customers can create orders with multiple items
- Orders have a status (pending, preparing, ready, delivered)
- Customers can leave reviews for restaurants
Your schema should include:
- All necessary types (Restaurant, MenuItem, Order, etc.)
- Query operations (getRestaurant, searchMenuItems, etc.)
- Mutation operations (createOrder, updateOrderStatus, etc.)
- At least one enum type
- At least one input type
- Proper documentation
Bonus: Add authentication and authorization to your schema
Conclusion and Key Takeaways
- GraphQL schemas provide a contract between clients and servers
- The Schema Definition Language (SDL) offers a clear way to define types
- Type systems include scalar, object, input, enum, interface, and union types
- Relationships between types form the foundation of the data graph
- Entry points (Query, Mutation, Subscription) define available operations
- Directives add metadata and behavior to schema elements
- Well-designed schemas are self-documenting and evolve gracefully
In the next lecture, we'll explore queries, mutations, and resolvers—the mechanisms that make your schema functional.