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 (app.create_router)
CREATE OR REPLACE FUNCTION app.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 app.routers
WHERE hostname = p_hostname AND tenant_id = 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 app.routers (hostname, ip_address, tenant_id, 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 app.routers 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 shape250+
lines in traditional GraphQL
(resolver + validation + error handling + logging)
50
lines in FraiseQL
(just the declarative contract)
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 |
|---|---|---|---|
| Lines per mutation | 80-150 lines | 15-30 lines | 5x reduction |
| Files per mutation | 3-5 files | 1 file | Single source of truth |
| Validation code | 20-40 lines/mutation | 0 lines (in PostgreSQL) | Not in app code |
| Error handling code | 15-30 lines/mutation | 1 line (error_config) | Declarative config |
| Manual audit logging | 5-10 lines/mutation | 0 lines (automatic) | Built-in via triggers |
| LLM context needed | 200-400 lines | 30-50 lines | 8x less to read |
| Time to generate (LLM) | 5-10 minutes | 30-60 seconds | 10x faster |
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.