Appendix 4: Web APIs, Pydantic, Dash, and Python Decorators
November 2025 (7259 Words, 41 Minutes)
1. Introduction: The Technical Foundations for Your Project
Welcome to Appendix 4! This appendix accompanies the Road Profile Viewer project assignment, where you’ll extend a Dash web application with database persistence and file upload functionality.
Project Repository: road-profile-viewer-class-template (Project 1)
If you’re studying independently, you can follow along by checking out the repository above and reading the README.md for the assignment requirements.
This appendix provides the technical background you need—whether it’s a reminder of concepts from other courses or new material you haven’t encountered yet.
1.1 What This Appendix Covers
By the end of this lecture, you’ll understand:
- Web APIs with FastAPI — Why we separate frontend and backend, and how they communicate
- CRUD Operations — The fundamental database operations and their REST equivalents
- Pydantic Models — Data validation that catches errors before they cause problems
- Dash as a Frontend — Why Dash is special and how it differs from traditional web frontends
- Python Decorators — Demystifying the
@syntax you see everywhere in FastAPI and Dash
1.2 Your Project Context
In your assignment, you’re building a system that looks like this:
┌─────────────────────────────────────────────────────────────────┐
│ User's Browser │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Dash Frontend (Python) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Dropdown │ │ Upload │ │ Plotly Visualization │ │
│ │ Selector │ │ Page │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI Backend (Python) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ GET /profiles│ │POST /profiles│ │ Pydantic Validation │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SQLite Database │
│ (id, name, x_coordinates, y_coordinates) │
└─────────────────────────────────────────────────────────────────┘
Each layer has a specific responsibility:
- Dash handles the user interface and visualization
- FastAPI handles business logic and data validation
- SQLite stores the road profiles persistently
Let’s understand each piece.
2. Understanding Web APIs with FastAPI
2.1 What is an API?
API stands for Application Programming Interface. In the context of web development, an API is a way for different programs to communicate with each other over the network.
Think of it like a restaurant:
- You (the client) don’t go into the kitchen
- You tell the waiter (the API) what you want
- The kitchen (the server) prepares your order
- The waiter brings back the result
In your project:
- Dash is the client (the user’s interface)
- FastAPI is the waiter (the API)
- SQLite is the kitchen (where data is stored and retrieved)
2.2 Why Separate Frontend and Backend?
You might wonder: “Why not just have Dash talk directly to the database?”
Good question! Here are the reasons for separation:
| Concern | Direct Database Access | API Layer |
|---|---|---|
| Security | Database credentials in frontend code | Credentials stay on server |
| Validation | Each frontend duplicates validation | Central validation in one place |
| Flexibility | Changing DB requires changing all frontends | Change DB without touching frontends |
| Testing | Hard to test without a database | API can be tested independently |
| Scaling | Everything scales together | Scale frontend and backend separately |
For your assignment, the separation also earns you the extra point for using the FastAPI approach!
2.3 REST APIs: The Standard Way
REST (Representational State Transfer) is the most common style for web APIs. It uses standard HTTP methods to perform operations on resources (things like road profiles).
The key HTTP methods are:
| HTTP Method | Purpose | Example URL | What It Does |
|---|---|---|---|
GET |
Read data | /profiles |
Get all profiles |
GET |
Read one item | /profiles/1 |
Get profile with id=1 |
POST |
Create new data | /profiles |
Create a new profile |
PUT |
Update existing data | /profiles/1 |
Update profile with id=1 |
DELETE |
Remove data | /profiles/1 |
Delete profile with id=1 |
2.4 Why FastAPI?
FastAPI is a modern Python web framework. Here’s why it’s popular:
1. Type Hints = Automatic Validation
@app.get("/profiles/{profile_id}")
def get_profile(profile_id: int): # FastAPI validates that profile_id is an int
...
If someone requests /profiles/abc, FastAPI automatically returns an error—you don’t write validation code.
2. Automatic Documentation
FastAPI generates interactive API documentation at /docs. You can test your API directly in the browser!
3. Modern Python
FastAPI uses Python 3.6+ features like type hints and async/await, making code cleaner and more maintainable.
4. Performance
FastAPI is one of the fastest Python frameworks, comparable to Node.js and Go.
2.5 Your First FastAPI Application
Here’s a minimal FastAPI app for your project:
# api/main.py
from fastapi import FastAPI
app = FastAPI()
# In-memory storage for now (you'll replace with database)
profiles = [
{"id": 1, "name": "default", "x_coordinates": [0, 10, 20], "y_coordinates": [0, 5, 2]}
]
@app.get("/profiles")
def list_profiles():
"""Return all road profiles."""
return profiles
@app.get("/profiles/{profile_id}")
def get_profile(profile_id: int):
"""Return a specific road profile by ID."""
for profile in profiles:
if profile["id"] == profile_id:
return profile
return {"error": "Profile not found"}
@app.post("/profiles")
def create_profile(profile: dict):
"""Create a new road profile."""
profile["id"] = len(profiles) + 1
profiles.append(profile)
return profile
Running the API:
uvicorn api.main:app --reload
Testing it:
# Get all profiles
curl http://localhost:8000/profiles
# Get profile with id=1
curl http://localhost:8000/profiles/1
# Create a new profile
curl -X POST http://localhost:8000/profiles \
-H "Content-Type: application/json" \
-d '{"name": "mountain_road", "x_coordinates": [0, 10, 20], "y_coordinates": [0, 5, 8]}'
2.6 How Dash Talks to FastAPI
In your Dash application, you’ll use the requests library to communicate with FastAPI:
# In your Dash app
import requests
# Get all profiles for the dropdown
response = requests.get("http://localhost:8000/profiles")
profiles = response.json() # [{"id": 1, "name": "default", ...}, ...]
# Create a new profile from uploaded JSON
new_profile = {
"name": "mountain_road",
"x_coordinates": [0, 10, 20],
"y_coordinates": [0, 5, 8]
}
response = requests.post("http://localhost:8000/profiles", json=new_profile)
created = response.json()
This is the foundation of your project’s architecture!
3. CRUD Operations: A Reminder
3.1 What is CRUD?
CRUD is an acronym for the four basic operations you can perform on data:
- Create — Add new data
- Read — Retrieve existing data
- Update — Modify existing data
- Delete — Remove data
If you’ve taken a database course, you’ve seen these operations in SQL:
| CRUD Operation | SQL Statement | Example |
|---|---|---|
| Create | INSERT |
INSERT INTO profiles (name, x_coords) VALUES ('road1', '[0,10]') |
| Read | SELECT |
SELECT * FROM profiles WHERE id = 1 |
| Update | UPDATE |
UPDATE profiles SET name = 'new_name' WHERE id = 1 |
| Delete | DELETE |
DELETE FROM profiles WHERE id = 1 |
3.2 CRUD in REST APIs
REST APIs map CRUD operations to HTTP methods:
| CRUD | HTTP Method | URL Pattern | Request Body |
|---|---|---|---|
| Create | POST |
/profiles |
New profile data |
| Read (all) | GET |
/profiles |
None |
| Read (one) | GET |
/profiles/{id} |
None |
| Update | PUT |
/profiles/{id} |
Updated profile data |
| Delete | DELETE |
/profiles/{id} |
None |
3.3 CRUD for Road Profiles
For your assignment, here’s exactly what each operation looks like:
Create — Add a new road profile:
POST /profiles
Content-Type: application/json
{
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Response:
{
"id": 2,
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Read — Get all profiles (for dropdown):
GET /profiles
Response:
[
{"id": 1, "name": "default", "x_coordinates": [...], "y_coordinates": [...]},
{"id": 2, "name": "mountain_road", "x_coordinates": [...], "y_coordinates": [...]}
]
Read — Get one profile (for visualization):
GET /profiles/2
Response:
{
"id": 2,
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Update — Rename a profile:
PUT /profiles/2
Content-Type: application/json
{
"name": "alpine_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Delete — Remove a profile:
DELETE /profiles/2
4. Pydantic Models: Data Validation Made Easy
4.1 The Problem: Invalid Data
What happens when someone uploads this JSON?
{
"name": "",
"x_coordinates": [0, 10, 20],
"y_coordinates": [0, 5]
}
There are several problems:
- Empty name (should be 1-100 characters)
- x and y coordinates have different lengths (3 vs 2)
Without validation, this bad data goes into your database and causes errors later.
4.2 What is Pydantic?
Pydantic is a Python library for data validation using type hints. Instead of writing validation code manually, you define a model and Pydantic does the checking for you.
from pydantic import BaseModel
class RoadProfile(BaseModel):
name: str
x_coordinates: list[float]
y_coordinates: list[float]
Now when you create a RoadProfile:
# This works
profile = RoadProfile(
name="mountain",
x_coordinates=[0.0, 10.0, 20.0],
y_coordinates=[0.0, 5.0, 8.0]
)
# This fails with a clear error
profile = RoadProfile(
name=123, # Should be string!
x_coordinates=[0.0, 10.0],
y_coordinates=[0.0, 5.0]
)
# ValidationError: name - Input should be a valid string
4.3 Adding Constraints with Field
For your assignment, names must be 1-100 characters and coordinates need at least 2 points:
from pydantic import BaseModel, Field
from typing import List
class RoadProfile(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
The ... means the field is required (no default value).
Testing validation:
# Empty name - fails!
RoadProfile(name="", x_coordinates=[0, 10], y_coordinates=[0, 5])
# ValidationError: name - String should have at least 1 character
# Only one point - fails!
RoadProfile(name="road", x_coordinates=[0], y_coordinates=[0])
# ValidationError: x_coordinates - List should have at least 2 items
4.4 Custom Validators
Your assignment requires that x and y coordinates have the same length. Pydantic can’t express this with Field alone—you need a custom validator:
from pydantic import BaseModel, Field, model_validator
from typing import List
class RoadProfile(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
@model_validator(mode='after')
def check_coordinate_lengths(self):
if len(self.x_coordinates) != len(self.y_coordinates):
raise ValueError(
f"x_coordinates ({len(self.x_coordinates)} points) and "
f"y_coordinates ({len(self.y_coordinates)} points) must have the same length"
)
return self
Now mismatched lengths fail:
RoadProfile(
name="bad_road",
x_coordinates=[0, 10, 20],
y_coordinates=[0, 5] # Only 2 points!
)
# ValidationError: x_coordinates (3 points) and y_coordinates (2 points) must have the same length
4.5 Pydantic + FastAPI Integration
Here’s the magic: FastAPI uses Pydantic automatically!
from fastapi import FastAPI
from pydantic import BaseModel, Field, model_validator
from typing import List
app = FastAPI()
class RoadProfileCreate(BaseModel):
"""Schema for creating a new road profile."""
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
@model_validator(mode='after')
def check_coordinate_lengths(self):
if len(self.x_coordinates) != len(self.y_coordinates):
raise ValueError("x and y coordinates must have the same length")
return self
class RoadProfileResponse(BaseModel):
"""Schema for road profile responses (includes ID)."""
id: int
name: str
x_coordinates: List[float]
y_coordinates: List[float]
@app.post("/profiles", response_model=RoadProfileResponse)
def create_profile(profile: RoadProfileCreate):
# FastAPI automatically validates 'profile' using Pydantic!
# If validation fails, FastAPI returns a 422 error with details
# Your database logic here...
new_id = save_to_database(profile)
return RoadProfileResponse(
id=new_id,
name=profile.name,
x_coordinates=profile.x_coordinates,
y_coordinates=profile.y_coordinates
)
Invalid request:
curl -X POST http://localhost:8000/profiles \
-H "Content-Type: application/json" \
-d '{"name": "", "x_coordinates": [0], "y_coordinates": [0, 5]}'
Automatic error response (422 Unprocessable Entity):
{
"detail": [
{
"loc": ["body", "name"],
"msg": "String should have at least 1 character",
"type": "string_too_short"
},
{
"loc": ["body", "x_coordinates"],
"msg": "List should have at least 2 items",
"type": "too_short"
}
]
}
4.6 Beyond the Exercise: More Pydantic Features
Pydantic can do much more. Here are some patterns you might find useful:
Optional Fields with Defaults:
from typing import Optional
class RoadProfile(BaseModel):
name: str
description: Optional[str] = None # Optional, defaults to None
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
Nested Models:
class Coordinates(BaseModel):
x: List[float]
y: List[float]
class RoadProfile(BaseModel):
name: str
coordinates: Coordinates # Nested model
Field Aliases (for JSON with different key names):
class RoadProfile(BaseModel):
name: str
x_coords: List[float] = Field(..., alias="x_coordinates")
class Config:
populate_by_name = True # Accept both 'x_coords' and 'x_coordinates'
Serialization to JSON:
profile = RoadProfile(name="test", x_coordinates=[0, 10], y_coordinates=[0, 5])
# Convert to dictionary
profile.model_dump()
# {'name': 'test', 'x_coordinates': [0.0, 10.0], 'y_coordinates': [0.0, 5.0]}
# Convert to JSON string
profile.model_dump_json()
# '{"name":"test","x_coordinates":[0.0,10.0],"y_coordinates":[0.0,5.0]}'
5. Dash as a Python Frontend
5.1 What Makes Dash Special?
Dash is different from traditional web frontends:
| Aspect | Traditional Frontend (React, Vue) | Dash |
|---|---|---|
| Language | JavaScript/TypeScript | Python |
| Where code runs | In the browser | On the server (mostly) |
| Learning curve | HTML + CSS + JS + Framework | Python + Dash components |
| Best for | Complex interactive apps | Data apps & dashboards |
| Visualization | Separate libraries (D3, Chart.js) | Plotly built-in |
For data scientists and engineers who know Python, Dash lets you build web apps without learning JavaScript.
5.2 The Hidden Backend
Here’s the key insight: Dash runs a Flask server behind the scenes.
When a user interacts with your Dash app:
- User clicks a dropdown in their browser
- Browser sends a request to the Dash server
- Your Python callback function runs on the server
- Dash sends the result back to the browser
- Browser updates the display
┌──────────────────┐ ┌──────────────────┐
│ User's Browser │ ──────► │ Dash Server │
│ │ │ (Python) │
│ [Dropdown ▼] │ │ │
│ │ ◄────── │ callback runs │
│ [Updated Graph] │ │ here │
└──────────────────┘ └──────────────────┘
This is why Dash is “special”—it’s really a full-stack framework that handles both frontend and backend.
5.3 Dash Components: Building Blocks
Dash provides pre-built components that translate to HTML:
from dash import Dash, html, dcc
app = Dash(__name__)
app.layout = html.Div([
html.H1("Road Profile Viewer"), # <h1>Road Profile Viewer</h1>
dcc.Dropdown( # <select>...</select>
id="profile-dropdown",
options=[
{"label": "Default", "value": 1},
{"label": "Mountain", "value": 2}
],
value=1
),
dcc.Graph(id="profile-graph") # <div> with Plotly chart
])
html.*— Standard HTML elements (div, h1, p, etc.)dcc.*— Dash Core Components (interactive elements like dropdowns, graphs, inputs)
5.4 Plotly Integration
Dash is built by Plotly, so charts integrate seamlessly:
import plotly.graph_objects as go
from dash import Dash, dcc, html, callback, Output, Input
app = Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(
id="profile-dropdown",
options=[{"label": "Default", "value": 1}],
value=1
),
dcc.Graph(id="profile-graph")
])
@callback(
Output("profile-graph", "figure"),
Input("profile-dropdown", "value")
)
def update_graph(profile_id):
# Fetch profile data (from API in your project)
x = [0, 10, 20, 30]
y = [0, 5, 8, 3]
# Create Plotly figure
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Road Profile'))
fig.update_layout(title="Road Profile", xaxis_title="Distance", yaxis_title="Elevation")
return fig
if __name__ == "__main__":
app.run(debug=True)
5.5 Multi-Page Dash Apps
Your assignment requires an upload page at /upload. Dash supports multi-page apps:
pages/
├── home.py # Main visualization page
└── upload.py # Upload page
# pages/home.py
from dash import html, dcc, register_page
register_page(__name__, path="/")
layout = html.Div([
html.H1("Road Profile Viewer"),
dcc.Dropdown(id="profile-dropdown"),
dcc.Graph(id="profile-graph"),
dcc.Link("Go to Upload", href="/upload")
])
# pages/upload.py
from dash import html, dcc, register_page
register_page(__name__, path="/upload")
layout = html.Div([
html.H1("Upload Road Profile"),
dcc.Upload(
id="upload-data",
children=html.Div(["Drag and Drop or ", html.A("Select Files")]),
),
html.Div(id="preview-container"),
html.Button("Save Profile", id="save-button"),
dcc.Link("Back to Viewer", href="/")
])
# app.py
from dash import Dash, page_container
app = Dash(__name__, use_pages=True)
app.layout = page_container
if __name__ == "__main__":
app.run(debug=True)
5.6 Why Dash Needs Decorators
You’ve probably noticed this pattern in Dash:
@callback(
Output("profile-graph", "figure"),
Input("profile-dropdown", "value")
)
def update_graph(selected_profile):
...
What is @callback? It’s a decorator. Dash uses decorators to:
- Register your function as a callback
- Connect inputs (what triggers the callback) to outputs (what gets updated)
- Handle all the network communication automatically
Let’s demystify decorators in the next section.
6. Python Decorators Demystified
6.1 The Mystery
If you’ve used Dash or FastAPI, you’ve seen this pattern:
@app.get("/profiles")
def list_profiles():
return profiles
or
@callback(Output("graph", "figure"), Input("dropdown", "value"))
def update_graph(value):
...
What is the @ doing? Why is there code above the function definition?
6.2 Functions as First-Class Objects
In Python, functions are objects like any other. You can:
Assign them to variables:
def greet(name):
return f"Hello, {name}!"
say_hello = greet # No parentheses = reference to function, not calling it
print(say_hello("Alice")) # "Hello, Alice!"
Pass them to other functions:
def apply_twice(func, value):
return func(func(value))
def add_one(x):
return x + 1
result = apply_twice(add_one, 5) # add_one(add_one(5)) = 7
Return them from functions:
def create_multiplier(factor):
def multiply(x):
return x * factor
return multiply # Return the inner function
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
This last pattern—a function that returns a function—is the key to understanding decorators.
6.3 Building a Decorator Step by Step
Let’s say we want to log every time a function is called. Without decorators:
def greet(name):
print(f"greet was called with: {name}") # Manual logging
return f"Hello, {name}!"
def farewell(name):
print(f"farewell was called with: {name}") # Duplicate!
return f"Goodbye, {name}!"
This is tedious and error-prone. Instead, let’s create a decorator:
Step 1: A function that wraps another function
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} was called with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
Step 2: Apply the wrapper manually
def greet(name):
return f"Hello, {name}!"
greet = log_calls(greet) # Replace greet with wrapped version
greet("Alice")
# Output:
# greet was called with args=('Alice',), kwargs={}
# greet returned: Hello, Alice!
Step 3: Use @ syntax (syntactic sugar)
The @decorator syntax is just a shorthand:
@log_calls
def greet(name):
return f"Hello, {name}!"
# This is EXACTLY equivalent to:
# def greet(name):
# return f"Hello, {name}!"
# greet = log_calls(greet)
That’s all a decorator is! It’s a function that takes a function and returns a modified version.
6.4 Decorators with Arguments
FastAPI uses decorators like @app.get("/profiles"). How does that work?
The trick is nesting: a function that returns a decorator.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
# Output:
# Hello!
# Hello!
# Hello!
The chain is:
repeat(3)returns a decorator function- That decorator is applied to
say_hello
6.5 How FastAPI Decorators Work
Now you can understand FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.get("/profiles")
def list_profiles():
return [{"id": 1, "name": "default"}]
What happens:
app.get("/profiles")returns a decorator- That decorator:
- Registers the URL
/profileswith HTTP methodGET - Associates it with the
list_profilesfunction - Returns the function unchanged (or slightly modified)
- Registers the URL
Here’s a simplified version of what FastAPI does internally:
class SimpleFastAPI:
def __init__(self):
self.routes = {}
def get(self, path):
def decorator(func):
self.routes[("GET", path)] = func
return func
return decorator
def post(self, path):
def decorator(func):
self.routes[("POST", path)] = func
return func
return decorator
app = SimpleFastAPI()
@app.get("/profiles")
def list_profiles():
return [{"id": 1}]
@app.post("/profiles")
def create_profile(data):
return {"id": 2, **data}
# Now app.routes contains:
# {
# ("GET", "/profiles"): list_profiles,
# ("POST", "/profiles"): create_profile
# }
6.6 How Dash Decorators Work
Dash’s @callback works similarly:
@callback(
Output("graph", "figure"),
Input("dropdown", "value")
)
def update_graph(value):
...
The decorator:
- Registers
update_graphas a callback - Connects it to the specified inputs and outputs
- Dash calls this function whenever
dropdown.valuechanges
Here’s a simplified version:
class SimpleDash:
def __init__(self):
self.callbacks = []
def callback(self, output, *inputs):
def decorator(func):
self.callbacks.append({
"function": func,
"output": output,
"inputs": inputs
})
return func
return decorator
app = SimpleDash()
@app.callback(Output("graph", "figure"), Input("dropdown", "value"))
def update_graph(value):
return create_figure(value)
# Now app.callbacks contains information about when to call update_graph
6.7 Common Built-in Decorators
Python has several useful built-in decorators:
@property — Turn a method into a read-only attribute:
class RoadProfile:
def __init__(self, x_coords, y_coords):
self._x = x_coords
self._y = y_coords
@property
def num_points(self):
return len(self._x)
profile = RoadProfile([0, 10, 20], [0, 5, 8])
print(profile.num_points) # 3 (no parentheses needed!)
@staticmethod — A method that doesn’t need self:
class MathUtils:
@staticmethod
def add(a, b):
return a + b
MathUtils.add(2, 3) # 5 (no instance needed)
@classmethod — A method that receives the class, not the instance:
class RoadProfile:
def __init__(self, name, x, y):
self.name = name
self.x = x
self.y = y
@classmethod
def from_json(cls, json_data):
return cls(
json_data["name"],
json_data["x_coordinates"],
json_data["y_coordinates"]
)
data = {"name": "test", "x_coordinates": [0, 10], "y_coordinates": [0, 5]}
profile = RoadProfile.from_json(data)
6.8 Practical Example: Logging Decorator for API Debugging
Here’s a useful decorator for debugging your FastAPI endpoints:
import time
from functools import wraps
def log_endpoint(func):
@wraps(func) # Preserves function name and docstring
def wrapper(*args, **kwargs):
start = time.time()
print(f"→ {func.__name__} called")
try:
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"← {func.__name__} returned in {elapsed:.3f}s")
return result
except Exception as e:
print(f"✗ {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
# Usage in FastAPI
@app.get("/profiles")
@log_endpoint
def list_profiles():
return profiles
# When called:
# → list_profiles called
# ← list_profiles returned in 0.001s
Note: The order of decorators matters! They’re applied bottom-up:
@app.get("/profiles") # Applied second
@log_endpoint # Applied first
def list_profiles():
...
# Equivalent to:
# list_profiles = app.get("/profiles")(log_endpoint(list_profiles))
7. Putting It All Together
7.1 Architecture Overview
Here’s how all the pieces fit together in your project:
┌─────────────────────────────────────────────────────────────────────────┐
│ User's Browser │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Dash-generated HTML/JS │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Dropdown │ │ Upload Form │ │ Plotly Graph │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
HTTP requests │ HTTP responses
(JSON data) ▼ (JSON data)
┌─────────────────────────────────────────────────────────────────────────┐
│ Dash Server (Python) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ @callback functions │ │
│ │ │ │
│ │ @callback(Output("graph"), Input("dropdown")) │ │
│ │ def update_graph(profile_id): │ │
│ │ profile = requests.get(f"{API_URL}/profiles/{profile_id}") │ │
│ │ return create_figure(profile.json()) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
HTTP requests │ HTTP responses
(JSON data) ▼ (JSON data)
┌─────────────────────────────────────────────────────────────────────────┐
│ FastAPI Server (Python) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ @app.get, @app.post routes │ │
│ │ │ │
│ │ @app.post("/profiles") │ │
│ │ def create_profile(profile: RoadProfileCreate): ← Pydantic! │ │
│ │ # Validation happens automatically │ │
│ │ return db.create(profile) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
SQL queries │ Query results
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SQLite Database │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ profiles table: │ │
│ │ ┌────┬───────────────┬────────────────────┬───────────────────┐ │ │
│ │ │ id │ name │ x_coordinates │ y_coordinates │ │ │
│ │ ├────┼───────────────┼────────────────────┼───────────────────┤ │ │
│ │ │ 1 │ default │ [0, 10, 20, 30] │ [0, 5, 8, 3] │ │ │
│ │ │ 2 │ mountain_road │ [0, 10, 20, 30, 40]│ [0, 2, 5, 8, 6] │ │ │
│ │ └────┴───────────────┴────────────────────┴───────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
7.2 Data Flow: Upload to Visualization
Let’s trace what happens when a user uploads a new road profile:
Step 1: User uploads JSON file in Dash
# Dash callback triggered by file upload
@callback(Output("preview-graph", "figure"), Input("upload-data", "contents"))
def preview_upload(contents):
if contents is None:
return {}
# Decode base64 content from upload
content_string = contents.split(",")[1]
decoded = base64.b64decode(content_string)
data = json.loads(decoded)
# Show preview graph
return create_figure(data["x_coordinates"], data["y_coordinates"])
Step 2: User clicks “Save” → Dash sends to FastAPI
@callback(Output("save-message", "children"), Input("save-button", "n_clicks"), State("upload-data", "contents"))
def save_profile(n_clicks, contents):
if n_clicks is None:
return ""
# Parse uploaded data
data = parse_upload(contents)
# Send to FastAPI
response = requests.post(f"{API_URL}/profiles", json=data)
if response.status_code == 200:
return "Profile saved successfully!"
else:
return f"Error: {response.json()['detail']}"
Step 3: FastAPI validates with Pydantic and saves
@app.post("/profiles", response_model=RoadProfileResponse)
def create_profile(profile: RoadProfileCreate):
# Pydantic already validated:
# - name is 1-100 chars
# - x and y have at least 2 points
# - x and y have same length
# Save to database
db_profile = db.create_profile(profile)
return db_profile
Step 4: Dropdown updates, user can view new profile
@callback(Output("profile-dropdown", "options"), Input("save-button", "n_clicks"))
def refresh_dropdown(_):
# Fetch updated list from API
response = requests.get(f"{API_URL}/profiles")
profiles = response.json()
return [{"label": p["name"], "value": p["id"]} for p in profiles]
7.3 Key Takeaways
- APIs separate concerns — Frontend handles display, backend handles logic and data
- CRUD maps to HTTP methods — POST=Create, GET=Read, PUT=Update, DELETE=Delete
- Pydantic validates automatically — Define the rules once, validation happens everywhere
- Dash is Python all the way — No JavaScript needed for data-focused web apps
- Decorators are just function wrappers — They register, modify, or enhance functions
- The @ syntax is syntactic sugar —
@decoratorequalsfunc = decorator(func)
8. Quick Reference
8.1 HTTP Methods → CRUD
| CRUD | HTTP | SQL | FastAPI Decorator |
|---|---|---|---|
| Create | POST | INSERT | @app.post("/path") |
| Read | GET | SELECT | @app.get("/path") |
| Update | PUT | UPDATE | @app.put("/path") |
| Delete | DELETE | DELETE | @app.delete("/path") |
8.2 Pydantic Validators
from pydantic import BaseModel, Field, field_validator, model_validator
class Example(BaseModel):
# Basic constraints
name: str = Field(..., min_length=1, max_length=100)
count: int = Field(..., ge=0, le=1000) # >= 0, <= 1000
items: List[str] = Field(..., min_length=1)
# Field-level validation
@field_validator('name')
@classmethod
def name_must_not_contain_spaces(cls, v):
if ' ' in v:
raise ValueError('must not contain spaces')
return v
# Model-level validation (after all fields)
@model_validator(mode='after')
def check_something(self):
# Access self.name, self.count, etc.
return self
8.3 Decorator Patterns
# Simple decorator (no arguments)
def my_decorator(func):
def wrapper(*args, **kwargs):
# Before
result = func(*args, **kwargs)
# After
return result
return wrapper
@my_decorator
def my_function():
pass
# Decorator with arguments
def my_decorator(arg1, arg2):
def decorator(func):
def wrapper(*args, **kwargs):
# Use arg1, arg2 here
return func(*args, **kwargs)
return wrapper
return decorator
@my_decorator("value1", "value2")
def my_function():
pass
9. Further Reading
- FastAPI Documentation: https://fastapi.tiangolo.com/
- Pydantic Documentation: https://docs.pydantic.dev/
- Dash Documentation: https://dash.plotly.com/
- Python Decorators Tutorial: https://realpython.com/primer-on-python-decorators/
- REST API Design: https://restfulapi.net/