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 problems:
- 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 benefits:
- 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 migrationsfraiseql 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 & Query Path Compilation
Section titled “Phase 2: Analysis & Query Path Compilation”The type graph is analyzed against your existing SQL views to optimize query paths:
graph LR A[Type Graph] --> B[SQL View Analysis] B --> C[Query Path Optimization] C --> D[Compiled Query Map]Phase 3: Output
Section titled “Phase 3: Output”The final phase produces the compiled schema artifact and SDK type information:
graph LR A[Compiled Query Map] --> B[schema.compiled.json] A --> C[SDK Client Types] A --> D[Migration Artifacts]Transport-Aware Compilation
Section titled “Transport-Aware Compilation”The compiler generates different database view shapes for different transports.
JSON-shaped views (GraphQL and REST): The compiler generates views using json_agg/row_to_json — PostgreSQL produces JSON directly, and the server passes it through to the client. This is the fast path for GraphQL and REST.
Row-shaped views (gRPC): When [grpc] is enabled, the compiler generates standard SELECT views with typed columns for all operations. The server maps columns to protobuf fields — no JSON intermediate. The database does less work, the server does less work, the wire payload is smaller.
The same SDK annotations drive both shapes. The compiler determines the shape based on which transport annotations are present.
fraiseql compile ↓schema.compiled.json ├── JSON views (v_post, v_user, ...) → GraphQL + REST handlers └── Row views (rv_post, rv_user, ...) → gRPC handlerInput: 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”schema.compiled.json— Maps each GraphQL type to its SQL view with pre-compiled query paths- SDK Client Types — Generated client types for your preferred language
- Migration Artifacts — Schema migration helpers
Configuration
Section titled “Configuration”All configuration lives in a single TOML file:
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"
[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