A GraphQL-to-PostgreSQL translator with a CQRS architecture
Views for queries. Functions for mutations. GraphQL for developers.

FraiseQL is a Python framework that translates GraphQL queries directly into PostgreSQL queries, embracing a CQRS (Command Query Responsibility Segregation) architecture where database views handle queries and PostgreSQL functions handle mutations.
β‘ Quick Links
π― Core Concepts
FraiseQL has four fundamental patterns:
1. Types are Python Classes
@fraise_type
class User:
id: UUID
name: str
email: str
2. Queries are Functions (Not Resolvers!)
@fraiseql.query
async def get_user(info, id: UUID) -> User:
db = info.context["db"]
return await db.find_one("user_view", id=id)
3. All Data in JSONB Column (v0.1.0a14+)
CREATE VIEW user_view AS
SELECT
id,
tenant_id,
jsonb_build_object(
'id', id,
'name', name,
'email', email
) as data
FROM users;
4. Repository Handles Database
db = info.context["db"]
user = await db.find_one("user_view", id=user_id)
π― Built-in Validation with Scalar Types
FraiseQL includes comprehensive scalar types for common data validation needs:
from fraiseql.types import Port, IpAddress, CIDR, Hostname, MacAddress, EmailAddress
@fraise_type
class NetworkDevice:
hostname: Hostname
ip_address: IpAddress
mac_address: MacAddress
subnet: CIDR
ssh_port: Port
admin_email: EmailAddress
Available Scalars: Port
, IpAddress
, CIDR
, Hostname
, MacAddress
, EmailAddress
, UUID
, DateTime
, Date
, JSON
, and more.
β Complete Scalar Reference
β¨ Key Features
π Simple Patterns | Decorator-based API with no resolver classes |
π JSONB-First | All data flows through JSONB columns for consistency |
π Type-Safe | Full type hints with Python 3.11+ |
π‘οΈ SQL Injection Safe | Parameterized queries throughout |
π Pluggable Auth | Built-in Auth0, easy to add others |
β‘ FastAPI Integration | Production-ready ASGI application |
ποΈ High Performance | Direct SQL queries, no N+1 problems |
π― CQRS Architecture | Views for queries, functions for mutations |
π¦ Installation
pip install fraiseql
pip install "fraiseql[auth0]"
pip install "fraiseql[tracing]"
pip install "fraiseql[dev]"
β οΈ Breaking Changes:
- v0.1.0a14: All database views must now return data in a JSONB
data
column. See the Migration Guide
- v0.1.0a18: Partial object instantiation is now supported in development mode, allowing nested queries to request only specific fields
π Quick Start
1. Hello World (No Database)
import fraiseql
from datetime import datetime
from uuid import UUID, uuid4
@fraise_type
class Book:
id: UUID
title: str
author: str
published: datetime
@fraiseql.query
async def books(info) -> list[Book]:
"""Get all books."""
return [
Book(
id=uuid4(),
title="The Great Gatsby",
author="F. Scott Fitzgerald",
published=datetime(1925, 4, 10)
)
]
app = fraiseql.create_fraiseql_app(
types=[Book],
production=False
)
2. With Database (The Right Way)
"""
CREATE VIEW book_view AS
SELECT
id, -- For filtering
author, -- For author queries
published, -- For date filtering
jsonb_build_object(
'id', id,
'title', title,
'author', author,
'published', published
) as data -- REQUIRED: All object data here!
FROM books;
"""
@fraiseql.query
async def books(info, author: str | None = None) -> list[Book]:
"""Get books, optionally filtered by author."""
db = info.context["db"]
if author:
return await db.find("book_view", author=author)
return await db.find("book_view")
app = fraiseql.create_fraiseql_app(
database_url="postgresql://localhost/mydb",
types=[Book],
production=False
)
3. Common Mistakes to Avoid
class Query:
async def resolve_users(self, info):
pass
@fraiseql.query
async def users(info) -> list[User]:
db = info.context["db"]
return await db.find("user_view")
CREATE VIEW bad_view AS
SELECT id, name, email FROM users;
CREATE VIEW good_view AS
SELECT id, jsonb_build_object(
'id', id, 'name', name, 'email', email
) as data FROM users;
π See the Getting Started Guide for a complete walkthrough
π New in v0.1.0a18: Partial Object Instantiation
FraiseQL now supports partial object instantiation for nested queries in development mode. This means you can request only the fields you need from nested objects without errors:
query GetUsers {
users {
id
name
profile {
avatar
}
}
}
@fraise_type
class Profile:
id: UUID
avatar: str
email: str
bio: str
This brings FraiseQL closer to GraphQL's promise of "ask for what you need, get exactly that". See the Partial Instantiation Guide for details.
Complex Nested Query Example
With partial instantiation, you can now build efficient queries that traverse multiple levels of relationships:
query BlogDashboard {
posts(where: { published_at: { neq: null } }) {
id
title
published_at
author {
name
profile {
avatar
}
}
comments {
id
content
author {
name
}
}
}
}
All nested objects will be properly instantiated with only the requested fields, avoiding errors from missing required fields in the type definitions.
π― Why FraiseQL?
The Problem
Traditional GraphQL servers require complex resolver hierarchies, N+1 query problems, and lots of boilerplate.
The FraiseQL Solution
- Direct SQL queries from GraphQL queries
- JSONB pattern for consistent data access
- No resolver classes - just functions
- Type-safe with full IDE support
- Production-ready with auth, caching, and monitoring
- Partial field selection (v0.1.0a18+) for optimal queries
π Learn More
Real-World Example
Here's how you might structure a blog application:
@fraise_type
class Post:
id: UUID
title: str
content: str
author: User
comments: list['Comment']
tags: list[str]
published_at: datetime | None
@fraise_type
class Comment:
id: UUID
content: str
author: User
created_at: datetime
With a corresponding view:
CREATE VIEW post_details AS
SELECT
p.id,
p.author_id,
p.published_at,
jsonb_build_object(
'id', p.id,
'title', p.title,
'content', p.content,
'author', (
SELECT data FROM user_profile
WHERE id = p.author_id
),
'comments', (
SELECT jsonb_agg(
jsonb_build_object(
'id', c.id,
'content', c.content,
'author', (
SELECT data FROM user_profile
WHERE id = c.author_id
),
'created_at', c.created_at
)
ORDER BY c.created_at
)
FROM comments c
WHERE c.post_id = p.id
),
'tags', p.tags,
'published_at', p.published_at
) as data
FROM posts p;
Authentication
FraiseQL includes a pluggable authentication system:
from fraiseql.auth.decorators import requires_auth
from fraiseql.auth.auth0 import Auth0Config
@fraise_type
class Query:
@requires_auth
async def me(self, info) -> User:
user_id = info.context["user"].user_id
app = create_fraiseql_app(
database_url="postgresql://localhost/mydb",
auth=Auth0Config(
domain="your-domain.auth0.com",
api_identifier="https://api.example.com"
),
types=[User, Query],
)
LLM-Friendly Architecture
FraiseQL's design makes it exceptionally well-suited for AI-assisted development:
Clear Contracts
- Explicit type definitions with decorators (
@fraise_type
, @fraise_input
)
- Structured mutations with success/failure unions
- Well-defined boundaries between queries (views) and mutations (functions)
- No hidden magic - what you define is what you get
Simple, Common Languages
- Just Python and SQL - no proprietary DSLs or complex configurations
- Standard PostgreSQL - 40+ years of documentation and examples
- Familiar patterns - decorators and dataclasses that LLMs understand well
Predictable Code Generation
When you ask an LLM to generate a FraiseQL API, it can reliably produce:
@fraise_type
class Product:
id: UUID
name: str
price: Decimal
in_stock: bool
"""
CREATE VIEW product_catalog AS
SELECT
id, -- For filtering
category_id, -- For joins
jsonb_build_object(
'id', id,
'name', name,
'price', price,
'in_stock', quantity > 0
) as data -- All product data in 'data' column
FROM products;
"""
This simplicity means:
- Lower token costs - concise, standard patterns
- Higher accuracy - LLMs trained on Python/SQL perform better
- Faster iteration - generate, test, and refine quickly
- Maintainable output - generated code looks like human-written code
Development
Prerequisites
- Python 3.11+
- PostgreSQL 13+
- Docker (optional, for integration tests)
Setting Up
git clone https://github.com/fraiseql/fraiseql.git
cd fraiseql
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
pytest -m "not database"
Database Testing
FraiseQL uses a hybrid database testing approach that automatically adapts to your environment for optimal speed:
π Fastest Setup (Recommended):
brew install postgresql
sudo apt install postgresql
createdb fraiseql_test
export TEST_DATABASE_URL="postgresql://localhost/fraiseql_test"
pytest
π³ Zero-Setup Fallback:
pytest
See Database Testing Guide for complete setup instructions and troubleshooting.
Code Quality
ruff check src/
pyright
ruff format src/ tests/
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Why "FraiseQL"?
"Fraise" is French for strawberry. This project was heavily inspired by the excellent Strawberry GraphQL library, whose elegant API design showed us how delightful Python GraphQL development could be. While we take a different architectural approach, we aim to preserve that same developer-friendly experience.
Current Status
FraiseQL is in active development. We're working on:
- Performance benchmarks and optimization
- Additional authentication providers
- Enhanced query compilation for production
- More comprehensive documentation
- Real-world example applications
License
MIT License - see LICENSE for details.
FraiseQL: Where GraphQL meets PostgreSQL. π