The Complete Guide to Setting Up GraphQL in Your Project
A step-by-step guide to building a GraphQL API with Python and Strawberry, from schema design to database integration, authentication, and optimization.
Advertisement
GraphQL is one of those technologies that sounds complicated until you actually try it. I remember when I first heard about it at PythonSkillset — everyone was talking about how it would replace REST, and I thought, "Great, another thing I need to learn." But once I set it up in a real project, I realized it's actually simpler than it seems. Let me walk you through the entire process, from zero to a working GraphQL API.
Why GraphQL Instead of REST?
Before we dive into setup, let's talk about why you'd want GraphQL in the first place. With REST, you often end up with either too much data (over-fetching) or not enough (under-fetching). For example, if you have a user profile endpoint that returns 20 fields but you only need the name and email, you're still getting all that extra data. GraphQL lets you ask for exactly what you need.
At PythonSkillset, we switched one of our internal tools from REST to GraphQL and saw our API response sizes drop by about 40%. That's a real difference when you're serving thousands of requests.
What You'll Need
Before we start, make sure you have: - Python 3.7 or higher installed - A basic understanding of Python classes and functions - pip (Python's package manager)
Step 1: Choose Your GraphQL Library
For Python, the two main options are Graphene and Strawberry. I prefer Strawberry because it uses Python type hints, which makes the code cleaner and more readable. But Graphene is also solid and has been around longer.
For this guide, we'll use Strawberry because it feels more "Pythonic" and integrates well with modern Python features.
pip install strawberry-graphql
Step 2: Define Your Data Types
The first thing you need is a schema. Think of this as a blueprint for your API. Let's say we're building a simple blog system.
import strawberry
@strawberry.type
class Author:
id: int
name: str
email: str
@strawberry.type
class Post:
id: int
title: str
content: str
author: Author
published_at: str
Notice how clean this looks. Each class becomes a GraphQL type automatically. The @strawberry.type decorator tells Strawberry to treat this as a GraphQL object type.
Step 3: Create Your Query Resolvers
Now we need to tell GraphQL how to actually get the data. This is where resolvers come in. A resolver is just a function that returns data for a specific field.
@strawberry.type
class Query:
@strawberry.field
def get_author(self, id: int) -> Author:
# In a real app, you'd query your database here
return Author(id=id, name="Jane Doe", email="jane@example.com")
@strawberry.field
def get_posts(self) -> list[Post]:
# Simulating a database query
author = Author(id=1, name="Jane Doe", email="jane@example.com")
return [
Post(id=1, title="First Post", content="Hello world", author=author, published_at="2024-01-15"),
Post(id=2, title="Second Post", content="GraphQL is cool", author=author, published_at="2024-01-20")
]
The @strawberry.field decorator marks each method as a queryable field. The return type annotations tell GraphQL what shape the data will have.
Step 4: Build the Schema
Now we connect everything together:
schema = strawberry.Schema(query=Query)
That's it. One line. The schema object now knows about all your types and queries.
Step 5: Set Up the Server
You need a way to serve your GraphQL API. FastAPI works great with Strawberry:
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
Run this with uvicorn:
uvicorn main:app --reload
Now visit http://localhost:8000/graphql and you'll see the GraphQL playground where you can test your queries.
Step 6: Write Your First Query
Open the playground and try this:
{
getPosts {
title
content
author {
name
}
}
}
You'll get back exactly the fields you asked for — no more, no less. That's the beauty of GraphQL.
Step 7: Add Mutations (Writing Data)
Queries are for reading data. For creating, updating, or deleting, you need mutations. Here's how:
@strawberry.type
class Mutation:
@strawberry.mutation
def create_post(self, title: str, content: str, author_id: int) -> Post:
# In a real app, save to database
author = Author(id=author_id, name="Jane Doe", email="jane@example.com")
return Post(id=3, title=title, content=content, author=author, published_at="2024-02-01")
Add it to your schema:
schema = strawberry.Schema(query=Query, mutation=Mutation)
Now you can run mutations like:
mutation {
createPost(title: "New Post", content: "My content", authorId: 1) {
id
title
}
}
Step 8: Handle Real Data with a Database
In a real project, you won't return hardcoded data. Here's how to connect to a SQLite database using SQLAlchemy:
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
engine = create_engine("sqlite:///blog.db")
SessionLocal = sessionmaker(bind=engine)
class AuthorModel(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
class PostModel(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String)
content = Column(String)
author_id = Column(Integer)
Then update your resolvers to query the database:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@strawberry.type
class Query:
@strawberry.field
def get_posts(self) -> list[Post]:
db = next(get_db())
posts = db.query(PostModel).all()
return [
Post(
id=post.id,
title=post.title,
content=post.content,
author=Author(id=post.author_id, name="Jane", email="jane@example.com"),
published_at=str(post.published_at)
)
for post in posts
]
Step 8: Handle Errors Gracefully
GraphQL has a built-in error handling system. Instead of crashing your whole API when something goes wrong, you can return partial results with error messages:
from strawberry.types import Info
@strawberry.type
class Query:
@strawberry.field
def get_author(self, id: int, info: Info) -> Author | None:
try:
db = next(get_db())
author = db.query(AuthorModel).filter(AuthorModel.id == id).first()
if not author:
raise Exception(f"Author with id {id} not found")
return Author(id=author.id, name=author.name, email=author.email)
except Exception as e:
# GraphQL will handle this gracefully
raise e
Step 9: Add Authentication
Most real projects need authentication. Here's a simple way to add it:
from strawberry.permission import BasePermission
from fastapi import Request
class IsAuthenticated(BasePermission):
message = "You need to be logged in"
def has_permission(self, source, info: Info, **kwargs) -> bool:
request: Request = info.context["request"]
# Check for auth token in headers
auth_header = request.headers.get("Authorization")
if not auth_header:
return False
# Validate token (simplified example)
return auth_header.startswith("Bearer ")
@strawberry.type
class Query:
@strawberry.field(permission_classes=[IsAuthenticated])
def get_private_data(self) -> str:
return "This is secret data"
Step 10: Optimize with DataLoaders
One common problem with GraphQL is the N+1 query problem. If you have a list of posts and each post needs its author, you might end up querying the database once for the posts and then once for each author. DataLoaders batch these requests:
from strawberry.dataloader import DataLoader
from collections import defaultdict
class AuthorLoader(DataLoader):
async def load_keys(self, keys):
# Batch load all authors at once
db = next(get_db())
authors = db.query(AuthorModel).filter(AuthorModel.id.in_(keys)).all()
author_map = {author.id: author for author in authors}
return [author_map.get(key) for key in keys]
@strawberry.type
class Post:
@strawberry.field
async def author(self) -> Author:
loader = AuthorLoader()
author_data = await loader.load(self.author_id)
return Author(id=author_data.id, name=author_data.name, email=author_data.email)
Step 10: Test Your API
Don't skip testing. GraphQL makes testing easier because you can test individual resolvers:
import pytest
from strawberry.schema.config import StrawberryConfig
def test_get_posts():
# Create a test schema
test_schema = strawberry.Schema(query=Query)
# Execute a query
result = test_schema.execute_sync("""
query {
getPosts {
title
content
}
}
""")
assert result.data is not None
assert len(result.data["getPosts"]) > 0
assert "title" in result.data["getPosts"][0]
Common Pitfalls to Avoid
-
Don't expose your database models directly — Always create separate GraphQL types. This gives you control over what gets exposed.
-
Watch out for circular references — If Post has an Author and Author has Posts, you'll get infinite loops. Use
strawberry.lazyto handle this. -
Don't forget about pagination — Without it, a query for all posts could return thousands of results. Use
strawberry.relayfor cursor-based pagination.
When GraphQL Might Not Be Right
GraphQL isn't always the answer. If you have a simple API with a few endpoints, REST might be simpler. Also, GraphQL caching is more complex than REST caching. For public APIs where you want to leverage CDN caching, REST might be better.
But for complex applications with multiple data sources and varying client needs, GraphQL shines. At PythonSkillset, we use it for our dashboard where different teams need different data views.
Next Steps
Once you have the basics working, explore: - Subscriptions for real-time data - Federation for microservices - Apollo Studio for monitoring
The best way to learn is to build something real. Start with a small project — maybe a todo list or a simple blog — and expand from there. You'll quickly see why GraphQL has become so popular in modern web development.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.