Skip to content

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.

With a traditional GraphQL framework, a schema change cascades across multiple layers:

  1. Alter the database table
  2. Update the ORM model
  3. Regenerate GraphQL types
  4. Update resolvers
  5. Test everything together

With FraiseQL, the same change is localised:

  1. Update the SQL view (or add a column to it)
  2. Run fraiseql compile
  3. 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.

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]

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.

db/schema/02_read/v_user.sql
CREATE VIEW v_user AS
SELECT
u.id,
jsonb_build_object(
'id', u.id::text,
'name', u.name,
'email', u.email
) AS data
FROM tb_user u;

Views compose other views. A post view embeds its author by referencing v_user.data:

db/schema/02_read/v_post.sql
CREATE VIEW v_post AS
SELECT
p.id,
jsonb_build_object(
'id', p.id::text,
'title', p.title,
'content', p.content,
'author', vu.data
) AS data
FROM tb_post p
JOIN tb_user u ON u.pk_user = p.fk_user
JOIN 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.

Define your GraphQL schema in your preferred programming language:

schema.py
import fraiseql
@fraiseql.type
class User:
id: str
name: str
email: str
posts: list['Post']
@fraiseql.type
class Post:
id: str
title: str
content: str
author: User

This is real code in your language, with full IDE support, type checking, and refactoring tools — not a GraphQL SDL file.

When you run fraiseql compile, the compiler:

Terminal window
$ fraiseql compile
Compiled 2 types mapped to SQL views
Built query executor

What 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.

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
Terminal window
$ fraiseql run
GraphQL API running at http://localhost:8080/graphql
Traditional GraphQL vs FraiseQL: resolver-based N+1 pipeline compared to single SQL query path Traditional GraphQL vs FraiseQL: resolver-based N+1 pipeline compared to single SQL query path
Traditional GraphQL resolves fields one at a time (N+1 problem). FraiseQL matches a compiled path to a single SQL query.

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)

Writing SQL views is one half. Managing the database is the other. 🍯 Confiture provides four strategies for every scenario:

StrategyWhat It DoesWhen to Use
BuildCreates a fresh database from DDL files in under 1sDevelopment, CI/CD, testing
MigrateApplies incremental ALTER statementsProduction schema changes
SyncCopies production data with anonymizationRealistic local development
Schema-to-SchemaZero-downtime migration via FDWMajor production refactoring
Terminal window
# Fresh database from your DDL files
confiture build --env local
# Apply pending migrations
fraiseql migrate up --env production

The compiler transforms your schema through three phases:

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]

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]

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 handler

FraiseQL supports schema definition in Python, TypeScript, Go, Java, Rust, and more. All compile to the same optimized output.

  1. schema.compiled.json — Maps each GraphQL type to its SQL view with pre-compiled query paths
  2. SDK Client Types — Generated client types for your preferred language
  3. Migration Artifacts — Schema migration helpers

All configuration lives in a single TOML file:

fraiseql.toml
[fraiseql]
schema_file = "schema.json"
output_file = "schema.compiled.json"
[project]
name = "my-api"
version = "1.0.0"
[database]
url = "${DATABASE_URL}"
[server]
port = 8080
host = "0.0.0.0"

No YAML. No JSON. Just readable TOML.

AspectTraditionalFraiseQL
Query executionResolver functionsCompiled SQL view mapping
SQL authorshipGenerated/hiddenDeveloper-owned
N+1 handlingDataLoader (manual)Eliminated by design
PerformanceVariablePredictable (you see the query)
Database managementORM migrations🍯 Confiture (4 strategies)
Schema definitionSDL onlyAny language

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.

FraiseQL Demo API

One GraphQL query. One SQL SELECT. See the result.

Loading Apollo Sandbox...

This sandbox uses Apollo Sandbox (the same GraphQL IDE as fraiseql serve). Your queries execute against the endpoint below. No data is sent to Apollo. Learn more about privacy →