FastAPI GraphQL Integration
In this tutorial, we'll explore how to integrate GraphQL with FastAPI to create powerful, flexible APIs that allow clients to request exactly the data they need.
Introduction to GraphQL with FastAPI
GraphQL is a query language for APIs that gives clients the power to request exactly the data they need and nothing more. Unlike traditional REST APIs where endpoints return fixed data structures, GraphQL provides a more flexible approach where the client specifies the structure of the response.
FastAPI, with its modern features and high performance, pairs excellently with GraphQL. This combination offers several advantages:
- Reduced over-fetching: Clients only get the data they request
- Fewer API calls: Multiple resources can be fetched in a single request
- Strongly typed schema: Both FastAPI and GraphQL emphasize type safety
- Self-documenting APIs: GraphQL schemas serve as documentation
- Efficient frontend development: Frontend teams can work independently, requesting exactly the data they need
Setting Up GraphQL in FastAPI
There are several GraphQL libraries for Python, but we'll use Strawberry GraphQL, which offers excellent integration with FastAPI through its ASGI integration.
Step 1: Install Dependencies
First, let's install the required packages:
pip install fastapi uvicorn strawberry-graphql
Step 2: Define Your GraphQL Schema
Create a file named schema.py
with your GraphQL types and schema:
import strawberry
from typing import List
# Define GraphQL types
@strawberry.type
class Book:
id: str
title: str
author: str
published_year: int
# Sample data
books_db = [
Book(
id="1",
title="The Great Gatsby",
author="F. Scott Fitzgerald",
published_year=1925
),
Book(
id="2",
title="To Kill a Mockingbird",
author="Harper Lee",
published_year=1960
),
Book(
id="3",
title="1984",
author="George Orwell",
published_year=1949
)
]
# Define query resolvers
@strawberry.type
class Query:
@strawberry.field
def books(self) -> List[Book]:
return books_db
@strawberry.field
def book(self, id: str) -> Book:
for book in books_db:
if book.id == id:
return book
return None
# Create the schema
schema = strawberry.Schema(query=Query)
Step 3: Integrate with FastAPI
Now, let's create our FastAPI application and integrate the GraphQL schema:
from fastapi import FastAPI
import strawberry
from strawberry.asgi import GraphQL
from schema import schema
app = FastAPI(title="FastAPI with GraphQL")
# Create a GraphQL ASGI application
graphql_app = GraphQL(schema)
# Mount the GraphQL app at the /graphql endpoint
app.add_route("/graphql", graphql_app)
app.add_websocket_route("/graphql", graphql_app)
@app.get("/")
def read_root():
return {"message": "Navigate to /graphql to use the GraphiQL interface"}
Step 4: Run Your Application
Run your application with Uvicorn:
uvicorn main:app --reload
Now visit http://localhost:8000/graphql
to access the GraphiQL interface, where you can test your GraphQL API interactively.
Making GraphQL Queries
In the GraphiQL interface, you can run queries like:
query {
books {
id
title
author
published_year
}
}
Output:
{
"data": {
"books": [
{
"id": "1",
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"published_year": 1925
},
{
"id": "2",
"title": "To Kill a Mockingbird",
"author": "Harper Lee",
"published_year": 1960
},
{
"id": "3",
"title": "1984",
"author": "George Orwell",
"published_year": 1949
}
]
}
}
Or query a specific book:
query {
book(id: "2") {
title
author
}
}
Output:
{
"data": {
"book": {
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}
}
}
Notice how in the second query, we only requested the title and author fields, and that's all we got in the response.
Adding Mutations to Your GraphQL API
Let's expand our API to allow creating, updating, and deleting books:
# Add this to your schema.py file
@strawberry.type
class Mutation:
@strawberry.mutation
def add_book(self, title: str, author: str, published_year: int) -> Book:
# Generate a new ID (in a real app, use a better ID generation strategy)
new_id = str(len(books_db) + 1)
# Create new book
new_book = Book(
id=new_id,
title=title,
author=author,
published_year=published_year
)
# Add to database
books_db.append(new_book)
return new_book
@strawberry.mutation
def update_book(self, id: str, title: str = None, author: str = None,
published_year: int = None) -> Book:
for book in books_db:
if book.id == id:
if title:
book.title = title
if author:
book.author = author
if published_year:
book.published_year = published_year
return book
return None
@strawberry.mutation
def delete_book(self, id: str) -> bool:
for i, book in enumerate(books_db):
if book.id == id:
books_db.pop(i)
return True
return False
# Update the schema to include mutations
schema = strawberry.Schema(query=Query, mutation=Mutation)
Now you can use mutations in GraphiQL:
mutation {
addBook(title: "The Hobbit", author: "J.R.R. Tolkien", publishedYear: 1937) {
id
title
author
}
}
Building Relationships Between Types
One of GraphQL's strengths is handling relationships between data. Let's extend our example to include authors and their books:
import strawberry
from typing import List
@strawberry.type
class Author:
id: str
name: str
birth_year: int
@strawberry.field
def books(self) -> List["Book"]:
return [book for book in books_db if book.author_id == self.id]
@strawberry.type
class Book:
id: str
title: str
published_year: int
author_id: str
@strawberry.field
def author(self) -> Author:
for author in authors_db:
if author.id == self.author_id:
return author
return None
# Sample data
authors_db = [
Author(id="1", name="F. Scott Fitzgerald", birth_year=1896),
Author(id="2", name="Harper Lee", birth_year=1926),
Author(id="3", name="George Orwell", birth_year=1903)
]
books_db = [
Book(id="1", title="The Great Gatsby", author_id="1", published_year=1925),
Book(id="2", title="To Kill a Mockingbird", author_id="2", published_year=1960),
Book(id="3", title="1984", author_id="3", published_year=1949)
]
@strawberry.type
class Query:
@strawberry.field
def books(self) -> List[Book]:
return books_db
@strawberry.field
def authors(self) -> List[Author]:
return authors_db
@strawberry.field
def book(self, id: str) -> Book:
for book in books_db:
if book.id == id:
return book
return None
@strawberry.field
def author(self, id: str) -> Author:
for author in authors_db:
if author.id == id:
return author
return None
schema = strawberry.Schema(query=Query)
Now you can make nested queries:
query {
authors {
name
books {
title
published_year
}
}
}
Output:
{
"data": {
"authors": [
{
"name": "F. Scott Fitzgerald",
"books": [
{
"title": "The Great Gatsby",
"published_year": 1925
}
]
},
{
"name": "Harper Lee",
"books": [
{
"title": "To Kill a Mockingbird",
"published_year": 1960
}
]
},
{
"name": "George Orwell",
"books": [
{
"title": "1984",
"published_year": 1949
}
]
}
]
}
}
Authentication and Authorization with GraphQL
In real-world applications, you'll often need to protect your GraphQL API with authentication. Here's how you can implement it:
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
import strawberry
from strawberry.asgi import GraphQL
from strawberry.types import Info
from typing import List, Optional
# OAuth2 scheme for token authentication
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Authentication dependency
async def get_current_user(token: str = Depends(oauth2_scheme)):
# In a real app, validate token and fetch user from database
if token != "fake-token":
raise HTTPException(status_code=401, detail="Invalid token")
return {"username": "testuser"}
# Create a custom GraphQL context
class CustomContext:
def __init__(self, user=None):
self.user = user
# Dependency to create context with user info
async def get_context(user=Depends(get_current_user)):
return CustomContext(user=user)
@strawberry.type
class Book:
id: str
title: str
author: str
published_year: int
# Sample data
books_db = [
Book(id="1", title="The Great Gatsby", author="F. Scott Fitzgerald", published_year=1925),
Book(id="2", title="To Kill a Mockingbird", author="Harper Lee", published_year=1960)
]
@strawberry.type
class Query:
@strawberry.field
def books(self, info: Info) -> List[Book]:
# Check if user is authenticated
context = info.context
if not context.user:
raise Exception("Not authenticated")
return books_db
schema = strawberry.Schema(query=Query)
app = FastAPI()
# Create GraphQL app with context
graphql_app = GraphQL(
schema,
context_getter=get_context
)
app.add_route("/graphql", graphql_app)
app.add_websocket_route("/graphql", graphql_app)
# Create a token endpoint for authentication
@app.post("/token")
async def login():
return {"access_token": "fake-token", "token_type": "bearer"}
Error Handling in GraphQL
GraphQL handles errors differently from REST APIs. Let's see how to properly handle errors:
@strawberry.type
class Query:
@strawberry.field
def book(self, id: str) -> Optional[Book]:
for book in books_db:
if book.id == id:
return book
# Instead of returning None, raise an exception
raise Exception(f"Book with id {id} not found")
GraphQL will include the error in the response:
{
"data": {
"book": null
},
"errors": [
{
"message": "Book with id 99 not found",
"locations": [{"line": 2, "column": 3}],
"path": ["book"]
}
]
}
Testing GraphQL APIs
Testing GraphQL APIs is slightly different from testing REST APIs. Here's a simple example using pytest:
from fastapi.testclient import TestClient
import pytest
from main import app
client = TestClient(app)
def test_get_books():
query = """
query {
books {
id
title
author
}
}
"""
response = client.post("/graphql", json={"query": query})
assert response.status_code == 200
data = response.json()
assert "errors" not in data
assert "data" in data
assert "books" in data["data"]
assert len(data["data"]["books"]) > 0
def test_get_book_by_id():
query = """
query {
book(id: "1") {
title
author
}
}
"""
response = client.post("/graphql", json={"query": query})
assert response.status_code == 200
data = response.json()
assert "errors" not in data
assert data["data"]["book"]["title"] == "The Great Gatsby"
Real-World Example: Building a Blog API
Let's put everything together in a more comprehensive example of a blog API:
import strawberry
from typing import List, Optional
from datetime import datetime
from strawberry.fastapi import GraphQLRouter
# Define types
@strawberry.type
class User:
id: str
username: str
@strawberry.field
def posts(self) -> List["Post"]:
return [post for post in posts_db if post.author_id == self.id]
@strawberry.type
class Post:
id: str
title: str
content: str
author_id: str
created_at: datetime
@strawberry.field
def author(self) -> User:
for user in users_db:
if user.id == self.author_id:
return user
return None
@strawberry.field
def comments(self) -> List["Comment"]:
return [comment for comment in comments_db if comment.post_id == self.id]
@strawberry.type
class Comment:
id: str
content: str
post_id: str
user_id: str
created_at: datetime
@strawberry.field
def post(self) -> Post:
for post in posts_db:
if post.id == self.post_id:
return post
return None
@strawberry.field
def user(self) -> User:
for user in users_db:
if user.id == self.user_id:
return user
return None
# Sample data
users_db = [
User(id="1", username="john_doe"),
User(id="2", username="jane_smith")
]
posts_db = [
Post(
id="1",
title="Introduction to GraphQL",
content="GraphQL is a query language for APIs...",
author_id="1",
created_at=datetime(2023, 1, 15)
),
Post(
id="2",
title="FastAPI Best Practices",
content="When building APIs with FastAPI...",
author_id="2",
created_at=datetime(2023, 2, 10)
)
]
comments_db = [
Comment(
id="1",
content="Great post!",
post_id="1",
user_id="2",
created_at=datetime(2023, 1, 16)
),
Comment(
id="2",
content="Thanks for sharing.",
post_id="2",
user_id="1",
created_at=datetime(2023, 2, 11)
)
]
# Inputs for mutations
@strawberry.input
class PostInput:
title: str
content: str
@strawberry.input
class CommentInput:
content: str
post_id: str
# Define queries
@strawberry.type
class Query:
@strawberry.field
def posts(self) -> List[Post]:
return posts_db
@strawberry.field
def post(self, id: str) -> Optional[Post]:
for post in posts_db:
if post.id == id:
return post
return None
@strawberry.field
def users(self) -> List[User]:
return users_db
@strawberry.field
def user(self, id: str) -> Optional[User]:
for user in users_db:
if user.id == id:
return user
return None
# Define mutations
@strawberry.type
class Mutation:
@strawberry.mutation
def create_post(self, post_input: PostInput, user_id: str) -> Post:
# In a real app, verify that user exists
new_post = Post(
id=str(len(posts_db) + 1),
title=post_input.title,
content=post_input.content,
author_id=user_id,
created_at=datetime.now()
)
posts_db.append(new_post)
return new_post
@strawberry.mutation
def add_comment(self, comment_input: CommentInput, user_id: str) -> Comment:
# In a real app, verify that post exists
new_comment = Comment(
id=str(len(comments_db) + 1),
content=comment_input.content,
post_id=comment_input.post_id,
user_id=user_id,
created_at=datetime.now()
)
comments_db.append(new_comment)
return new_comment
# Create schema
schema = strawberry.Schema(query=Query, mutation=Mutation)
# Create FastAPI GraphQL router
graphql_app = GraphQLRouter(schema)
# In your main.py:
# from fastapi import FastAPI
# from blog_schema import graphql_app
#
# app = FastAPI()
# app.include_router(graphql_app, prefix="/graphql")
This example demonstrates:
- Complex relationships between types (users, posts, comments)
- Input types for mutations
- Nested resolvers for related data
- DateTime handling
- Using GraphQLRouter for integration with FastAPI
Performance Considerations
When working with GraphQL in production, consider these performance considerations:
-
N+1 Query Problem: GraphQL can lead to inefficient database queries. Use dataloader patterns or ORM features like SQLAlchemy's join options to optimize.
-
Query Complexity: Prevent abusive queries by limiting query depth, complexity, or rate-limiting.
-
Caching: Implement caching for frequently accessed data.
# Example of query complexity limitation with Strawberry
from strawberry.extensions import QueryDepthLimiter
schema = strawberry.Schema(
query=Query,
extensions=[
QueryDepthLimiter(max_depth=10) # Limit query nesting to 10 levels
]
)
Summary
In this tutorial, we've covered:
- Setting up GraphQL with FastAPI using Strawberry GraphQL
- Creating a type system with queries and mutations
- Building relationships between types
- Handling authentication and authorization
- Error handling in GraphQL
- Testing GraphQL APIs
- A real-world example of a blog API
GraphQL with FastAPI offers a powerful combination for building flexible, type-safe APIs. It allows clients to request exactly the data they need, reducing over-fetching and improving performance.
Additional Resources
- Strawberry GraphQL Documentation
- GraphQL Official Website
- FastAPI Documentation
- Apollo GraphQL (popular client library)
Exercises
- Extend the blog API to include categories for posts
- Add pagination to the posts query
- Implement full-text search for posts
- Add authentication using JWT tokens
- Create a subscription endpoint to get real-time notifications for new comments
With these advanced features, you can build sophisticated GraphQL APIs using FastAPI that meet complex requirements while maintaining performance and developer productivity.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)