
Table of contents
Open Table of contents
Setting up the Project
The first thing we need is a clean Python project. For this, we use Poetry, a modern dependency management tool. Poetry creates an isolated environment and handles installation cleanly. you can start by running following command.
poetry new graphql-api
cd graphql-api
This command will guide you to create a new project. After that, we need install the dependencies
poetry add fastapi strawberry-graphql databases sqlalchemy aiosqlite uvicorn
Here is what each package does. FastAPI runs the application. Strawberry adds GraphQL support. Databases gives us async database connections. SQLAlchemy defines our tables and models. aiosqlite is the driver to connect to SQLite in async mode. Finally, uvicorn runs the server.
Once this setup is complete, we are ready to start coding.
Database Setup
The database logic lives inside database.py
. Let’s look at the code:
from databases import Database
from sqlalchemy import create_engine, MetaData
DATABASE_URL = "sqlite:///./test.db"
database = Database(DATABASE_URL)
metadata = MetaData()
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
src/graphql-api/database.py
This file creates the connection to the database. The DATABASE_URL
points to a local SQLite file called test.db
. The Database
object from the databases
package gives us an async-friendly way to interact. We also define metadata
to hold the structure of our tables. The engine
is the actual SQLAlchemy engine used to create tables.
Notice the check_same_thread
setting. SQLite normally enforces that one connection belongs to one thread. Since FastAPI works asynchronously with multiple tasks, we disable this check so the connection works smoothly.
This file will be imported later wherever we need to connect to the database.
Defining Models
Now we move to models.py
, where we define the actual database tables.
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from .database import metadata
users = Table(
"users",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String, nullable=False),
)
posts = Table(
"posts",
metadata,
Column("id", Integer, primary_key=True),
Column("title", String, nullable=False),
Column("content", String, nullable=False),
Column("user_id", Integer, ForeignKey("users.id"))
)
src/graphql-api/models.py
Here we define two tables: users
and posts
.
The users
table has two columns: an id
as the primary key and a name
that cannot be empty. The posts
table stores blog posts. Each post has its own id
, a title
, some content
, and a user_id
that links back to the users
table. The ForeignKey("users.id")
ensures that every post belongs to a valid user.
This is the foundation of our database schema. Users can have many posts, but each post belongs to exactly one user.
Building the FastAPI and GraphQL App
Now comes the main file, main.py
. This is where everything connects. Let’s break it down step by step.
Importing and Setting Up
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry
from typing import List
from .database import database, engine, metadata
from .models import users, posts
src/graphql-api/main.py
We bring in FastAPI, Strawberry, typing helpers, and our database and model definitions. This sets the stage for the application.
We then create the tables in SQLite.
metadata.create_all(engine)
src/graphql-api/main.py
Defining GraphQL Types
@strawberry.type
class Post:
id: int
title: str
content: str
@strawberry.type
class User:
id: int
name: str
posts: List[Post]
src/graphql-api/main.py
In GraphQL, you need to define types that represent the shape of data clients can query. We have two types: Post
and User
. The User
type includes a list of posts, so when a query requests a user, you can also fetch their posts in one request. This is one of the strongest features of GraphQL: nested queries become natural.
Writing Queries
@strawberry.type
class Query:
@strawberry.field
async def all_users(self) -> List[User]:
query = users.select()
result = await database.fetch_all(query)
users_list = []
for row in result:
post_query = posts.select().where(posts.c.user_id == row["id"])
user_posts = await database.fetch_all(post_query)
posts_list = [Post(id=p["id"], title=p["title"], content=p["content"]) for p in user_posts]
users_list.append(User(id=row["id"], name=row["name"], posts=posts_list))
return users_list
@strawberry.field
async def get_user(self, id: int) -> User | None:
query = users.select().where(users.c.id == id)
user_row = await database.fetch_one(query)
if user_row:
post_query = posts.select().where(posts.c.user_id == id)
user_posts = await database.fetch_all(post_query)
posts_list = [Post(id=p["id"], title=p["title"], content=p["content"]) for p in user_posts]
return User(id=user_row["id"], name=user_row["name"], posts=posts_list)
return None
src/graphql-api/main.py
Here we define two queries.
The all_users
query fetches all users from the database, then for each user fetches their posts, and finally returns a list of users with their posts attached.
The get_user
query fetches a single user by ID. If the user exists, it also retrieves their posts. If the user does not exist, it returns None
.
Notice how much flexibility GraphQL gives us. With one request, you can get both the user and their posts instead of making separate calls.
Writing Mutations
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_user(self, name: str) -> User:
query = users.insert().values(name=name)
user_id = await database.execute(query)
return User(id=user_id, name=name, posts=[])
@strawberry.mutation
async def create_post(self, user_id: int, title: str, content: str) -> Post:
query = posts.insert().values(user_id=user_id, title=title, content=content)
post_id = await database.execute(query)
return Post(id=post_id, title=title, content=content)
src/graphql-api/main.py
Mutations are GraphQL’s way of writing data. We define two mutations here.
The create_user
mutation inserts a new row into the users
table with the given name. It returns a User
object with an empty posts list.
The create_post
mutation creates a new post for a given user. It inserts the title, content, and the user ID into the posts
table and returns the created post.
These two mutations allow clients to grow the database by adding users and posts.
Putting It All Together
Finally, we connect everything into the FastAPI app.
schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_app = GraphQLRouter(schema)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
src/graphql-api/main.py
We build the schema by combining queries and mutations. The GraphQLRouter
turns the schema into a FastAPI router. We then include it under the /graphql
path. When the app starts, we connect to the database. When the app shuts down, we disconnect cleanly.
Running the API
With everything ready, run the app using uvicorn.
poetry run uvicorn main:app --reload
Go to
http://127.0.0.1:8000/graphql
in your browser. You will see a GraphQL playground.
You can run queries this to test your API.
mutation {
createUser(name: "lakshan") {
id
name
}
}
This creates a user named lakshan. Then you can add a post.
mutation {
createPost(userId: 1, title: "Hello", content: "My first post") {
id
title
content
}
}
Finally, fetch everything.
query {
allUsers {
id
name
posts {
id
title
content
}
}
}
The result shows all the users and content they created. All of this happens through one endpoint, yet you control the shape of the response.
What We Built and Next Steps
We have built a complete GraphQL API using Strawberry and FastAPI, backed by SQLite. The API supports creating users, creating posts, and fetching data with nested queries. Along the way, we set up a clean project with Poetry, structured the database, and integrated everything into FastAPI.
There are many ways you can extend this project. You could add authentication so only certain users can create posts. You could implement pagination for the all_users
query to handle large datasets. You could also swap SQLite with PostgreSQL or MySQL for production use. If you want more advanced GraphQL features, you can add input types, subscriptions for real-time updates, or custom resolvers.
The key takeaway is that with Strawberry and FastAPI, building a GraphQL API in Python is both simple and powerful. You get strong typing, async performance, and a schema-first design that makes your API easy to use and maintain.