Your First Hour with FraiseQL¶
Welcome! You've just completed the 5-minute quickstart and have a working GraphQL API. Now let's spend the next 55 minutes building your skills progressively. By the end, you'll understand how to extend FraiseQL applications and implement production patterns.
Prerequisites¶
Before starting, ensure you have the necessary imports in your app.py:
from fraiseql.types import ID
from datetime import datetime
import fraiseql
from fraiseql.fastapi import create_fraiseql_app
from fraiseql.sql import create_graphql_where_input
Note: This tutorial requires Python 3.13+ and uses modern type syntax (list[str], str | None) instead of the older typing module imports.
Minute 0-5: Quickstart Recap¶
Complete the 5-minute quickstart first
You should now have:
- A working GraphQL API at
http://localhost:8000/graphql - A PostgreSQL database with a
v_noteview - A basic note-taking app
â Checkpoint: Can you run this query and get results?
Minute 5-15: Understanding What You Built¶
Key concepts you should now understand:
- Database-first GraphQL: Start with PostgreSQL, not GraphQL types
- JSONB Views:
tb_*tables âv_*views â GraphQL responses (see JSONB View Pattern) - CQRS Pattern: Reads (views) vs Writes (PostgreSQL functions)
- Naming Conventions:
tb_*,v_*,fn_*,tv_*
â Checkpoint: Can you explain why FraiseQL uses JSONB views instead of traditional ORMs?
đĄ Advanced Filtering: FraiseQL supports powerful PostgreSQL operators including array filtering, full-text search, JSONB queries, and regex matching. See Filter Operators Reference for details.
Minute 15-30: Extend Your API - Add Tags to Notes¶
Challenge: Add a "tags" feature so notes can be categorized.
Step 1: Update Database Schema¶
First, add a tags column to your note table:
-- Add tags column to tb_note
ALTER TABLE tb_note ADD COLUMN tags TEXT[] DEFAULT '{}';
-- Update sample data with tags
UPDATE tb_note SET tags = ARRAY['work', 'urgent'] WHERE title = 'First Note';
UPDATE tb_note SET tags = ARRAY['personal', 'ideas'] WHERE title = 'Second Note';
-- Add a note with 'work' in the title for filter examples
INSERT INTO tb_note (title, content, tags)
VALUES ('Work Meeting Notes', 'Discussed Q4 project timeline', ARRAY['work', 'meeting']);
Step 2: Update the View¶
Modify v_note to include tags. Important: Views must include both an id column AND a data column containing the JSONB object:
-- Drop and recreate view with tags
DROP VIEW v_note;
CREATE VIEW v_note AS
SELECT
id, -- Required: FraiseQL queries filter by this column
jsonb_build_object(
'id', id,
'title', title,
'content', content,
'tags', tags
) as data -- Required: Contains the GraphQL response data
FROM tb_note;
Why both columns? The id column enables efficient WHERE clause filtering (WHERE id = $1), while the data column contains the complete JSONB object returned to GraphQL.
After making schema changes, restart your server to pick up the new view definition.
Step 3: Update Python Type¶
Add tags to your Note type:
Step 4: Add Filtering with Where Input Types¶
FraiseQL provides automatic Where input type generation for powerful, type-safe filtering:
# app.py
# Generate automatic Where input type for Note
NoteWhereInput = create_graphql_where_input(Note)
@fraiseql.query
async def notes(info, where: NoteWhereInput | None = None) -> list[Note]:
"""Get notes with optional filtering."""
db = info.context["db"]
# Use repository's find method with where parameter
return await db.find("v_note", where=where)
Restart your server to register the updated query with where filtering.
Step 5: Test Your Changes¶
Test the powerful filtering capabilities:
query {
# Get all notes
notes {
id
title
tags
}
# Filter notes by title containing "work"
workNotes: notes(where: { title: { contains: "work" } }) {
title
content
}
# Filter notes with specific tag using array contains
urgentNotes: notes(where: { tags: { contains: "urgent" } }) {
title
tags
}
# Combine multiple conditions
complexFilter: notes(where: {
AND: [
{ title: { contains: "meeting" } },
{ tags: { contains: "work" } }
]
}) {
title
content
tags
}
}
Available Filter Operators:
eq,neq- equals, not equalscontains,startswith,endswith- string matchinggt,gte,lt,lte- comparisonsin,nin- list membershipisnull- null checkingAND,OR,NOT- logical operators
and many more specialized operators for specific Postgresql types (CIDR, LTREE etc.)
â Checkpoint: Can you create a note with tags and use the various filtering operators?
Minute 30-45: Add a Mutation - Delete Notes¶
Challenge: Add the ability to delete notes.
Step 1: Create Delete Function (Basic Pattern)¶
Create a PostgreSQL function for deletion that returns a simple boolean:
-- Create basic delete function (returns boolean)
CREATE OR REPLACE FUNCTION fn_delete_note(note_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
DELETE FROM tb_note WHERE id = note_id;
RETURN FOUND; -- Returns true if a row was deleted, false otherwise
END;
$$ LANGUAGE plpgsql;
Step 2: Add Python Mutation (Basic Pattern)¶
Add a simple mutation to your app:
# app.py
@fraiseql.mutation
async def delete_note(info, id: ID) -> bool:
"""Delete a note by ID (returns true if deleted, false if not found)."""
db = info.context["db"]
return await db.fetchval("SELECT fn_delete_note($1)", id)
Restart your server to register the new mutation.
Step 3: Test the Mutation¶
Try this in GraphQL playground:
Step 4: Improve Error Handling (Production Pattern)¶
The boolean return is simple but doesn't provide error details. Let's improve this with structured success/failure types.
First, update the database function to return JSONB with error information:
-- Improved function that returns JSONB with error details
CREATE OR REPLACE FUNCTION fn_delete_note(note_id UUID)
RETURNS JSONB AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM tb_note WHERE id = note_id;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
IF deleted_count = 0 THEN
RETURN jsonb_build_object(
'success', false,
'message', 'Note not found',
'code', 'NOT_FOUND'
);
ELSE
RETURN jsonb_build_object(
'success', true,
'message', 'Note deleted successfully'
);
END IF;
END;
$$ LANGUAGE plpgsql;
Next, define success and failure types using FraiseQL decorators:
# app.py
@fraiseql.success
class DeleteNoteSuccess:
"""Successful deletion response."""
message: str = "Note deleted successfully"
@fraiseql.failure
class DeleteNoteError:
"""Deletion error response."""
message: str
code: str = "NOT_FOUND"
@fraiseql.mutation
async def delete_note(info, id: ID) -> DeleteNoteSuccess | DeleteNoteError:
"""Delete a note by ID with detailed error handling."""
db = info.context["db"]
# Call function that returns JSONB directly from database
# FraiseQL automatically maps JSONB to the appropriate type
result = await db.fetchval("SELECT fn_delete_note($1)", id)
# Return the appropriate type based on success field
if result.get("success"):
return DeleteNoteSuccess(message=result["message"])
else:
return DeleteNoteError(
message=result["message"],
code=result.get("code", "UNKNOWN_ERROR")
)
Why this pattern? Using @success and @error decorators creates a proper GraphQL union type, allowing clients to handle success and error cases explicitly in their queries.
Restart your server to register the updated mutation with new types.
â Checkpoint: Can you delete a note and handle the case where the note doesn't exist?
Minute 45-60: Production Patterns - Timestamps¶
Challenge: Add created_at and updated_at timestamps with automatic updates.
Step 1: Add Timestamp Columns¶
-- Add timestamp columns
ALTER TABLE tb_note ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
ALTER TABLE tb_note ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
-- Update existing records
UPDATE tb_note SET created_at = NOW(), updated_at = NOW();
Step 2: Create Update Trigger¶
-- Function to update updated_at
-- âšī¸ FraiseQL Best Practice: Use DEFAULT instead of triggers
-- Triggers hide logic from AI and make code harder to understand.
-- For timestamp updates, use explicit application code:
-- Python mutation example:
-- @mutation
-- async def update_note(id: str, title: str, context: Context) -> Note:
-- return await context.db.update("tb_note", id, {
-- "title": title,
-- "updated_at": datetime.utcnow() # Explicit!
-- })
-- Or use DEFAULT for automatic creation timestamps:
-- created_at TIMESTAMPTZ DEFAULT NOW() -- Set once on INSERT
Step 3: Update View¶
-- Recreate view with timestamps
DROP VIEW v_note;
CREATE VIEW v_note AS
SELECT
id, -- Required: enables WHERE clause filtering
jsonb_build_object(
'id', id,
'title', title,
'content', content,
'tags', tags,
'createdAt', created_at,
'updatedAt', updated_at
) as data -- Required: contains GraphQL response
FROM tb_note;
Restart your server after updating the view.
Step 4: Update Python Type¶
# app.py
@fraiseql.type(sql_source="v_note")
class Note:
id: ID
title: str
content: str
tags: list[str]
created_at: datetime # Add this
updated_at: datetime # Add this
What is sql_source? This parameter tells FraiseQL which database view to query. It's optional when the view name matches the class name (e.g., class Note â view v_note), but becomes required if:
- The view name doesn't follow the v_{lowercase_class_name} pattern
- You want to explicitly document the data source
- You're using a table view (tv_*) instead of a regular view
In this example, we could omit sql_source since FraiseQL automatically infers v_note from the class name Note. However, being explicit makes the code more readable and maintainable.
Restart your server to register the updated Note type with timestamps.
Step 5: Test Automatic Updates¶
Create a note, then update it and verify updated_at changes but created_at stays the same.
â Checkpoint: Do timestamps update automatically when you modify notes?
đ Congratulations¶
You've completed your first hour with FraiseQL! You now know how to:
- â Extend existing APIs with new fields
- â Add filtering capabilities
- â Implement write operations (mutations)
- â Handle errors gracefully
- â Add production-ready features like timestamps
What's Next?¶
Immediate Next Steps (2-3 hours)¶
- Beginner Learning Path - Deep dive into all core concepts
- Blog API Tutorial - Build a complete application
Explore Examples (30 minutes each)¶
- E-commerce API (../examples/ecommerce/) - Shopping cart, products, orders
- Real-time Chat (../examples/real_time_chat/) - Subscriptions and real-time updates
- Multi-tenant SaaS (../examples/apq_multi_tenant/) - Enterprise patterns
Advanced Topics¶
- Performance Guide - Optimization techniques
- Multi-tenancy - Building SaaS applications
Need Help?¶
- Troubleshooting Guide - Common issues and solutions
- Quick Reference - Copy-paste code patterns
- GitHub Discussions - Community support
Ready for more? The Beginner Learning Path will take you from here to building production applications! đ