How It Works
FraiseQL is a database-first GraphQL framework. You write SQL views that compose nested JSONB responses. FraiseQL maps your GraphQL types to those views. At runtime, every query resolves to a single SQL statement — no resolvers, no N+1, no DataLoader.
Why this approach exists
Section titled “Why this approach exists”With a traditional GraphQL framework, a schema change cascades across multiple layers:
- Alter the database table
- Update the ORM model
- Regenerate GraphQL types
- Update resolvers
- Test everything together
With FraiseQL, the same change is localised:
- Update the SQL view (or add a column to it)
- Run
fraiseql compile - Done — the API reflects the view
The compile step is the key: FraiseQL reads your views and Python decorators, validates that everything aligns, and produces a compiled description of the API that the server executes without further interpretation. The database schema and the API schema are decoupled — a view is the contract between them.
The Three Layers
Section titled “The Three Layers”graph LR A[Write SQL Views<br/>v_* + fn_*] --> B[fraiseql compile<br/>GraphQL mapping] B --> C[fraiseql run<br/>GraphQL API server] C --> D[Single SQL query<br/>per GraphQL request]1. Write SQL Views
Section titled “1. Write SQL Views”You write SQL views following a simple pattern: each view has an id column and a data JSONB column containing the complete response for that entity.
CREATE VIEW v_user ASSELECT u.id, jsonb_build_object( 'id', u.id::text, 'name', u.name, 'email', u.email ) AS dataFROM tb_user u;Views compose other views. A post view embeds its author by referencing v_user.data:
CREATE VIEW v_post ASSELECT p.id, jsonb_build_object( 'id', p.id::text, 'title', p.title, 'content', p.content, 'author', vu.data ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;CREATE VIEW v_user ASSELECT u.id, JSON_OBJECT( 'id', u.id, 'name', u.name, 'email', u.email ) AS dataFROM tb_user u;Views compose other views. A post view embeds its author by referencing v_user.data:
CREATE VIEW v_post ASSELECT p.id, JSON_OBJECT( 'id', p.id, 'title', p.title, 'content', p.content, 'author', vu.data ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;CREATE VIEW v_user ASSELECT u.id, json_object( 'id', u.id, 'name', u.name, 'email', u.email ) AS dataFROM tb_user u;Views compose other views. A post view embeds its author by referencing v_user.data:
CREATE VIEW v_post ASSELECT p.id, json_object( 'id', p.id, 'title', p.title, 'content', p.content, 'author', vu.data ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;CREATE VIEW dbo.v_userWITH SCHEMABINDING ASSELECT u.id, ( SELECT u.id, u.name, u.email FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) AS dataFROM dbo.tb_user u;Views compose other views. A post view embeds its author by referencing v_user.data:
CREATE VIEW dbo.v_postWITH SCHEMABINDING ASSELECT p.id, ( SELECT p.id, p.title, p.content, vu.data AS author FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) AS dataFROM dbo.tb_post pJOIN dbo.tb_user u ON u.pk_user = p.fk_userJOIN dbo.v_user vu ON vu.id = u.id;Key insight: Each view owns its fields. Add a field to v_user once, and every view that embeds v_user.data gets it automatically. No duplication.
This is SQL you write, review, and own. You can use CTEs, window functions, stored procedures, custom aggregations — the full power of your database. FraiseQL works with PostgreSQL, SQL Server, MySQL, and SQLite (extensible to any database with a Rust driver). Or you can ask an LLM to generate the views. The pattern is consistent enough that local models produce accurate results.
2. Define GraphQL Types
Section titled “2. Define GraphQL Types”Define your GraphQL schema in your preferred programming language:
import fraiseql
@fraiseql.typeclass User: id: str name: str email: str posts: list['Post']
@fraiseql.typeclass Post: id: str title: str content: str author: Userimport { type, field } from 'fraiseql';
@type()class User { @field() id!: string; @field() name!: string; @field() email!: string; @field() posts!: Post[];}
@type()class Post { @field() id!: string; @field() title!: string; @field() content!: string; @field() author!: User;}package main
import "github.com/fraiseql/fraiseql"
type User struct { ID string Name string Email string Posts []Post}
type Post struct { ID string Title string Content string Author User}This is real code in your language, with full IDE support, type checking, and refactoring tools — not a GraphQL SDL file.
3. Compile the Mapping
Section titled “3. Compile the Mapping”When you run fraiseql compile, the compiler:
$ fraiseql compile✓ Compiled 2 types → mapped to SQL views✓ Built query executorWhat compilation does NOT do: It does not generate SQL views. Your views already exist in the database, created by you (or by 🍯 Confiture). Compilation maps GraphQL types to those views.
4. Serve
Section titled “4. Serve”The compiled executor serves GraphQL queries:
- No resolver execution — queries map directly to SQL views
- No N+1 queries — relationships are pre-joined in the views
- No runtime overhead — query paths are pre-compiled
$ fraiseql run→ GraphQL API running at http://localhost:8080/graphqlWhy Database-First Matters
Section titled “Why Database-First Matters”Traditional GraphQL (Runtime Resolvers)
Section titled “Traditional GraphQL (Runtime Resolvers)”Query → Parse → Validate → Execute Resolvers → Assemble Response ↓ Multiple DB Queries (N+1) ↓ DataLoader Batching ↓ Memory OverheadProblems:
- Resolver functions execute for every field
- N+1 queries require DataLoader workarounds
- Performance depends on how resolvers are written
- You debug generated SQL you didn’t write
FraiseQL (Database-First)
Section titled “FraiseQL (Database-First)”Query → Match Compiled Path → Single SQL Query → Stream ResponseBenefits:
- No resolver functions to execute
- Single SQL query per request — the view you wrote
- Predictable, consistent performance — you see the query plan
- Full database power — nothing is abstracted away (PostgreSQL, SQL Server, MySQL, SQLite)
The Database Lifecycle
Section titled “The Database Lifecycle”Writing SQL views is one half. Managing the database is the other. 🍯 Confiture provides four strategies for every scenario:
| Strategy | What It Does | When to Use |
|---|---|---|
| Build | Creates a fresh database from DDL files in under 1s | Development, CI/CD, testing |
| Migrate | Applies incremental ALTER statements | Production schema changes |
| Sync | Copies production data with anonymization | Realistic local development |
| Schema-to-Schema | Zero-downtime migration via FDW | Major production refactoring |
# Fresh database from your DDL filesconfiture build --env local
# Apply pending migrationsconfiture migrate up --env productionThe Compilation Pipeline
Section titled “The Compilation Pipeline”The compiler transforms your schema through three phases:
Phase 1: Parsing
Section titled “Phase 1: Parsing”Schema files in any supported language are validated and parsed into a type graph:
graph LR A[Schema Files<br/>Python / TS / Go] --> B[Lexer & Parser] B --> C[Type Graph]Phase 2: Analysis & SQL Generation
Section titled “Phase 2: Analysis & SQL Generation”The type graph is analyzed to generate SQL views and optimize query paths:
graph LR A[Type Graph] --> B[SQL View Analysis] B --> C[Query Path Optimization] C --> D[Compiled Query Map]Phase 3: Code Generation & Output
Section titled “Phase 3: Code Generation & Output”The final phase produces a compiled Rust binary, migration files, and all deployment artifacts:
graph LR A[Compiled Query Map] --> B[Rust Code Generation] B --> C[Query Executor Binary] B --> D[SDK Types<br/>6 languages] B --> E[Migration Files]Input: Schema in Any Language
Section titled “Input: Schema in Any Language”FraiseQL supports schema definition in Python, TypeScript, Go, Java, Rust, and more. All compile to the same optimized output.
Output: Compiled Artifacts
Section titled “Output: Compiled Artifacts”- View Mapping — Maps each GraphQL type to its SQL view
- Query Executor — High-performance compiled query paths
- SDK Types — Schema authoring libraries in 6 languages
Configuration
Section titled “Configuration”All configuration lives in a single TOML file:
[project]name = "my-api"version = "1.0.0"
[database]url = "${DATABASE_URL}"
[server]port = 8080host = "0.0.0.0"No YAML. No JSON. Just readable TOML.
Comparison
Section titled “Comparison”| Aspect | Traditional | FraiseQL |
|---|---|---|
| Query execution | Resolver functions | Compiled SQL view mapping |
| SQL authorship | Generated/hidden | Developer-owned |
| N+1 handling | DataLoader (manual) | Eliminated by design |
| Performance | Variable | Predictable (you see the query) |
| Database management | ORM migrations | 🍯 Confiture (4 strategies) |
| Schema definition | SDL only | Any language |
See It Live
Section titled “See It Live”Query against a running FraiseQL instance. The demo uses the same single-query architecture described above — inspect the response shape and relate it back to the v_post view pattern.
Next Steps
Section titled “Next Steps”- Developer-Owned SQL — Why SQL ownership is a strength
- 🍯 Confiture — Database lifecycle management
- CQRS Pattern — Understanding the architecture
- Schema Definition — How to define types