A production-ready GraphQL API built with FastAPI and Strawberry (code-first), backed by SQLAlchemy + SQLite. Includes N+1 prevention via eager loading, full CRUD mutations with input validation, and an interactive GraphQL Playground.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python main.py
# API at http://localhost:8000
# GraphQL Playground at http://localhost:8000/graphqlquery {
users {
id
username
email
posts {
id
title
createdAt
}
}
}query {
post(id: 1) {
id
title
content
author {
username
email
}
}
}query {
posts(limit: 5, offset: 0) {
id
title
author { username }
}
}mutation {
createUser(input: { username: "carol", email: "carol@example.com" }) {
id
username
email
}
}mutation {
createPost(input: {
title: "My First Post"
content: "Hello GraphQL world!"
authorId: 1
}) {
id
title
createdAt
author { username }
}
}mutation {
updateUser(id: 1, input: { email: "newemail@example.com" }) {
id
username
email
}
}mutation {
deletePost(id: 2) {
success
message
}
}graphql-fastapi/
├── main.py # FastAPI app + GraphQL router + startup seed
├── db/
│ ├── __init__.py # Engine, session, get_db dependency
│ └── models.py # SQLAlchemy ORM models (User, Post)
└── schema/
├── types.py # Strawberry types and input types
├── queries.py # Query resolvers (eager loading for N+1 prevention)
└── mutations.py # Create/update/delete resolvers with validation
Without optimization, fetching 100 users with their posts would make 101 queries. This is prevented with SQLAlchemy's joinedload:
# BAD: N+1 — one query per user to load posts
users = db.query(User).all()
for user in users:
posts = user.posts # triggers a new query for each user
# GOOD: 2 queries total (1 for users, 1 join for posts)
from sqlalchemy.orm import joinedload
users = db.query(User).options(joinedload(User.posts)).all()Change the database URL in db/__init__.py:
DATABASE_URL = "postgresql://user:password@localhost/dbname"And install the driver:
pip install psycopg2-binary