Built for the AI-assisted development era. Clear patterns, strong typing, database-first architecture.
Claude, Copilot, and other AI coding assistants work best with explicit patterns, clear types, and minimal magic. FraiseQL is designed from the ground up to be readable and predictable for both humans and LLMs.
Traditional GraphQL frameworks require LLMs to understand:
Result: Higher hallucination rates, more manual corrections, slower iteration.
With FraiseQL, LLMs see:
@fraiseql.mutation, @fraiseql.inputFrom a production codebase: printoptim_backend. Notice how an LLM can easily understand intent, types, and data flow.
# schema.graphql
type Router {
id: ID!
hostname: String!
ipAddress: String
macAddress: String
}
input CreateRouterInput {
hostname: String!
ipAddress: String
macAddress: String
}
# resolvers/router.py (50+ lines)
async def create_router(parent, info, input):
# Validate hostname format
if not is_valid_hostname(input.hostname):
raise GraphQLError("Invalid hostname")
# Validate IP address if provided
if input.ip_address:
if not is_valid_ip(input.ip_address):
raise GraphQLError("Invalid IP")
# Validate MAC address if provided
if input.mac_address:
if not is_valid_mac(input.mac_address):
raise GraphQLError("Invalid MAC")
# Check for duplicates
existing = await db.query(
"SELECT * FROM routers WHERE ..."
)
if existing:
raise GraphQLError("Router exists")
# Insert into database
router = await db.query(
"INSERT INTO routers ..."
)
# Log change
await db.query(
"INSERT INTO change_log ..."
)
return router
LLM Challenges:
import fraiseql
from fraiseql.types import Hostname, IpAddress, MacAddress
@fraiseql.input
class CreateRouterInput:
hostname: Hostname
ip_address: IpAddress | None = None
mac_address: MacAddress | None = None
note: str | None = None
@fraiseql.success
class CreateRouterSuccess:
message: str = "Router created successfully"
router: Router
@fraiseql.failure
class CreateRouterError:
message: str
conflict_router: Router | None = None
@fraiseql.mutation(
function="create_router",
context_params={
"tenant_id": "input_pk_organization",
"user_id": "input_created_by",
},
error_config=fraiseql.DEFAULT_ERROR_CONFIG,
)
class CreateRouter:
input: CreateRouterInput
success: CreateRouterSuccess
failure: CreateRouterError
LLM Benefits:
Instead of writing validation logic, use FraiseQL's built-in types. LLMs can easily understand Hostname vs str.
# β Traditional: LLM must understand validation logic
hostname: str # Needs: pattern validation, length checks, character validation
# β
FraiseQL: Intent is clear from the type
from fraiseql.types import Hostname, IpAddress, MacAddress, EmailAddress
hostname: Hostname # Self-documenting, auto-validated
ip_address: IpAddress # IPv4/IPv6 validation built-in
mac_address: MacAddress # MAC format validation automatic
email: EmailAddress # RFC-compliant email validation
π‘ LLM Impact: Type hints become single source of truth. No need to parse validation logic scattered across files.
Business rules are enforced in the database, not in Python code. This means no validation code in the app layer for LLMs to reason about.
-- PostgreSQL function (fn_create_router)
CREATE OR REPLACE FUNCTION fn_create_router(
p_hostname TEXT,
p_ip_address INET,
p_tenant_id UUID,
p_user_id UUID
) RETURNS jsonb AS $$
DECLARE
v_router_id UUID;
v_conflict RECORD;
BEGIN
-- Check for duplicate hostname (database enforces uniqueness)
SELECT * INTO v_conflict
FROM tb_router
WHERE hostname = p_hostname AND fk_tenant = p_tenant_id;
IF FOUND THEN
RETURN jsonb_build_object(
'status', 'conflict:already_exists',
'message', 'Router with this hostname already exists',
'conflict_router', row_to_json(v_conflict)
);
END IF;
-- Insert router (constraints enforce data integrity)
INSERT INTO tb_router (hostname, ip_address, fk_tenant, created_by)
VALUES (p_hostname, p_ip_address, p_tenant_id, p_user_id)
RETURNING id INTO v_router_id;
-- Audit logging happens automatically via trigger
-- (tb_entity_change_log table)
RETURN jsonb_build_object(
'status', 'ok',
'message', 'Router created successfully',
'router', (SELECT row_to_json(r) FROM tb_router r WHERE id = v_router_id)
);
END;
$$ LANGUAGE plpgsql;
π‘ LLM Impact: Python code is purely declarative. LLMs don't need to understand business rulesβjust the interface contract.
Error handling is configured, not coded. LLMs can predict behavior from the error config.
# Example from printoptim_backend mutation templates
# For entities where duplicates are errors:
error_config = fraiseql.STRICT_UNIQUE_CONFIG
# - "conflict:already_exists" β GraphQL error
# - "noop:existing" β GraphQL error
# For idempotent operations:
error_config = fraiseql.DEFAULT_ERROR_CONFIG
# - "noop:not_found" β Success (null entity)
# - "noop:no_changes" β Success (current entity)
# For delete operations:
error_config = fraiseql.DELETE_CONFIG
# - "noop:not_found" β Success (idempotent delete)
π‘ LLM Impact: Predictable error semantics. LLMs can generate correct error handling without guessing.
Multi-tenant context and user tracking are declaratively configured, not manually threaded through code.
@fraiseql.mutation(
function="create_location",
context_params={
"tenant_id": "input_pk_organization", # From GraphQL context
"user_id": "input_created_by", # From GraphQL context
},
)
class CreateLocation:
# FraiseQL automatically injects:
# - tenant_id from request context
# - user_id from authenticated user
# - Both passed to PostgreSQL function
# - No manual parameter passing needed
π‘ LLM Impact: Security and multi-tenancy are visible in configuration, not hidden in middleware logic.
Real code from printoptim_backend showing how FraiseQL handles complex nested mutations with clear, LLM-readable patterns.
import fraiseql
from fraiseql.types.definitions import UNSET
@fraiseql.input
class CreateNestedPublicAddressInput:
"""Input for creating a nested public address."""
# Required fields (matching frontend expectations)
city: str
city_code: str
latitude: float
longitude: float
postal_code: str
street_name: str
# Optional fields - UNSET allows backend to distinguish
# between "not provided" vs "explicitly null"
street_number: str | None = UNSET
country: str | None = UNSET
street_suffix: str | None = UNSET
external_address_id: str | None = UNSET # For deduplication
@fraiseql.input
class CreateLocationInput:
"""Input for creating a new location."""
# Required fields
name: str
location_level_id: uuid.UUID
# Hierarchy relationships
parent_id: uuid.UUID | None = UNSET
location_type_id: uuid.UUID | None = UNSET
# Address handling - flexible: create nested or link existing
address: CreateNestedPublicAddressInput | None = UNSET
public_address_id: uuid.UUID | None = UNSET
# Physical properties
has_elevator: bool | None = UNSET
has_stairs: bool | None = UNSET
n_stair_steps: int | None = UNSET
available_width_mm: int | None = UNSET
available_depth_mm: int | None = UNSET
available_height_mm: int | None = UNSET
@fraiseql.success
class CreateLocationSuccess:
message: str = "Location created successfully"
location: Location # Full location object with nested address
@fraiseql.failure
class CreateLocationError:
message: str
conflict_location: Location | None = None
original_payload: dict | None = None # For debugging
@fraiseql.mutation(
function="create_location",
context_params={
"tenant_id": "input_pk_organization",
"user_id": "input_created_by",
},
error_config=fraiseql.DEFAULT_ERROR_CONFIG,
)
class CreateLocation:
"""Create a new location with optional nested address creation."""
input: CreateLocationInput
success: CreateLocationSuccess
failure: CreateLocationError
public_address_idUNSET means "not provided", None means "explicitly null"CreateNestedPublicAddressInput shapeTraditional GraphQL
250+ lines in Python
(resolver + validation + error handling + logging)
FraiseQL
50 lines Python + 80 lines PL/pgSQL
(declarative contract + database function)
Inference economics: PL/pgSQL is 30-50% shorter than equivalent Python. Fewer tokens, cheaper inference. Decades of stable syntax means LLMs rarely hallucinate.
LLM reads existing mutation patterns (e.g., CreateRouter) and generates:
import fraiseql
from fraiseql.types import Hostname, IpAddress
@fraiseql.input
class CreateDnsServerInput:
hostname: Hostname
ip_address: IpAddress
note: str | None = None
@fraiseql.mutation(
function="create_dns_server",
context_params={
"tenant_id": "input_pk_organization",
"user_id": "input_created_by",
},
)
class CreateDnsServer:
input: CreateDnsServerInput
success: CreateDnsServerSuccess
failure: CreateDnsServerError
β Correct on first try. Pattern matching is trivial for LLMs.
LLM sees error_config pattern and updates:
@fraiseql.mutation(
function="create_dns_server",
context_params={
"tenant_id": "input_pk_organization",
"user_id": "input_created_by",
},
error_config=fraiseql.STRICT_UNIQUE_CONFIG, # Added
)
class CreateDnsServer:
# ... rest stays the same
β Correct. LLM understands error config semantics from examples.
LLM generates full CRUD suite by pattern matching:
@fraiseql.mutation(
function="update_dns_server",
context_params={"tenant_id": "input_pk_organization", "user_id": "input_updated_by"},
)
class UpdateDnsServer:
input: UpdateDnsServerInput
success: UpdateDnsServerSuccess
failure: UpdateDnsServerError
@fraiseql.mutation(
function="delete_dns_server",
context_params={"tenant_id": "input_pk_organization", "user_id": "input_deleted_by"},
error_config=fraiseql.DELETE_CONFIG,
)
class DeleteDnsServer:
input: DeletionInput # Standard deletion input
success: DeleteDnsServerSuccess
failure: DeleteDnsServerError
β Correct. Conventions are clear and consistent.
printoptim_backend has 50+ mutations generated with Claude in hours, not weeks.
Explicit patterns reduce guesswork. LLMs generate correct code on first try.
LLM-generated code is also human-readable. No magic, no surprises.
LLMs can't "forget" audit logging or multi-tenant isolationβit's built into the framework.
From printoptim_backend (50+ entities, 150+ mutations, production SaaS)
| Metric | Traditional GraphQL | FraiseQL | Impact |
|---|---|---|---|
| Python lines per mutation | 80-150 lines | 15-30 lines | Logic moves to PostgreSQL |
| PostgreSQL function | 0 lines (logic in Python) | 40-80 lines | Business logic in DB |
| Python files per mutation | 3-5 files | 1 file | Simpler Python layer |
| Where validation lives | Python code | PostgreSQL fn_* function | ACID guarantees |
| Error handling pattern | 15-30 lines/mutation | 1 line (error_config) | Declarative config |
| Audit logging | Manual per mutation | Built into fn_* functions | Automatic via triggers |
| Time to generate (LLM) | 5-10 minutes | 30-60 seconds | Predictable patterns |
* Total code volume is similarβbut PostgreSQL patterns are universally understood by LLMs, reducing hallucinations.
FraiseQL's opinionated FastAPI integration amplifies LLM-friendliness with async-first, type-safe, dependency injection patterns.
from fastapi import FastAPI, Depends
from fraiseql.fastapi import create_fraiseql_app
# LLMs can easily understand this stack:
# - FastAPI for HTTP layer
# - FraiseQL for GraphQL β PostgreSQL
# - Type hints for everything
# - Dependency injection for context
app = FastAPI()
# Create FraiseQL GraphQL endpoint
graphql_app = create_fraiseql_app(
database_url="postgresql://...",
types=[Router, Location, DnsServer], # Type registry
mutations=[CreateRouter, UpdateRouter, DeleteRouter],
production=True, # Enables APQ, TurboRouter
)
# Mount GraphQL endpoint
app.mount("/graphql", graphql_app)
# CQRS separation: Queries via GET, Mutations via POST
# LLMs understand this pattern instantly
I'm using FraiseQL, a Python GraphQL framework with database-first architecture.
Key patterns:
- Mutations are declarative classes with @fraiseql.mutation decorator
- Input types use @fraiseql.input with type hints (Hostname, IpAddress, etc.)
- Validation happens in PostgreSQL functions, not Python code
- Error handling via error_config (STRICT_UNIQUE_CONFIG, DELETE_CONFIG, etc.)
- Context injection via context_params (tenant_id, user_id)
Task: Create CRUD mutations for [entity name] with these fields: [field list]
Reference existing mutations in [directory path] for patterns.
Result: Claude/Copilot generates production-ready mutations in seconds, following exact project patterns.
FraiseQL's LLM-optimized patterns are production-ready today.