GraphQL Basics
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand GraphQL concepts and principles
- - Install and use the Graphene library
- - Define GraphQL schemas
- - Create GraphQL types and resolvers
- - Write GraphQL queries
- - Handle mutations
- - Work with subscriptions
- - Understand GraphQL vs REST
- - Implement GraphQL with Flask
- - Handle errors in GraphQL
- - Use GraphQL introspection
- - Build GraphQL APIs
- - Apply GraphQL best practices
- - Debug GraphQL queries
Lesson 20.3: GraphQL Basics
Learning Objectives
By the end of this lesson, you will be able to:
- Understand GraphQL concepts and principles
- Install and use the Graphene library
- Define GraphQL schemas
- Create GraphQL types and resolvers
- Write GraphQL queries
- Handle mutations
- Work with subscriptions
- Understand GraphQL vs REST
- Implement GraphQL with Flask
- Handle errors in GraphQL
- Use GraphQL introspection
- Build GraphQL APIs
- Apply GraphQL best practices
- Debug GraphQL queries
Introduction to GraphQL
GraphQL is a query language for APIs and a runtime for executing those queries. It was developed by Facebook and provides a more efficient, powerful, and flexible alternative to REST.
Key Concepts:
- Query: Read data
- Mutation: Modify data
- Subscription: Real-time updates
- Schema: Defines the API structure
- Resolver: Functions that fetch data
- Type System: Strongly typed schema
Advantages over REST:
- Fetch exactly what you need
- Single endpoint
- Strongly typed
- Introspection
- Real-time subscriptions
GraphQL Concepts
Queries
# Query example
query {
user(id: "1") {
name
email
posts {
title
content
}
}
}
Mutations
# Mutation example
mutation {
createUser(name: "John", email: "john@example.com") {
id
name
email
}
}
Schema Definition
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!): Post!
}
Type System
# Scalar types
String
Int
Float
Boolean
ID
# Object types
User
Post
# Lists
[String]
[User!]!
# Non-nullable
String!
ID!
Graphene Library
Installation
# Install Graphene
pip install graphene
# Install Flask-GraphQL for Flask integration
pip install flask-graphql
# Install GraphQL Playground (optional, for testing)
pip install graphql-core
Basic Setup
import graphene
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, info):
return "Hello, GraphQL!"
schema = graphene.Schema(query=Query)
Flask Integration
from flask import Flask
from flask_graphql import GraphQLView
import graphene
app = Flask(__name__)
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, info):
return "Hello, GraphQL!"
schema = graphene.Schema(query=Query)
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)
)
if __name__ == '__main__':
app.run(debug=True)
Defining Types
Scalar Types
import graphene
class Query(graphene.ObjectType):
name = graphene.String()
age = graphene.Int()
height = graphene.Float()
is_active = graphene.Boolean()
user_id = graphene.ID()
def resolve_name(self, info):
return "John Doe"
def resolve_age(self, info):
return 30
def resolve_height(self, info):
return 5.9
def resolve_is_active(self, info):
return True
def resolve_user_id(self, info):
return "1"
Object Types
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Query(graphene.ObjectType):
user = graphene.Field(User)
def resolve_user(self, info):
return User(id="1", name="John Doe", email="john@example.com")
Lists
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
class Query(graphene.ObjectType):
users = graphene.List(User)
def resolve_users(self, info):
return [
User(id="1", name="Alice"),
User(id="2", name="Bob")
]
Non-Nullable Fields
import graphene
class User(graphene.ObjectType):
id = graphene.ID(required=True) # Non-nullable
name = graphene.String(required=True)
email = graphene.String() # Nullable
Resolvers
Basic Resolvers
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
# Fetch user from database
return User(id=id, name="John", email="john@example.com")
Resolvers with Arguments
import graphene
class Query(graphene.ObjectType):
user = graphene.Field(
User,
id=graphene.ID(required=True),
name=graphene.String()
)
def resolve_user(self, info, id, name=None):
# Fetch user by id or name
if name:
return User(id=id, name=name, email="email@example.com")
return User(id=id, name="John", email="john@example.com")
Nested Resolvers
import graphene
class Post(graphene.ObjectType):
id = graphene.ID()
title = graphene.String()
content = graphene.String()
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
posts = graphene.List(Post)
def resolve_posts(self, info):
# Fetch posts for this user
return [
Post(id="1", title="Post 1", content="Content 1"),
Post(id="2", title="Post 2", content="Content 2")
]
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
return User(id=id, name="John")
Queries
Simple Query
import graphene
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, info):
return "Hello, GraphQL!"
schema = graphene.Schema(query=Query)
# Query:
# {
# hello
# }
Query with Arguments
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
# Fetch user by id
return User(id=id, name="John", email="john@example.com")
schema = graphene.Schema(query=Query)
# Query:
# {
# user(id: "1") {
# name
# email
# }
# }
Query with Lists
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
class Query(graphene.ObjectType):
users = graphene.List(User)
def resolve_users(self, info):
return [
User(id="1", name="Alice"),
User(id="2", name="Bob")
]
schema = graphene.Schema(query=Query)
# Query:
# {
# users {
# id
# name
# }
# }
Mutations
Basic Mutation
import graphene
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
user = graphene.Field(lambda: User)
def mutate(self, info, name, email):
# Create user in database
user = User(id="1", name=name, email=email)
return CreateUser(user=user)
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
# Mutation:
# mutation {
# createUser(name: "John", email: "john@example.com") {
# user {
# id
# name
# email
# }
# }
# }
Mutation with Input Types
import graphene
class UserInput(graphene.InputObjectType):
name = graphene.String(required=True)
email = graphene.String(required=True)
age = graphene.Int()
class CreateUser(graphene.Mutation):
class Arguments:
input = UserInput(required=True)
user = graphene.Field(lambda: User)
def mutate(self, info, input):
user = User(
id="1",
name=input.name,
email=input.email
)
return CreateUser(user=user)
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
# Mutation:
# mutation {
# createUser(input: {name: "John", email: "john@example.com"}) {
# user {
# id
# name
# }
# }
# }
Complete Example: Blog API
"""
Complete GraphQL Example: Blog API
"""
from flask import Flask
from flask_graphql import GraphQLView
import graphene
# In-memory storage (use database in production)
users = {
"1": {"id": "1", "name": "Alice", "email": "alice@example.com"},
"2": {"id": "2", "name": "Bob", "email": "bob@example.com"}
}
posts = {
"1": {"id": "1", "title": "Post 1", "content": "Content 1", "author_id": "1"},
"2": {"id": "2", "title": "Post 2", "content": "Content 2", "author_id": "2"}
}
# GraphQL Types
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
posts = graphene.List(lambda: Post)
def resolve_posts(self, info):
return [Post(**post) for post in posts.values() if post["author_id"] == self.id]
class Post(graphene.ObjectType):
id = graphene.ID()
title = graphene.String()
content = graphene.String()
author = graphene.Field(User)
def resolve_author(self, info):
author_id = self.author_id if hasattr(self, 'author_id') else None
if author_id and author_id in users:
return User(**users[author_id])
return None
# Queries
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
users = graphene.List(User)
post = graphene.Field(Post, id=graphene.ID(required=True))
posts = graphene.List(Post)
def resolve_user(self, info, id):
if id in users:
return User(**users[id])
return None
def resolve_users(self, info):
return [User(**user) for user in users.values()]
def resolve_post(self, info, id):
if id in posts:
return Post(**posts[id])
return None
def resolve_posts(self, info):
return [Post(**post) for post in posts.values()]
# Mutations
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
user = graphene.Field(User)
def mutate(self, info, name, email):
user_id = str(len(users) + 1)
user = {"id": user_id, "name": name, "email": email}
users[user_id] = user
return CreateUser(user=User(**user))
class CreatePost(graphene.Mutation):
class Arguments:
title = graphene.String(required=True)
content = graphene.String(required=True)
author_id = graphene.ID(required=True)
post = graphene.Field(Post)
def mutate(self, info, title, content, author_id):
if author_id not in users:
raise Exception("User not found")
post_id = str(len(posts) + 1)
post = {
"id": post_id,
"title": title,
"content": content,
"author_id": author_id
}
posts[post_id] = post
return CreatePost(post=Post(**post))
class UpdatePost(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
title = graphene.String()
content = graphene.String()
post = graphene.Field(Post)
def mutate(self, info, id, title=None, content=None):
if id not in posts:
raise Exception("Post not found")
post = posts[id]
if title:
post["title"] = title
if content:
post["content"] = content
return UpdatePost(post=Post(**post))
class DeletePost(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
success = graphene.Boolean()
def mutate(self, info, id):
if id not in posts:
raise Exception("Post not found")
del posts[id]
return DeletePost(success=True)
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
create_post = CreatePost.Field()
update_post = UpdatePost.Field()
delete_post = DeletePost.Field()
# Schema
schema = graphene.Schema(query=Query, mutation=Mutation)
# Flask App
app = Flask(__name__)
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)
)
if __name__ == '__main__':
app.run(debug=True)
Example Queries
# Get all users
query {
users {
id
name
email
}
}
# Get user with posts
query {
user(id: "1") {
name
email
posts {
id
title
}
}
}
# Get all posts with authors
query {
posts {
id
title
content
author {
name
email
}
}
}
# Create user
mutation {
createUser(name: "Charlie", email: "charlie@example.com") {
user {
id
name
email
}
}
}
# Create post
mutation {
createPost(title: "New Post", content: "Post content", authorId: "1") {
post {
id
title
author {
name
}
}
}
}
# Update post
mutation {
updatePost(id: "1", title: "Updated Title") {
post {
id
title
content
}
}
}
# Delete post
mutation {
deletePost(id: "1") {
success
}
}
Error Handling
Custom Errors
import graphene
class UserNotFoundError(Exception):
pass
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
if id not in users:
raise UserNotFoundError(f"User {id} not found")
return User(**users[id])
Error Formatting
from graphql import GraphQLError
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
if id not in users:
raise GraphQLError(
message=f"User {id} not found",
extensions={"code": "USER_NOT_FOUND"}
)
return User(**users[id])
GraphQL vs REST
REST Example
# REST API
GET /api/users # Get all users
GET /api/users/1 # Get user 1
GET /api/users/1/posts # Get posts for user 1
POST /api/users # Create user
PUT /api/users/1 # Update user 1
DELETE /api/users/1 # Delete user 1
GraphQL Example
# Single endpoint: /graphql
# Query
query {
users {
id
name
posts {
title
}
}
}
# Mutation
mutation {
createUser(name: "John", email: "john@example.com") {
id
name
}
}
Comparison
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple | Single |
| Data Fetching | Fixed | Flexible |
| Over-fetching | Common | Avoided |
| Under-fetching | Common | Avoided |
| Type System | No | Yes |
| Introspection | Limited | Full |
| Caching | Easy | Complex |
| Learning Curve | Easy | Moderate |
Best Practices
1. Use Input Types for Mutations
# Good: Use InputObjectType
class UserInput(graphene.InputObjectType):
name = graphene.String(required=True)
email = graphene.String(required=True)
class CreateUser(graphene.Mutation):
class Arguments:
input = UserInput(required=True)
2. Handle Errors Gracefully
def resolve_user(self, info, id):
try:
user = get_user_from_db(id)
if not user:
raise GraphQLError("User not found")
return user
except Exception as e:
raise GraphQLError(f"Error fetching user: {str(e)}")
3. Use Field Resolvers
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
posts = graphene.List(Post)
def resolve_posts(self, info):
# Lazy loading
return get_posts_for_user(self.id)
4. Validate Input
class CreateUser(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)
def mutate(self, info, email):
if '@' not in email:
raise GraphQLError("Invalid email format")
# Create user
5. Use Enums
class UserRole(graphene.Enum):
ADMIN = "admin"
USER = "user"
GUEST = "guest"
class User(graphene.ObjectType):
role = graphene.Field(UserRole)
Common Mistakes and Pitfalls
1. N+1 Query Problem
# WRONG: N+1 queries
class User(graphene.ObjectType):
posts = graphene.List(Post)
def resolve_posts(self, info):
# This runs for each user
return get_posts_for_user(self.id)
# CORRECT: Batch loading
class User(graphene.ObjectType):
posts = graphene.List(Post)
def resolve_posts(self, info):
# Batch load all posts
return batch_load_posts([user.id for user in users])
2. Not Using Input Types
# WRONG: Many arguments
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
age = graphene.Int()
address = graphene.String()
# ... many more
# CORRECT: Use InputObjectType
class UserInput(graphene.InputObjectType):
name = graphene.String(required=True)
email = graphene.String(required=True)
# ... all fields
class CreateUser(graphene.Mutation):
class Arguments:
input = UserInput(required=True)
3. Not Handling Errors
# WRONG: No error handling
def resolve_user(self, info, id):
return users[id] # May raise KeyError
# CORRECT: Handle errors
def resolve_user(self, info, id):
if id not in users:
raise GraphQLError("User not found")
return users[id]
Practice Exercise
Exercise: GraphQL API
Objective: Create a GraphQL API for a task management application.
Instructions:
-
Create a GraphQL API with:
- Task type (id, title, description, completed, created_at)
- User type (id, name, email, tasks)
- Queries: tasks, task, users, user
- Mutations: createTask, updateTask, deleteTask, createUser
-
Your API should include:
- Proper type definitions
- Resolvers for all fields
- Error handling
- Input validation
Example Solution:
"""
GraphQL Exercise: Task Management API
"""
from flask import Flask
from flask_graphql import GraphQLView
import graphene
from datetime import datetime
# In-memory storage
tasks = {}
users = {}
task_id_counter = 1
user_id_counter = 1
# GraphQL Types
class Task(graphene.ObjectType):
id = graphene.ID()
title = graphene.String()
description = graphene.String()
completed = graphene.Boolean()
created_at = graphene.String()
user = graphene.Field(lambda: User)
def resolve_user(self, info):
user_id = self.user_id if hasattr(self, 'user_id') else None
if user_id and user_id in users:
return User(**users[user_id])
return None
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
tasks = graphene.List(Task)
def resolve_tasks(self, info):
return [Task(**task) for task in tasks.values() if task.get("user_id") == self.id]
# Queries
class Query(graphene.ObjectType):
task = graphene.Field(Task, id=graphene.ID(required=True))
tasks = graphene.List(Task, completed=graphene.Boolean())
user = graphene.Field(User, id=graphene.ID(required=True))
users = graphene.List(User)
def resolve_task(self, info, id):
if id not in tasks:
raise graphene.GraphQLError(f"Task {id} not found")
return Task(**tasks[id])
def resolve_tasks(self, info, completed=None):
task_list = [Task(**task) for task in tasks.values()]
if completed is not None:
task_list = [t for t in task_list if t.completed == completed]
return task_list
def resolve_user(self, info, id):
if id not in users:
raise graphene.GraphQLError(f"User {id} not found")
return User(**users[id])
def resolve_users(self, info):
return [User(**user) for user in users.values()]
# Mutations
class CreateTask(graphene.Mutation):
class Arguments:
title = graphene.String(required=True)
description = graphene.String()
user_id = graphene.ID(required=True)
task = graphene.Field(Task)
def mutate(self, info, title, description=None, user_id=None):
global task_id_counter
if user_id and user_id not in users:
raise graphene.GraphQLError(f"User {user_id} not found")
task_id = str(task_id_counter)
task = {
"id": task_id,
"title": title,
"description": description or "",
"completed": False,
"created_at": datetime.now().isoformat(),
"user_id": user_id
}
tasks[task_id] = task
task_id_counter += 1
return CreateTask(task=Task(**task))
class UpdateTask(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
title = graphene.String()
description = graphene.String()
completed = graphene.Boolean()
task = graphene.Field(Task)
def mutate(self, info, id, title=None, description=None, completed=None):
if id not in tasks:
raise graphene.GraphQLError(f"Task {id} not found")
task = tasks[id]
if title is not None:
task["title"] = title
if description is not None:
task["description"] = description
if completed is not None:
task["completed"] = completed
return UpdateTask(task=Task(**task))
class DeleteTask(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
success = graphene.Boolean()
def mutate(self, info, id):
if id not in tasks:
raise graphene.GraphQLError(f"Task {id} not found")
del tasks[id]
return DeleteTask(success=True)
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
user = graphene.Field(User)
def mutate(self, info, name, email):
global user_id_counter
# Check if email already exists
for user in users.values():
if user.get("email") == email:
raise graphene.GraphQLError("Email already exists")
user_id = str(user_id_counter)
user = {
"id": user_id,
"name": name,
"email": email
}
users[user_id] = user
user_id_counter += 1
return CreateUser(user=User(**user))
class Mutation(graphene.ObjectType):
create_task = CreateTask.Field()
update_task = UpdateTask.Field()
delete_task = DeleteTask.Field()
create_user = CreateUser.Field()
# Schema
schema = graphene.Schema(query=Query, mutation=Mutation)
# Flask App
app = Flask(__name__)
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True)
)
if __name__ == '__main__':
app.run(debug=True)
Example Queries:
# Get all tasks
query {
tasks {
id
title
description
completed
user {
name
}
}
}
# Get completed tasks
query {
tasks(completed: true) {
id
title
}
}
# Get user with tasks
query {
user(id: "1") {
name
email
tasks {
id
title
completed
}
}
}
# Create task
mutation {
createTask(title: "New Task", description: "Task description", userId: "1") {
task {
id
title
completed
}
}
}
# Update task
mutation {
updateTask(id: "1", completed: true) {
task {
id
title
completed
}
}
}
# Delete task
mutation {
deleteTask(id: "1") {
success
}
}
Expected Output: A complete GraphQL API with task and user management.
Challenge (Optional):
- Add task categories
- Add task priorities
- Implement task filtering and sorting
- Add user authentication
- Add subscriptions for real-time updates
- Add pagination
Key Takeaways
- GraphQL - Query language for APIs
- Graphene - Python library for GraphQL
- Schema - Defines API structure
- Types - Object types, scalar types, input types
- Queries - Read data
- Mutations - Modify data
- Resolvers - Functions that fetch data
- Type system - Strongly typed schema
- Single endpoint - All operations through /graphql
- Flexible queries - Fetch exactly what you need
- Error handling - GraphQLError for errors
- Input types - Use for complex mutations
- Nested resolvers - Handle relationships
- Best practices - Input types, error handling, validation
- GraphQL vs REST - Understand differences
Quiz: GraphQL
Test your understanding with these questions:
-
What does GraphQL stand for?
- A) Graph Query Language
- B) Graph Query Library
- C) GraphQL Query Language
- D) None of the above
-
What is used to read data in GraphQL?
- A) Mutation
- B) Query
- C) Subscription
- D) Resolver
-
What is used to modify data in GraphQL?
- A) Mutation
- B) Query
- C) Subscription
- D) Resolver
-
What Python library is used for GraphQL?
- A) GraphQL-Python
- B) Graphene
- C) GraphQL-py
- D) Flask-GraphQL
-
What defines the API structure?
- A) Resolver
- B) Schema
- C) Type
- D) Query
-
What fetches data in GraphQL?
- A) Query
- B) Resolver
- C) Mutation
- D) Schema
-
What is the main advantage of GraphQL over REST?
- A) Multiple endpoints
- B) Fetch exactly what you need
- C) Easier caching
- D) Simpler syntax
-
What type is used for mutations with multiple fields?
- A) ObjectType
- B) InputObjectType
- C) ScalarType
- D) ListType
-
What indicates a non-nullable field?
- A) !
- B) ?
- C) *
- D) +
-
What is GraphiQL?
- A) GraphQL library
- B) GraphQL IDE
- C) GraphQL server
- D) GraphQL client
Answers:
- D) None of the above (GraphQL is the name)
- B) Query (read data)
- A) Mutation (modify data)
- B) Graphene (Python GraphQL library)
- B) Schema (defines structure)
- B) Resolver (fetches data)
- B) Fetch exactly what you need (main advantage)
- B) InputObjectType (for mutations)
- A) ! (non-nullable indicator)
- B) GraphQL IDE (interactive IDE)
Next Steps
Excellent work! You've mastered GraphQL basics. You now understand:
- GraphQL concepts
- Graphene library
- How to build GraphQL APIs
- Queries and mutations
What's Next?
- Course 5: Python for Data Science
- Learn data science fundamentals
- Work with data analysis libraries
- Explore machine learning
Additional Resources
- GraphQL Documentation: graphql.org/
- Graphene Documentation: docs.graphene-python.org/
- Flask-GraphQL: github.com/graphql-python/flask-graphql
- GraphQL Best Practices: graphql.org/learn/best-practices/
Lesson completed! You're ready to move on to the next course.