πŸ€– LLM-Optimized Development

Built for the AI-assisted development era. Clear patterns, strong typing, database-first architecture.

Why LLM-Optimized Matters

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.

The LLM Development Challenge

Traditional GraphQL frameworks require LLMs to understand:

  • ❌ Complex resolver chains with implicit data flow
  • ❌ Runtime validation scattered across decorators and classes
  • ❌ N+1 query patterns hidden in ORM relationships
  • ❌ Type definitions disconnected from database schema
  • ❌ Error handling logic mixed with business logic

Result: Higher hallucination rates, more manual corrections, slower iteration.

FraiseQL's LLM-First Design

With FraiseQL, LLMs see:

  • βœ… Explicit type hints on every parameter
  • βœ… Declarative patterns: @fraiseql.mutation, @fraiseql.input
  • βœ… Validation in PostgreSQL functions, not app code
  • βœ… Single source of truth: database schema drives types
  • βœ… Error configs are explicit and predictable

Real-World Example: Creating a Router

From a production codebase: printoptim_backend. Notice how an LLM can easily understand intent, types, and data flow.

❌ Traditional GraphQL

Scattered across multiple files
# 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:

  • β€’ 3 separate files to understand
  • β€’ 15+ lines of validation logic
  • β€’ Implicit error handling patterns
  • β€’ Manual SQL construction
  • β€’ Easy to forget audit logging

βœ… FraiseQL

printoptim_backend/mutations/router/create.py (15 lines total)
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:

  • βœ… Single file, complete picture
  • βœ… Types enforce validation automatically
  • βœ… Explicit error handling contract
  • βœ… Database function called automatically
  • βœ… Audit logging built into DB function

Key LLM-Friendly Patterns

1️⃣

Specialized Types Replace Validation Code

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.

2️⃣

Validation Lives in PostgreSQL Functions

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.

3️⃣

Explicit Error Configuration

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.

4️⃣

Automatic Context Injection

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.

Complex Example: Nested Address Creation

Real code from printoptim_backend showing how FraiseQL handles complex nested mutations with clear, LLM-readable patterns.

printoptim_backend/mutations/location/create.py
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

What LLMs Can Instantly Understand:

  • βœ… Flexible Input: Can either create nested address OR link existing via public_address_id
  • βœ… UNSET Semantics: UNSET means "not provided", None means "explicitly null"
  • βœ… Nested Objects: Type system clearly shows CreateNestedPublicAddressInput shape
  • βœ… Success/Failure Contract: Explicit response types with conflict handling
  • βœ… Context Injection: tenant_id and user_id injected automatically
  • βœ… No Business Logic: All validation and creation logic in PostgreSQL function

Compare: How Much Code Does an LLM Need to Read?

250+

lines in traditional GraphQL

(resolver + validation + error handling + logging)

50

lines in FraiseQL

(just the declarative contract)

LLM Development Workflow

1

Prompt: "Create a mutation to add a DNS server"

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.

2

Prompt: "Add duplicate detection for DNS servers"

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.

3

Prompt: "Add update and delete mutations"

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.

Why This Matters for Production

⚑

10x Faster Development

printoptim_backend has 50+ mutations generated with Claude in hours, not weeks.

  • βœ… LLM generates full CRUD operations from examples
  • βœ… Patterns are consistent across entire codebase
  • βœ… Minimal manual corrections needed
  • βœ… Junior devs + Claude = Senior-level output
🎯

Lower Hallucination Rate

Explicit patterns reduce guesswork. LLMs generate correct code on first try.

  • βœ… Type hints prevent invalid field references
  • βœ… Decorator patterns are unambiguous
  • βœ… Error configs have predictable semantics
  • βœ… Context injection removes parameter confusion
πŸ‘₯

Maintainable by Humans

LLM-generated code is also human-readable. No magic, no surprises.

  • βœ… Code review is straightforward
  • βœ… New team members understand patterns quickly
  • βœ… Debugging is easier (less code to read)
  • βœ… Refactoring is low-risk
πŸ›‘οΈ

Production-Ready Defaults

LLMs can't "forget" audit logging or multi-tenant isolationβ€”it's built into the framework.

  • βœ… tb_entity_change_log automatic audit trail
  • βœ… Tenant isolation via context_params
  • βœ… SQL injection protection by default
  • βœ… Error tracking via request IDs

Real Production Metrics

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

FastAPI: The LLM-Friendly Web Framework

FraiseQL's opinionated FastAPI integration amplifies LLM-friendliness with async-first, type-safe, dependency injection patterns.

Why FastAPI + FraiseQL = Perfect for LLMs

FastAPI Brings:

  • βœ… Explicit type hints everywhere
  • βœ… Dependency injection (testable, clear)
  • βœ… Async/await (no callback hell)
  • βœ… Auto-generated OpenAPI docs
  • βœ… CQRS-friendly endpoint separation

FraiseQL Adds:

  • βœ… Declarative mutation patterns
  • βœ… Database-first validation
  • βœ… Zero-boilerplate CRUD
  • βœ… Automatic error handling
  • βœ… Multi-tenant context injection
Full FastAPI + FraiseQL Integration
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

Start Building with LLMs

Prompt Template for Claude/Copilot

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.

Ready to Accelerate Development with AI?

FraiseQL's LLM-optimized patterns are production-ready today.