Tutorial: A Simple FastAPI Project¶
This tutorial walks through creating a small FastAPI app that renders a form from a Pydantic model using pydantic-schemaforms.
It also explains when to use the sync handler (handle_form) vs the async handler (handle_form_async).
Prerequisites¶
- Python 3.14+
1) Create a project¶
mkdir schemaforms-fastapi-demo
cd schemaforms-fastapi-demo
python -m venv .venv
source .venv/bin/activate
2) Install dependencies¶
pip install "pydantic-schemaforms[fastapi]" uvicorn
3) Create main.py¶
Create a file named main.py:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, EmailStr
from pydantic_schemaforms import create_form_from_model, handle_form_async
class User(BaseModel):
name: str
email: EmailStr
app = FastAPI(title="SchemaForms Demo")
@app.api_route("/user", methods=["GET", "POST"], response_class=HTMLResponse)
async def user_form(request: Request):
# Build a builder from your model (choose a framework theme).
builder = create_form_from_model(User, framework="bootstrap")
if request.method == "POST":
# FastAPI form parsing is async.
form = await request.form()
# Validate + render response.
result = await handle_form_async(builder, submitted_data=dict(form))
# On success, result contains parsed/validated data.
if result.get("success"):
return f"Saved: {result['data']}"
# On failure, you typically re-render the form (with errors).
return result["form_html"]
# Initial render.
result = await handle_form_async(builder)
return result["form_html"]
4) Run the server¶
uvicorn main:app --reload
Open http://127.0.0.1:8000/user
Sync vs Async (what’s the difference?)¶
handle_form() (sync)¶
Use handle_form(builder, ...) when your web framework is synchronous (WSGI) and you already have submitted data as a plain dict.
Typical environments:
- Flask / Django (classic request/response)
- CLI apps or scripts that validate a dict
Example (Flask):
from flask import Flask, request
from pydantic import BaseModel, EmailStr
from pydantic_schemaforms import create_form_from_model, handle_form
class User(BaseModel):
name: str
email: EmailStr
app = Flask(__name__)
@app.route("/user", methods=["GET", "POST"])
def user_form():
builder = create_form_from_model(User, framework="bootstrap")
if request.method == "POST":
result = handle_form(builder, submitted_data=request.form.to_dict())
if result.get("success"):
return f"Saved: {result['data']}"
return result["form_html"]
return handle_form(builder)["form_html"]
handle_form_async() (async)¶
Use handle_form_async(builder, ...) when you are in an async runtime (ASGI) and you are already await-ing things (like request.form() in FastAPI/Starlette).
Typical environments:
- FastAPI / Starlette
- Any async stack where you want to keep the request handler non-blocking
Important FastAPI note¶
FastAPI’s Request.form() is async, so the most natural implementation is an async def route and handle_form_async().
If you already have a dict of submitted data (for example from a different parsing path), you can still call handle_form() inside an async def route — but the moment you need to await request parsing, you’ll generally prefer the async handler for consistency.
Next steps¶
- Learn about asset delivery (
asset_mode) indocs/assets.md - See the broader integration pattern in
docs/quickstart.md