Home

Appendix 4: Web APIs, Pydantic, Dash, and Python Decorators

appendix fastapi pydantic dash decorators api crud python

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:

  1. Web APIs with FastAPI — Why we separate frontend and backend, and how they communicate
  2. CRUD Operations — The fundamental database operations and their REST equivalents
  3. Pydantic Models — Data validation that catches errors before they cause problems
  4. Dash as a Frontend — Why Dash is special and how it differs from traditional web frontends
  5. 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:

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:

In your project:

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:

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:

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:

  1. User clicks a dropdown in their browser
  2. Browser sends a request to the Dash server
  3. Your Python callback function runs on the server
  4. Dash sends the result back to the browser
  5. 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
])

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:

  1. Register your function as a callback
  2. Connect inputs (what triggers the callback) to outputs (what gets updated)
  3. 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:

  1. repeat(3) returns a decorator function
  2. 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:

  1. app.get("/profiles") returns a decorator
  2. That decorator:
    • Registers the URL /profiles with HTTP method GET
    • Associates it with the list_profiles function
    • Returns the function unchanged (or slightly modified)

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:

  1. Registers update_graph as a callback
  2. Connects it to the specified inputs and outputs
  3. Dash calls this function whenever dropdown.value changes

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

  1. APIs separate concerns — Frontend handles display, backend handles logic and data
  2. CRUD maps to HTTP methods — POST=Create, GET=Read, PUT=Update, DELETE=Delete
  3. Pydantic validates automatically — Define the rules once, validation happens everywhere
  4. Dash is Python all the way — No JavaScript needed for data-focused web apps
  5. Decorators are just function wrappers — They register, modify, or enhance functions
  6. The @ syntax is syntactic sugar@decorator equals func = 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

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk