Better APIs for Frappe.
FrappeAPI brings FastAPI-style routing and validation to the Frappe Framework. Define endpoints with type hints, get automatic validation and documentation.
pip install frappeapifrom frappeapi import FrappeAPI
app = FrappeAPI()
@app.get()
def hello(name: str = "World"):
return {"message": f"Hello, {name}!"}Enable FastAPI-style paths for cleaner URLs:
from frappeapi import FrappeAPI
app = FrappeAPI(fastapi_path_format=True)
@app.get("/items/{item_id}")
def get_item(item_id: str):
return {"id": item_id}
# GET /api/items/abc123
# Response: {"id": "abc123"}Multiple path parameters:
@app.get("/users/{user_id}/orders/{order_id}")
def get_user_order(user_id: str, order_id: int):
return {"user_id": user_id, "order_id": order_id}
# GET /api/users/john/orders/42
# Response: {"user_id": "john", "order_id": 42}Combine path and query parameters:
@app.get("/products/{category}")
def list_products(
category: str, # Path parameter
sort_by: str = "name", # Query parameter
limit: int = 10 # Query parameter
):
return {"category": category, "sort_by": sort_by, "limit": limit}
# GET /api/products/electronics?sort_by=price&limit=20Automatic type parsing:
@app.get()
def get_product_details(
product_id: int,
unit_price: float,
in_stock: bool
):
return {
"product_id": product_id, # "123" -> 123
"unit_price": unit_price, # "9.99" -> 9.99
"in_stock": in_stock # "true" -> True
}Optional parameters with defaults:
@app.get()
def list_products(
category: str = "all",
page: int = 1,
search: str | None = None
):
return {"category": category, "page": page, "search": search}Enum parameters:
from enum import Enum
class OrderStatus(str, Enum):
pending = "pending"
processing = "processing"
completed = "completed"
@app.get()
def list_orders(status: OrderStatus = OrderStatus.pending):
return {"status": status}List parameters:
from frappeapi import Query
@app.get()
def search_products(
tags: List[str] = Query(default=[]),
categories: List[int] = Query(default=[])
):
return {"tags": tags, "categories": categories}
# GET ?tags=electronics&tags=sale&categories=1&categories=2
# Response: {"tags": ["electronics", "sale"], "categories": [1, 2]}Aliased parameters:
from typing import Annotated
from frappeapi import Query
@app.get()
def search_items(
search_text: Annotated[str, Query(alias="q")] = "",
page_number: Annotated[int, Query(alias="p")] = 1
):
return {"search": search_text, "page": page_number}
# GET ?q=laptop&p=2Query parameter models:
from pydantic import BaseModel, Field
class ProductFilter(BaseModel):
search: str | None = None
category: str = "all"
min_price: float = Field(0, ge=0)
in_stock: bool = True
@app.get()
def filter_products(filters: Annotated[ProductFilter, Query()]):
return filtersSingle model:
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
description: str | None = None
price: float = Field(..., gt=0)
@app.post()
def create_item(item: Item):
return itemMultiple body parameters:
class User(BaseModel):
username: str
email: str
class Item(BaseModel):
name: str
price: float
@app.post()
def create_user_item(user: User, item: Item):
return {"user": user, "item": item}
# Request body:
# {
# "user": {"username": "john", "email": "john@example.com"},
# "item": {"name": "Laptop", "price": 999.99}
# }Nested models:
from pydantic import HttpUrl
class Image(BaseModel):
url: HttpUrl
name: str
class Product(BaseModel):
name: str
price: float
images: List[Image]
@app.post()
def create_product(product: Product):
return productfrom typing import Annotated
from frappeapi import Form
@app.post()
def create_user_profile(
username: Annotated[str, Form()],
email: Annotated[str, Form()],
bio: Annotated[str | None, Form()] = None
):
return {"username": username, "email": email, "bio": bio}Small files (in-memory):
from typing import Annotated
from frappeapi import File, Form
@app.post()
def upload_document(
file: Annotated[bytes, File()],
description: Annotated[str | None, Form()] = None
):
return {"file_size": len(file), "description": description}Large files (streamed):
from frappeapi import UploadFile
@app.post()
def upload_large_file(file: UploadFile):
return {
"filename": file.filename,
"content_type": file.content_type
}Filter response data:
class UserResponse(BaseModel):
id: int
username: str
email: str
@app.get(response_model=UserResponse)
def get_user(user_id: int):
return {
"id": user_id,
"username": "john_doe",
"email": "john@example.com",
"password": "secret" # Filtered out
}List responses:
class Product(BaseModel):
id: int
name: str
price: float
@app.get(response_model=List[Product])
def list_products():
return [
{"id": 1, "name": "Laptop", "price": 999.99},
{"id": 2, "name": "Mouse", "price": 24.99}
]Raise HTTP exceptions:
from frappeapi.exceptions import HTTPException
@app.get()
def get_item(item_id: int):
if item_id < 0:
raise HTTPException(status_code=400, detail="Item ID must be positive")
return {"id": item_id}Custom exception handlers:
from frappeapi import JSONResponse, Request
class ItemNotFound(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFound)
def item_not_found_handler(request: Request, exc: ItemNotFound):
return JSONResponse(
status_code=404,
content={"error": "ITEM_NOT_FOUND", "detail": f"Item {exc.item_id} not found"}
)Override validation error handler:
from frappeapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
def validation_error_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": "VALIDATION_ERROR",
"details": [{"field": e["loc"][-1], "message": e["msg"]} for e in exc.errors()]
}
)from typing import Annotated
from frappeapi import Header
@app.get()
def get_user_info(
user_agent: Annotated[str, Header()],
x_custom_header: Annotated[str, Header()]
):
return {"user_agent": user_agent, "custom_header": x_custom_header}
# Headers: User-Agent, X-Custom-Header (hyphen converted to underscore)String validation:
class Product(BaseModel):
name: str = Field(min_length=3, max_length=50)
sku: str = Field(pattern="^[A-Z]{2}-[0-9]{4}$") # Format: XX-0000Numeric validation:
class Order(BaseModel):
quantity: int = Field(gt=0, le=100)
unit_price: float = Field(gt=0)
discount_percent: float = Field(ge=0, le=100)FrappeAPI automatically detects your Frappe version:
| Frappe Version | Support |
|---|---|
| v14.x | Stable |
| v15.x | Stable |
| v16.x | Beta |
Check detected version:
import frappeapi
print(frappeapi.get_detected_frappe_version()) # Returns: 14, 15, or 16FrappeAPI follows FastAPI's interface. For detailed information, see FastAPI's documentation.
- Frappe V14 support
- Frappe V15 support
- Frappe V16 support (develop branch)
-
app.get(...) -
app.post(...) -
app.put(...) -
app.patch(...) -
app.delete(...)
- Automatic type parsing based on type hints
- Required parameters (
needy: str,needy: str = ...) - Optional parameters with defaults (
skip: int = 0) - Optional parameters without defaults (
limit: int | None = None) - Enum support
- Boolean parameters
- List parameters (
?q=foo&q=bar) - Aliased parameters (
Query(alias="query")) - Query parameter models
- Automatic documentation generation
- Pydantic model body (
item: Item) - Multiple body parameters
- Singular values with
Body() - Embed body parameter
- Nested models
- Automatic type parsing
- Basic header support
- Header parameter models
- Duplicate headers
- Forbid extra headers
- Cookie parameter support
- Form fields with
Form() - Multiple form fields
- Form data as Pydantic model
- Forbid extra form fields
- Small files with
File() - Large files with
UploadFile - Optional file uploads
- Multiple file uploads
- HTTPException
- RequestValidationError
- ResponseValidationError
- Custom exception handlers
- Override default handlers
- Frappe transaction management
-
response_modelparameter - Return type annotations
- Output filtering
-
response_modeltakes precedence over return type
- String validations (
min_length,max_length,pattern) - Numeric validations (
gt,ge,lt,le) - Metadata (
title,description,deprecated) -
include_in_schema
- Rate limiting
- Dependencies
- Middleware
- Debugging capabilities
- Dotted path parameters
- PR #23135: Type hints for API functions
- PR #22300: Enhanced
frappe.whitelist() - PR #19029: Type safety improvements
- Issue #14905: API documentation discussion