<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ FastAPI - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ FastAPI - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 06 Jun 2026 11:17:17 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/fastapi/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an End-to-End ML Platform Locally: From Experiment Tracking to CI/CD ]]>
                </title>
                <description>
                    <![CDATA[ Machine learning projects don’t end at training a model in a Jupyter notebook. The hard part is the “last mile”: turning that notebook model into something you can run reliably, update safely, and tru ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-end-to-end-ml-platform-locally-from-experiment-tracking-to-cicd/</link>
                <guid isPermaLink="false">69b9bab4c22d3eeb8afd5284</guid>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mlops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Platform Engineering  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sandeep Bharadwaj Mannapur ]]>
                </dc:creator>
                <pubDate>Tue, 17 Mar 2026 20:33:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8401d978-0bed-4534-af93-f6bfc1b77c89.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Machine learning projects don’t end at training a model in a Jupyter notebook. The hard part is the “last mile”: turning that notebook model into something you can run reliably, update safely, and trust over time.</p>
<p>Most ML systems fail in production for boring (and painful) reasons: the training code and the serving code drift apart, input data changes shape, a “small” preprocessing tweak breaks predictions, or the model silently degrades because real-world behavior shifts. None of these problems are solved by a better algorithm, they’re solved by engineering: repeatable pipelines, validation, versioning, monitoring, and automated checks.</p>
<p>In this hands-on handbook, you’ll build a complete mini ML platform on your local machine, an end-to-end project that takes a model from training to deployment with the core “last mile” infrastructure in place. We’ll use a fraud detection example (predicting fraudulent transactions), but the same workflow works for churn prediction or any binary classification problem. Everything runs locally (no cloud required), and every step is copy-paste runnable so you can follow along and verify outputs as you go.</p>
<p>By the end, you'll have a production-ready ML pipeline running on your machine – from training the model to serving predictions, with the infrastructure to test, monitor, and iterate with confidence. And yes, we'll do it in a hands-on manner with code snippets you can copy-paste and run. Let's dive in!</p>
<p>📦 <strong>Get the Complete Code</strong><br>All code from this handbook is available in a ready-to-run repository:<br><strong>Repository:</strong> <a href="https://github.com/sandeepmb/freecodecamp-local-ml-platform">https://github.com/sandeepmb/freecodecamp-local-ml-platform</a><br>Clone it and follow along, or use it as a reference implementation.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-project-overview-and-setup">Project Overview and Setup</a></p>
</li>
<li><p><a href="#heading-1-build-a-simple-model-and-api-the-naive-approach">Build a Simple Model and API (The Naive Approach)</a></p>
<ul>
<li><p><a href="#heading-11-train-a-quick-model">Train a Quick Model</a></p>
</li>
<li><p><a href="#heading-12-serve-predictions-with-fastapi">Serve Predictions with FastAPI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-2-where-the-naive-approach-breaks">Where the Naive Approach Breaks</a></p>
<ul>
<li><p><a href="#heading-problem-1-no-experiment-tracking-reproducibility">Problem 1: No Experiment Tracking (Reproducibility)</a></p>
</li>
<li><p><a href="#heading-problem-2-model-versioning-and-deployment-chaos">Problem 2: Model Versioning and Deployment Chaos</a></p>
</li>
<li><p><a href="#heading-problem-3-no-data-validation-garbage-in-garbage-out">Problem 3: No Data Validation – Garbage In, Garbage Out</a></p>
</li>
<li><p><a href="#heading-problem-4-model-drift-performance-decay-over-time">Problem 4: Model Drift – Performance Decay Over Time</a></p>
</li>
<li><p><a href="#heading-problem-5-no-ci-cd-or-deployment-safety">Problem 5: No CI/CD or Deployment Safety</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-3-add-experiment-tracking-and-model-registry-with-mlflow">Add Experiment Tracking and Model Registry with MLflow</a></p>
<ul>
<li><p><a href="#heading-31-how-to-set-up-the-mlflow-tracking-server">How to Set Up the MLflow Tracking Server</a></p>
</li>
<li><p><a href="#heading-32-how-to-log-experiments-in-code">How to Log Experiments in Code</a></p>
</li>
<li><p><a href="#heading-33-how-to-use-the-model-registry">How to Use the Model Registry</a></p>
</li>
<li><p><a href="#heading-34-update-api-to-load-from-registry">Update API to Load from Registry</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-4-ensure-feature-consistency-with-feast">Ensure Feature Consistency with Feast</a></p>
<ul>
<li><p><a href="#heading-41-what-is-feast-and-why-use-it">What Is Feast and Why Use It?</a></p>
</li>
<li><p><a href="#heading-42-install-and-initialize-feast">Install and Initialize Feast</a></p>
</li>
<li><p><a href="#heading-43-define-feature-definitions">Define Feature Definitions</a></p>
</li>
<li><p><a href="#heading-44-materialize-features-to-the-online-store">Materialize Features to the Online Store</a></p>
</li>
<li><p><a href="#heading-45-retrieve-features-for-training-and-serving">Retrieve Features for Training and Serving</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-5-add-data-validation-with-great-expectations">Add Data Validation with Great Expectations</a></p>
<ul>
<li><p><a href="#heading-51-define-expectations">Define Expectations</a></p>
</li>
<li><p><a href="#heading-52-integrate-validation-into-fastapi">Integrate Validation into FastAPI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-6-monitor-model-performance-and-data-drift">Monitor Model Performance and Data Drift</a></p>
<ul>
<li><p><a href="#heading-61-the-four-pillars-of-ml-observability">The Four Pillars of ML Observability</a></p>
</li>
<li><p><a href="#heading-62-build-a-drift-monitor-with-evidently">Build a Drift Monitor with Evidently</a></p>
</li>
<li><p><a href="#heading-63-production-monitoring-strategy">Production Monitoring Strategy</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-7-automate-testing-and-deployment-with-ci-cd">Automate Testing and Deployment with CI/CD</a></p>
<ul>
<li><p><a href="#heading-71-write-tests-for-data-and-model">Write Tests for Data and Model</a></p>
</li>
<li><p><a href="#heading-72-github-actions-workflow">GitHub Actions Workflow</a></p>
</li>
<li><p><a href="#heading-73-dockerize-the-application">Dockerize the Application</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-8-incident-response-playbook">Incident Response Playbook</a></p>
<ul>
<li><p><a href="#heading-scenario-false-positive-spike">Scenario: False Positive Spike</a></p>
</li>
<li><p><a href="#heading-scenario-gradual-performance-decay">Scenario: Gradual Performance Decay</a></p>
</li>
<li><p><a href="#heading-scenario-upstream-data-schema-change">Scenario: Upstream Data Schema Change</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-9-how-to-put-it-all-together">How to Put It All Together</a></p>
</li>
<li><p><a href="#heading-10-whats-next-scale-to-production">What’s Next: Scale to Production</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-project-overview-and-setup"><strong>Project Overview and Setup</strong></h2>
<p>Before we jump into coding, let's set the stage. Our use-case is <strong>credit card fraud detection</strong> – a binary classification problem where we predict whether a transaction is fraudulent (<code>is_fraud = 1</code>) or legitimate (<code>is_fraud = 0</code>). This is a common ML task and a good proxy for production ML challenges because fraud patterns can change over time (allowing us to discuss model drift), and bad input data (for example, malformed transaction info) can cause serious issues if not handled properly.</p>
<h3 id="heading-tech-stack"><strong>Tech Stack</strong></h3>
<p>We will use Python-based tools that are popular in MLOps but still beginner-friendly:</p>
<table>
<thead>
<tr>
<th><strong>Tool</strong></th>
<th><strong>Purpose</strong></th>
<th><strong>Why We Chose It</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>MLflow</strong></td>
<td>Experiment tracking and model registry</td>
<td>Open-source, widely adopted, great UI</td>
</tr>
<tr>
<td><strong>Feast</strong></td>
<td>Feature store for consistent feature serving</td>
<td>Production-grade, runs locally, same API for offline/online</td>
</tr>
<tr>
<td><strong>FastAPI</strong></td>
<td>High-performance web framework for serving predictions</td>
<td>Fast, automatic docs, modern Python</td>
</tr>
<tr>
<td><strong>Great Expectations</strong></td>
<td>Data validation framework</td>
<td>Declarative expectations, great reports</td>
</tr>
<tr>
<td><strong>Evidently</strong></td>
<td>Monitoring for data drift and model decay</td>
<td>Beautiful reports, easy to integrate</td>
</tr>
<tr>
<td><strong>Docker</strong></td>
<td>Containerization for environment consistency</td>
<td>Industry standard, works everywhere</td>
</tr>
<tr>
<td><strong>GitHub Actions</strong></td>
<td>CI/CD automation</td>
<td>Free for public repos, tight GitHub integration</td>
</tr>
</tbody></table>
<p>Let me explain each tool briefly:</p>
<p><strong>MLflow</strong> is an open-source platform designed to manage the ML lifecycle. It provides experiment tracking (logging parameters, metrics, and artifacts), a model registry (versioning models with aliases), and model serving capabilities. We'll use it to ensure our experiments are reproducible and our models are versioned.</p>
<p><strong>Feast</strong> (Feature Store) is an open-source feature store that helps manage and serve features consistently between training and inference. This prevents a common problem called "training-serving skew" where the features used in production differ slightly from those used in training, causing silent accuracy degradation.</p>
<p><strong>FastAPI</strong> is a modern, fast web framework for building APIs with Python. It's known for being easy to use, efficient, and producing automatic interactive documentation. We'll use it to serve our model predictions.</p>
<p><strong>Great Expectations</strong> is an open-source tool for data quality testing. It allows us to define "expectations" on data (like "amount should be positive" or "hour should be between 0 and 23") and test incoming data against them.</p>
<p><strong>Evidently</strong> is an open-source library for monitoring data and model performance over time. It can detect data drift (when input distributions change) and model decay (when accuracy drops).</p>
<p><strong>Docker</strong> ensures the same environment and dependencies in development and deployment, avoiding the classic "works on my machine" problem.</p>
<p><strong>GitHub Actions</strong> provides CI/CD automation. An efficient CI/CD pipeline helps integrate and deploy changes faster and with fewer errors.</p>
<p>💡 <strong>Mental Model</strong>: Think of this as building a "safety net" around your ML model. Each tool we add catches a different failure mode, like defensive driving for machine learning.</p>
<h3 id="heading-prerequisites"><strong>Prerequisites</strong></h3>
<p>You'll need:</p>
<ul>
<li><p><strong>Python 3.9+</strong> installed on your machine</p>
</li>
<li><p><strong>Docker Desktop</strong> installed and running</p>
</li>
<li><p><strong>GitHub account</strong> (if you want to try the CI/CD pipeline)</p>
</li>
<li><p><strong>Basic familiarity with Python</strong> and ML concepts (what training and prediction mean)</p>
</li>
</ul>
<p>You don't need MLOps or Kubernetes experience. Everything will be done locally with just Python and Docker – <strong>no cloud and no Kubernetes needed</strong>.</p>
<h3 id="heading-project-structure"><strong>Project Structure</strong></h3>
<p>Let's set up a basic project structure on your local machine. Open your terminal and run:</p>
<pre><code class="language-python"># Create project directory and subfolders
mkdir ml-platform-tutorial &amp;&amp; cd ml-platform-tutorial
mkdir -p data models src tests feature_repo

# Set up a virtual environment (recommended)
python -m venv venv
source venv/bin/activate   # On Windows: venv\Scripts\activate
</code></pre>
<p>Your project structure should look like this:</p>
<pre><code class="language-python">ml-platform-tutorial/
├── data/              # Training and test datasets
├── models/            # Saved model files
├── src/               # Source code
├── tests/             # Test files
├── feature_repo/      # Feast feature repository
├── venv/              # Virtual environment
└── requirements.txt   # Dependencies
</code></pre>
<p>Next, create a <code>requirements.txt</code> with all the necessary libraries:</p>
<pre><code class="language-python"># requirements.txt

# Core ML libraries
pandas==2.2.0
numpy==1.26.3
scikit-learn==1.4.0

# Experiment tracking and model registry
mlflow==2.10.0

# Feature store
feast==0.36.0

# API framework
fastapi==0.109.0
uvicorn==0.27.0
httpx==0.26.0

# Data validation
great-expectations==0.18.8

# Monitoring
evidently==0.7.20

# Testing
pytest==8.0.0
pytest-cov==4.1.0

# Utilities
pyarrow==15.0.0
pydantic==2.6.0
</code></pre>
<p>📌 <strong>Version Note:</strong> Exact versions are pinned to ensure reproducibility. Newer versions may work, but all examples were tested with the versions listed here.</p>
<p>Install the dependencies:</p>
<pre><code class="language-python">pip install -r requirements.txt
</code></pre>
<p>This might take a few minutes as it installs all the packages. Once complete, we're ready to start building our project step by step.</p>
<p><strong>Checkpoint:</strong> You should have a project folder with <code>data/</code>, <code>models/</code>, <code>src/</code>, <code>tests/</code>, and <code>feature_repo/</code> directories, and an activated virtual environment with all dependencies installed. Verify by running <code>python -c "import mlflow; import feast; import fastapi; print('All imports successful!')"</code>.</p>
<p><strong>Figure 1: The Complete ML Platform We'll Build</strong></p>
<p><em>Don't worry if this looks complex, we'll build each component step by step, starting with the simplest piece and connecting them together.</em></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771392341567/4bfdd727-32fb-4f30-a63e-c94f61a9f2db.png" alt="Architecture diagram of a local end-to-end machine learning platform for fraud detection. Transaction data flows through model training, experiment tracking and model registry in MLflow, feature management in Feast, data validation with Great Expectations, prediction serving through FastAPI, monitoring with Evidently, and automated testing and deployment with Docker and GitHub Actions." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-1-build-a-simple-model-and-api-the-naive-approach"><strong>1. Build a Simple Model and API (The Naive Approach)</strong></h2>
<p>To illustrate why we need all these tools, let's start by building a <strong>naive ML system without any MLOps infrastructure</strong>. We'll train a simple model and deploy it quickly, then observe what problems arise. This "naive approach" is how most ML projects start – and understanding its limitations will motivate the solutions we implement later.</p>
<h3 id="heading-11-train-a-quick-model"><strong>1.1 Train a Quick Model</strong></h3>
<p>First, we need some data. For simplicity, we'll generate a synthetic dataset for fraud detection so that we don't rely on any external data files. The dataset will have features like:</p>
<ul>
<li><p><code>amount</code>: Transaction amount in dollars</p>
</li>
<li><p><code>hour</code>: Hour of the day (0-23) when the transaction occurred</p>
</li>
<li><p><code>day_of_week</code>: Day of the week (0=Monday, 6=Sunday)</p>
</li>
<li><p><code>merchant_category</code>: Type of merchant (grocery, restaurant, retail, online, travel)</p>
</li>
<li><p><code>is_fraud</code>: Label indicating if the transaction is fraudulent (1) or legitimate (0)</p>
</li>
</ul>
<p>We will simulate that only ~2% of transactions are fraud, which is an imbalance typical in real fraud data. This imbalance is important because it affects how we evaluate our model.</p>
<p>Create <code>src/generate_data.py</code>:</p>
<pre><code class="language-python"># src/generate_data.py
"""
Generate synthetic fraud detection dataset.

This script creates realistic-looking transaction data where fraudulent
transactions have different patterns than legitimate ones:
- Fraud tends to have higher amounts
- Fraud tends to occur late at night
- Fraud is more common for online and travel merchants
"""
import pandas as pd
import numpy as np

def generate_transactions(n_samples=10000, fraud_ratio=0.02, seed=42):
    """
    Generate synthetic fraud detection dataset.
    
    Args:
        n_samples: Total number of transactions to generate
        fraud_ratio: Proportion of fraudulent transactions (default 2%)
        seed: Random seed for reproducibility
    
    Returns:
        DataFrame with transaction features and fraud labels
    
    Fraud transactions have different patterns:
    - Higher amounts (mean \(245 vs \)33 for legit)
    - Late night hours (0-5, 23)
    - More likely to be online or travel merchants
    """
    np.random.seed(seed)
    n_fraud = int(n_samples * fraud_ratio)
    n_legit = n_samples - n_fraud

    # Legitimate transactions: normal shopping patterns
    # - Amounts follow a log-normal distribution (most small, some large)
    # - Hours are uniformly distributed throughout the day
    # - Merchant categories weighted toward everyday shopping
    legit = pd.DataFrame({
        "amount": np.random.lognormal(mean=3.5, sigma=1.2, size=n_legit),  # ~$33 average
        "hour": np.random.randint(0, 24, size=n_legit),
        "day_of_week": np.random.randint(0, 7, size=n_legit),
        "merchant_category": np.random.choice(
            ["grocery", "restaurant", "retail", "online", "travel"],
            size=n_legit,
            p=[0.30, 0.25, 0.25, 0.15, 0.05]  # Weighted toward everyday shopping
        ),
        "is_fraud": 0
    })
    
    # Fraudulent transactions: suspicious patterns
    # - Higher amounts (fraudsters go big)
    # - Late night hours (less scrutiny)
    # - More online and travel (easier to exploit)
    fraud = pd.DataFrame({
        "amount": np.random.lognormal(mean=5.5, sigma=1.5, size=n_fraud),  # ~$245 average
        "hour": np.random.choice([0, 1, 2, 3, 4, 5, 23], size=n_fraud),  # Late night
        "day_of_week": np.random.randint(0, 7, size=n_fraud),
        "merchant_category": np.random.choice(
            ["grocery", "restaurant", "retail", "online", "travel"],
            size=n_fraud,
            p=[0.05, 0.05, 0.10, 0.60, 0.20]  # Weighted toward online/travel
        ),
        "is_fraud": 1
    })
    
    # Combine and shuffle
    df = pd.concat([legit, fraud], ignore_index=True)
    df = df.sample(frac=1, random_state=seed).reset_index(drop=True)
    
    return df

if __name__ == "__main__":
    # Generate dataset
    print("Generating synthetic fraud detection dataset...")
    df = generate_transactions(n_samples=10000, fraud_ratio=0.02)
    
    # Split into train (80%) and test (20%)
    train_df = df.sample(frac=0.8, random_state=42)
    test_df = df.drop(train_df.index)
    
    # Save to CSV files
    train_df.to_csv("data/train.csv", index=False)
    test_df.to_csv("data/test.csv", index=False)
    
    # Print summary statistics
    print(f"\nDataset generated successfully!")
    print(f"Training set: {len(train_df):,} transactions")
    print(f"Test set: {len(test_df):,} transactions")
    print(f"Overall fraud ratio: {df['is_fraud'].mean():.2%}")
    print(f"\nLegitimate transactions - Average amount: ${df[df['is_fraud']==0]['amount'].mean():.2f}")
    print(f"Fraudulent transactions - Average amount: ${df[df['is_fraud']==1]['amount'].mean():.2f}")
    print(f"\nMerchant category distribution (fraud):")
    print(df[df['is_fraud']==1]['merchant_category'].value_counts(normalize=True))
</code></pre>
<p>Run the data generation script:</p>
<pre><code class="language-python">python src/generate_data.py
</code></pre>
<p>You should see output like:</p>
<pre><code class="language-python">Generating synthetic fraud detection dataset...

Dataset generated successfully!
Training set: 8,000 transactions
Test set: 2,000 transactions
Overall fraud ratio: 2.00%

Legitimate transactions - Average amount: $33.45
Fraudulent transactions - Average amount: $245.67

Merchant category distribution (fraud):
online        0.60
travel        0.20
retail        0.10
restaurant    0.05
grocery       0.05
</code></pre>
<p>Now you have <code>data/train.csv</code> and <code>data/test.csv</code> with ~8000 training and ~2000 testing transactions.</p>
<p><strong>Why This Matters:</strong> The synthetic data has realistic patterns — fraud is rare (2%), high-value, late-night, and concentrated in certain merchant categories. These patterns give our model something to learn.</p>
<p>Now, let's train a quick model. We'll use a simple <strong>Random Forest classifier</strong> from scikit-learn to predict <code>is_fraud</code>. In this naive version, we won't do much feature engineering – just label encode the categorical <code>merchant_category</code> and feed everything to the model.</p>
<p>Create <code>src/train_naive.py</code>:</p>
<pre><code class="language-python"># src/train_naive.py
"""
Train a fraud detection model - NAIVE VERSION.

This script demonstrates the "quick and dirty" approach to ML:
- No experiment tracking
- No model versioning
- Just train and save to a pickle file

We'll improve on this in later sections.
"""
import pandas as pd
import pickle
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import (
    accuracy_score, 
    f1_score, 
    precision_score, 
    recall_score,
    confusion_matrix,
    classification_report
)

def main():
    print("Loading data...")
    train_df = pd.read_csv("data/train.csv")
    test_df = pd.read_csv("data/test.csv")
    
    print(f"Training samples: {len(train_df):,}")
    print(f"Test samples: {len(test_df):,}")
    print(f"Training fraud ratio: {train_df['is_fraud'].mean():.2%}")
    
    # Encode the categorical feature
    # We need to save the encoder to use the same mapping at inference time
    print("\nEncoding categorical features...")
    encoder = LabelEncoder()
    train_df["merchant_encoded"] = encoder.fit_transform(train_df["merchant_category"])
    test_df["merchant_encoded"] = encoder.transform(test_df["merchant_category"])
    
    print(f"Merchant category mapping: {dict(zip(encoder.classes_, encoder.transform(encoder.classes_)))}")
    
    # Prepare features and labels
    feature_cols = ["amount", "hour", "day_of_week", "merchant_encoded"]
    X_train = train_df[feature_cols]
    y_train = train_df["is_fraud"]
    X_test = test_df[feature_cols]
    y_test = test_df["is_fraud"]
    
    # Train a Random Forest classifier
    print("\nTraining Random Forest model...")
    model = RandomForestClassifier(
        n_estimators=100,      # Number of trees
        max_depth=10,          # Maximum depth of each tree
        random_state=42,       # For reproducibility
        n_jobs=-1              # Use all CPU cores
    )
    model.fit(X_train, y_train)
    print("Training complete!")
    
    # Evaluate on test data
    print("\n" + "="*50)
    print("MODEL EVALUATION")
    print("="*50)
    
    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]
    
    print(f"\nAccuracy:  {accuracy_score(y_test, y_pred):.4f}")
    print(f"Precision: {precision_score(y_test, y_pred):.4f}")
    print(f"Recall:    {recall_score(y_test, y_pred):.4f}")
    print(f"F1-score:  {f1_score(y_test, y_pred):.4f}")
    
    print("\nConfusion Matrix:")
    cm = confusion_matrix(y_test, y_pred)
    print(f"  True Negatives:  {cm[0][0]:,} (correctly identified legitimate)")
    print(f"  False Positives: {cm[0][1]:,} (legitimate flagged as fraud)")
    print(f"  False Negatives: {cm[1][0]:,} (fraud missed - DANGEROUS!)")
    print(f"  True Positives:  {cm[1][1]:,} (correctly caught fraud)")
    
    print("\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=['Legitimate', 'Fraud']))
    
    # Feature importance
    print("\nFeature Importance:")
    for name, importance in sorted(
        zip(feature_cols, model.feature_importances_),
        key=lambda x: x[1],
        reverse=True
    ):
        print(f"  {name}: {importance:.4f}")
    
    # Save the model and encoder together
    print("\nSaving model to models/model.pkl...")
    with open("models/model.pkl", "wb") as f:
        pickle.dump((model, encoder), f)
    
    print("\nModel trained and saved successfully!")
    print("\nWARNING: This naive approach has several problems:")
    print("  - No record of hyperparameters or metrics")
    print("  - No model versioning")
    print("  - No way to reproduce this exact model")
    print("  - We'll fix these issues in the following sections!")

if __name__ == "__main__":
    main()
</code></pre>
<p>Run the training script:</p>
<pre><code class="language-python">python src/train_naive.py
</code></pre>
<p>You should see output similar to:</p>
<pre><code class="language-python">Loading data...
Training samples: 8,000
Test samples: 2,000
Training fraud ratio: 2.00%

Encoding categorical features...
Merchant category mapping: {'grocery': 0, 'online': 1, 'restaurant': 2, 'retail': 3, 'travel': 4}

Training Random Forest model...
Training complete!

==================================================
MODEL EVALUATION
==================================================

Accuracy:  0.9820
Precision: 0.7273
Recall:    0.6154
F1-score:  0.6667

Confusion Matrix:
  True Negatives:  1,956 (correctly identified legitimate)
  False Positives: 4 (legitimate flagged as fraud)
  False Negatives: 32 (fraud missed - DANGEROUS!)
  True Positives:  8 (correctly caught fraud)

Feature Importance:
  amount: 0.5423
  hour: 0.2156
  merchant_encoded: 0.1345
  day_of_week: 0.1076
</code></pre>
<p><strong>Important observation:</strong> You'll see ~98% accuracy but a lower F1-score (around 0.5-0.7). <strong>With only 2% fraud, accuracy is extremely misleading!</strong> A model that always predicts "not fraud" would achieve 98% accuracy while catching zero fraud. This is why we focus on F1-score, precision, and recall for imbalanced classification problems.</p>
<p>💡 If you're new to imbalanced classification, remember: high accuracy can be meaningless when the positive class is rare.</p>
<p>The script outputs a file <code>models/model.pkl</code> containing both the trained model and the label encoder (we need both for inference).</p>
<p><strong>Checkpoint:</strong> You should now have:</p>
<ul>
<li><p><code>data/train.csv</code> (~8,000 rows)</p>
</li>
<li><p><code>data/test.csv</code> (~2,000 rows)</p>
</li>
<li><p><code>models/model.pkl</code> (trained model + encoder)</p>
</li>
</ul>
<p>The model should show ~98% accuracy but F1 around 0.5-0.7. Verify the files exist: <code>ls -la data/ models/</code></p>
<h3 id="heading-12-serve-predictions-with-fastapi"><strong>1.2 Serve Predictions with FastAPI</strong></h3>
<p>Now that we have a model, let's deploy it as an API so that clients can get predictions. We'll use <strong>FastAPI</strong> because it's straightforward, very fast, and produces automatic interactive documentation.</p>
<p>FastAPI is known for:</p>
<ul>
<li><p><strong>Easy to use</strong>: Pythonic syntax with type hints</p>
</li>
<li><p><strong>High performance</strong>: One of the fastest Python frameworks</p>
</li>
<li><p><strong>Automatic documentation</strong>: Swagger UI out of the box</p>
</li>
<li><p><strong>Data validation</strong>: Using Pydantic models</p>
</li>
</ul>
<p>Create <code>src/serve_naive.py</code>:</p>
<pre><code class="language-python"># src/serve_naive.py
"""
Serve fraud detection model as a REST API - NAIVE VERSION.

This is a simple API that:
1. Loads the trained model at startup
2. Accepts transaction data via POST request
3. Returns fraud prediction

We'll improve this with validation, monitoring, and better
model loading in later sections.
"""
import pickle
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional

# Load the trained model and encoder at startup
# This is loaded once when the server starts, not on every request
print("Loading model...")
with open("models/model.pkl", "rb") as f:
    model, encoder = pickle.load(f)
print("Model loaded successfully!")

# Create the FastAPI application
app = FastAPI(
    title="Fraud Detection API",
    description="""
    Predict whether a credit card transaction is fraudulent.
    
    This API accepts transaction details and returns:
    - Whether the transaction is predicted to be fraud
    - The probability of fraud (0.0 to 1.0)
    
    **Note:** This is the naive version without validation or monitoring.
    """,
    version="1.0.0"
)

# Define the input schema using Pydantic
# This provides automatic validation and documentation
class Transaction(BaseModel):
    """Schema for a transaction to be evaluated for fraud."""
    amount: float = Field(
        ..., 
        description="Transaction amount in dollars",
        example=150.00
    )
    hour: int = Field(
        ..., 
        description="Hour of the day (0-23)",
        example=14
    )
    day_of_week: int = Field(
        ..., 
        description="Day of week (0=Monday, 6=Sunday)",
        example=3
    )
    merchant_category: str = Field(
        ..., 
        description="Type of merchant",
        example="online"
    )

class PredictionResponse(BaseModel):
    """Schema for the prediction response."""
    is_fraud: bool = Field(description="Whether the transaction is predicted as fraud")
    fraud_probability: float = Field(description="Probability of fraud (0.0 to 1.0)")
    
@app.post("/predict", response_model=PredictionResponse)
def predict(transaction: Transaction):
    """
    Predict whether a transaction is fraudulent.
    
    Takes transaction details and returns a fraud prediction
    along with the probability score.
    """
    # Convert the request to a dictionary
    data = transaction.dict()
    
    # Encode the merchant category using the same encoder from training
    # This ensures consistency between training and serving
    try:
        data["merchant_encoded"] = encoder.transform([data["merchant_category"]])[0]
    except ValueError:
        # Handle unknown merchant categories
        # In production, we'd want better handling here
        data["merchant_encoded"] = 0
    
    # Prepare features in the same order as training
    X = [[
        data["amount"],
        data["hour"],
        data["day_of_week"],
        data["merchant_encoded"]
    ]]
    
    # Get prediction and probability
    prediction = model.predict(X)[0]
    probability = model.predict_proba(X)[0][1]  # Probability of class 1 (fraud)
    
    return PredictionResponse(
        is_fraud=bool(prediction),
        fraud_probability=round(float(probability), 4)
    )

@app.get("/health")
def health_check():
    """
    Health check endpoint.
    
    Returns the status of the API. Useful for:
    - Load balancer health checks
    - Kubernetes liveness probes
    - Monitoring systems
    """
    return {
        "status": "healthy",
        "model_loaded": model is not None
    }

@app.get("/")
def root():
    """Root endpoint with API information."""
    return {
        "message": "Fraud Detection API",
        "version": "1.0.0",
        "docs": "/docs",
        "health": "/health"
    }
</code></pre>
<p>A few important things to note about this code:</p>
<ol>
<li><p><strong>Pydantic Models</strong>: We use <code>BaseModel</code> to define the expected input JSON schema. FastAPI automatically validates incoming requests against this schema.</p>
</li>
<li><p><strong>Type Hints</strong>: The type hints (<code>float</code>, <code>int</code>, <code>str</code>) provide both documentation and runtime validation.</p>
</li>
<li><p><strong>Feature Encoding</strong>: On each request, we encode the merchant category using the same <code>LabelEncoder</code> we saved from training. This ensures consistency between training and serving.</p>
</li>
<li><p><strong>Health Endpoint</strong>: The <code>/health</code> endpoint is standard practice for production APIs - it allows load balancers and monitoring systems to check if the service is running.</p>
</li>
</ol>
<p>To run this API, use Uvicorn (an ASGI server):</p>
<pre><code class="language-python">uvicorn src.serve_naive:app --reload --host 0.0.0.0 --port 8000
</code></pre>
<p>The <code>--reload</code> flag enables auto-reload during development (the server restarts when you change code).</p>
<p>You should see:</p>
<pre><code class="language-python">Loading model...
Model loaded successfully!
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process
</code></pre>
<p>Now open your browser and go to <code>http://localhost:8000/docs</code>. You'll see the <strong>Swagger UI</strong> – an auto-generated interactive documentation where you can test the API directly from your browser!</p>
<p>Test the API using curl in another terminal:</p>
<pre><code class="language-python"># Test with a legitimate-looking transaction
curl -X POST "http://localhost:8000/predict" \
  -H "Content-Type: application/json" \
  -d '{"amount": 50.0, "hour": 14, "day_of_week": 3, "merchant_category": "grocery"}'
</code></pre>
<p>Expected response:</p>
<pre><code class="language-python">{"is_fraud": false, "fraud_probability": 0.02}
</code></pre>
<pre><code class="language-python"># Test with a suspicious transaction (high amount, late night, online)
curl -X POST "http://localhost:8000/predict" \
  -H "Content-Type: application/json" \
  -d '{"amount": 500.0, "hour": 3, "day_of_week": 1, "merchant_category": "online"}'
</code></pre>
<p>Expected response:</p>
<pre><code class="language-python">{"is_fraud": true, "fraud_probability": 0.78}
</code></pre>
<p><strong>We have a working model served as an API!</strong> In a real scenario, we could now integrate this API with a payment processing frontend, mobile app, or any system that needs fraud predictions.</p>
<p>But before we celebrate, let's examine this naive approach for potential pitfalls...</p>
<p><strong>Checkpoint:</strong> Your API should be running at <code>http://localhost:8000</code>. The Swagger UI at <code>/docs</code> should show both endpoints (<code>/predict</code> and <code>/health</code>). Test with curl or the Swagger UI to verify predictions are returned.</p>
<h2 id="heading-2-where-the-naive-approach-breaks"><strong>2. Where the Naive Approach Breaks</strong></h2>
<p>Our quick-and-dirty ML pipeline works on the surface: it can train a model and serve predictions. However, <strong>hidden problems will emerge</strong> if we try to maintain or scale this system in production.</p>
<p>This section is critical: understanding these issues will motivate the solutions we implement in the following sections. Let's go through the problems one by one.</p>
<h3 id="heading-problem-1-no-experiment-tracking-reproducibility"><strong>Problem 1: No Experiment Tracking (Reproducibility)</strong></h3>
<p>Try this thought experiment: Run <code>train_naive.py</code> again with different hyperparameters (change <code>n_estimators</code> to 200, or <code>max_depth</code> to 15). Would you be able to <strong>exactly reproduce the previous model's results</strong> if someone asked?</p>
<p>Probably not. Currently, we have <strong>no record</strong> of:</p>
<ul>
<li><p>Which hyperparameters we used</p>
</li>
<li><p>What metrics we achieved</p>
</li>
<li><p>What version of the data we trained on</p>
</li>
<li><p>What library versions were installed</p>
</li>
<li><p>When the training happened</p>
</li>
<li><p>Who ran the training</p>
</li>
</ul>
<p>Three months from now, if your manager asks "How was this model trained? Can you reproduce the results?" – you'd be in trouble. You might have the code, but you don't know which version of the code, which parameters, or which data produced the model that's currently in production.</p>
<p><strong>Experiment tracking</strong> is the practice of logging all these details (code versions, parameters, metrics, data versions, artifacts) so experiments can be compared and replicated. Our naive approach lacks this entirely, making our results hard to trust or build upon.</p>
<h3 id="heading-problem-2-model-versioning-and-deployment-chaos"><strong>Problem 2: Model Versioning and Deployment Chaos</strong></h3>
<p>We trained one model and saved it as <code>model.pkl</code>. Now consider this scenario:</p>
<ol>
<li><p>You train a new model with different hyperparameters</p>
</li>
<li><p>You overwrite <code>model.pkl</code> with the new model</p>
</li>
<li><p>You deploy it to production</p>
</li>
<li><p>Users start complaining about more false positives</p>
</li>
<li><p>You want to roll back to the previous model</p>
</li>
<li><p><strong>Problem:</strong> The previous model was overwritten and is gone forever</p>
</li>
</ol>
<p>There's no systematic versioning. Questions you cannot answer:</p>
<ul>
<li><p>Which model version is currently in production?</p>
</li>
<li><p>What were the metrics for model v1 vs v2?</p>
</li>
<li><p>When was each model trained and by whom?</p>
</li>
<li><p>Can we instantly roll back if the new model performs worse?</p>
</li>
<li><p>What changed between versions?</p>
</li>
</ul>
<p>Without version control for models, you're flying blind. Imagine deploying code without Git – that's what we're doing with our model.</p>
<h3 id="heading-problem-3-no-data-validation-garbage-in-garbage-out"><strong>Problem 3: No Data Validation – Garbage In, Garbage Out</strong></h3>
<p>Right now, our API will accept <strong>any input</strong> and try to make a prediction. Let's see what happens with bad data.</p>
<p>Create a test script <code>src/test_bad_data.py</code>:</p>
<pre><code class="language-python"># src/test_bad_data.py
"""Test what happens when we send garbage data to the API."""
import requests

BASE_URL = "http://localhost:8000"

print("Testing API with various bad inputs...\n")

# Test 1: Negative amount
print("Test 1: Negative amount")
response = requests.post(f"{BASE_URL}/predict", json={
    "amount": -500.0,        # Negative amount - impossible!
    "hour": 14,
    "day_of_week": 3,
    "merchant_category": "online"
})
print(f"  Status: {response.status_code}")
print(f"  Response: {response.json()}\n")

# Test 2: Invalid hour
print("Test 2: Hour = 25 (should be 0-23)")
response = requests.post(f"{BASE_URL}/predict", json={
    "amount": 100.0,
    "hour": 25,              # Invalid hour!
    "day_of_week": 3,
    "merchant_category": "online"
})
print(f"  Status: {response.status_code}")
print(f"  Response: {response.json()}\n")

# Test 3: Invalid day of week
print("Test 3: day_of_week = 10 (should be 0-6)")
response = requests.post(f"{BASE_URL}/predict", json={
    "amount": 100.0,
    "hour": 14,
    "day_of_week": 10,       # Invalid day!
    "merchant_category": "online"
})
print(f"  Status: {response.status_code}")
print(f"  Response: {response.json()}\n")

# Test 4: Unknown merchant category
print("Test 4: Unknown merchant category")
response = requests.post(f"{BASE_URL}/predict", json={
    "amount": 100.0,
    "hour": 14,
    "day_of_week": 3,
    "merchant_category": "unknown_category"  # Not in training data!
})
print(f"  Status: {response.status_code}")
print(f"  Response: {response.json()}\n")

# Test 5: All bad at once
print("Test 5: Everything wrong")
response = requests.post(f"{BASE_URL}/predict", json={
    "amount": -1000.0,
    "hour": 99,
    "day_of_week": 15,
    "merchant_category": "totally_fake"
})
print(f"  Status: {response.status_code}")
print(f"  Response: {response.json()}\n")

print("Observation: The API happily accepts ALL garbage and returns predictions!")
print("This is dangerous - bad data leads to bad predictions with no warning.")
</code></pre>
<p>Run it (make sure your API is still running):</p>
<pre><code class="language-python">python src/test_bad_data.py
</code></pre>
<p>You'll see something like:</p>
<pre><code class="language-python">Testing API with various bad inputs...

Test 1: Negative amount
  Status: 200
  Response: {'is_fraud': False, 'fraud_probability': 0.15}

Test 2: Hour = 25 (should be 0-23)
  Status: 200
  Response: {'is_fraud': False, 'fraud_probability': 0.08}

...

Observation: The API happily accepts ALL garbage and returns predictions!
</code></pre>
<p><strong>The API accepts garbage and returns predictions with no warning!</strong> In production, this could mean:</p>
<ul>
<li><p>Incorrect predictions based on impossible data</p>
</li>
<li><p>Fraud going undetected because of malformed input</p>
</li>
<li><p>Legitimate transactions blocked based on corrupted data</p>
</li>
<li><p>No way to debug why predictions are wrong</p>
</li>
</ul>
<p>As the saying goes: <strong>"Garbage in, garbage out."</strong> But even worse – we don't even know garbage went in!</p>
<h3 id="heading-problem-4-model-drift-performance-decay-over-time"><strong>Problem 4: Model Drift – Performance Decay Over Time</strong></h3>
<p>Here's a scenario that happens in every production ML system:</p>
<ol>
<li><p><strong>January</strong>: You train your model on historical fraud data. It achieves 98% accuracy and 0.67 F1-score. Everyone's happy.</p>
</li>
<li><p><strong>February</strong>: The model is deployed and working well. Fraud is being caught.</p>
</li>
<li><p><strong>March</strong>: Fraudsters adapt. They start using different patterns – smaller amounts, different merchant categories, different times of day.</p>
</li>
<li><p><strong>April</strong>: Your model's accuracy has dropped from 98% to 85%. F1-score dropped from 0.67 to 0.35. Fraud is slipping through.</p>
</li>
<li><p><strong>May</strong>: A major fraud incident occurs. Investigation reveals the model has been underperforming for 2 months.</p>
</li>
</ol>
<p><strong>The problem:</strong> Nobody noticed for 2 months because there was no monitoring.</p>
<p>This phenomenon is called <strong>data drift</strong> (when input data distributions change) or <strong>concept drift</strong> (when the relationship between inputs and outputs changes). Both are inevitable in real-world systems.</p>
<p>Without monitoring:</p>
<ul>
<li><p>You don't know when performance degrades</p>
</li>
<li><p>You don't know why performance degrades</p>
</li>
<li><p>You can't take corrective action until users complain</p>
</li>
<li><p>By then, significant damage may have occurred</p>
</li>
</ul>
<h3 id="heading-problem-5-no-cicd-or-deployment-safety"><strong>Problem 5: No CI/CD or Deployment Safety</strong></h3>
<p>Our "deployment process" was literally:</p>
<ol>
<li><p>SSH into the server (or run locally)</p>
</li>
<li><p>Run <code>python src/train_naive.py</code></p>
</li>
<li><p>Copy model.pkl to the right place</p>
</li>
<li><p>Restart the API</p>
</li>
<li><p>Hope for the best</p>
</li>
</ol>
<p>There's:</p>
<ul>
<li><p><strong>No automated testing</strong>: A typo could break everything</p>
</li>
<li><p><strong>No staging environment</strong>: We test directly in production</p>
</li>
<li><p><strong>No gradual rollout</strong>: 100% of traffic hits the new model immediately</p>
</li>
<li><p><strong>No rollback capability</strong>: If something breaks, we have to manually fix it</p>
</li>
<li><p><strong>No audit trail</strong>: Who deployed what and when?</p>
</li>
</ul>
<p>This is how production incidents happen. A rushed deployment at 5 PM on Friday breaks the fraud detection system, and nobody notices until Monday when fraud losses have spiked.</p>
<p><strong>Figure 2:</strong> Problems with the Naive Approach</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771392425864/75c51059-5ab3-4e08-b3ad-7f5e9c3e7445.png" alt="Diagram showing the weaknesses of a naive machine learning setup: manual training and deployment, no experiment tracking, no model versioning, inconsistent features between training and serving, no data validation, no drift or performance monitoring, and no CI/CD safeguards such as automated tests, rollback, or audit trail." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-summary-what-we-need-to-fix"><strong>Summary: What We Need to Fix</strong></h3>
<p>Our simple ML service is missing critical infrastructure. Here's the mapping of problems to solutions:</p>
<table>
<thead>
<tr>
<th><strong>Problem</strong></th>
<th><strong>Impact</strong></th>
<th><strong>Solution</strong></th>
<th><strong>Section</strong></th>
</tr>
</thead>
<tbody><tr>
<td>No experiment tracking</td>
<td>Can't reproduce or compare models</td>
<td>MLflow Tracking</td>
<td>3</td>
</tr>
<tr>
<td>No model versioning</td>
<td>Can't roll back or audit</td>
<td>MLflow Registry</td>
<td>3</td>
</tr>
<tr>
<td>No feature consistency</td>
<td>Training-serving skew</td>
<td>Feast Feature Store</td>
<td>4</td>
</tr>
<tr>
<td>No data validation</td>
<td>Garbage predictions</td>
<td>Great Expectations</td>
<td>5</td>
</tr>
<tr>
<td>No monitoring</td>
<td>Drift goes unnoticed</td>
<td>Evidently</td>
<td>6</td>
</tr>
<tr>
<td>No CI/CD</td>
<td>Risky deployments</td>
<td>GitHub Actions + Docker</td>
<td>7</td>
</tr>
</tbody></table>
<p><strong>The good news:</strong> We can fix each of these by incrementally adding components to our pipeline. Each tool addresses a specific problem, and together they form a robust ML platform.</p>
<p>Let's start fixing these issues, one by one.</p>
<h2 id="heading-3-add-experiment-tracking-and-model-registry-with-mlflow"><strong>3. Add Experiment Tracking and Model Registry with MLflow</strong></h2>
<p><strong>What breaks without this:</strong> You can't reproduce yesterday's results, can't compare experiments, and can't roll back when a new model fails in production.</p>
<p>Our first fix addresses <strong>Problems 1 and 2</strong>: experiment reproducibility and model versioning.</p>
<p><strong>MLflow</strong> is an open-source platform designed to manage the ML lifecycle. We'll use two of its key components:</p>
<ol>
<li><p><strong>MLflow Tracking</strong>: Log experiments (parameters, metrics, artifacts) so you can compare runs and reproduce results</p>
</li>
<li><p><strong>MLflow Model Registry</strong>: Version your models with aliases (champion, challenger) and manage the deployment lifecycle</p>
</li>
</ol>
<p><strong>Why This Matters:</strong> Without tracking, ML is guesswork. With MLflow, every run is logged with parameters, metrics, and artifacts. You can compare runs side-by-side, understand what actually improved your model, and reproduce any past experiment. The Model Registry adds governance – you know exactly which model is in production and can roll back in seconds.</p>
<h3 id="heading-31-how-to-set-up-the-mlflow-tracking-server"><strong>3.1</strong> How to Set Up the MLflow Tracking Server</h3>
<p>MLflow can log experiments to a local directory by default, but to use the full UI and model registry, it's best to run the MLflow tracking server.</p>
<p>Open a <strong>new terminal</strong> (keep it separate from your API terminal) and run:</p>
<pre><code class="language-python"># Create a directory for MLflow data
mkdir -p mlruns

# Start the MLflow server
mlflow server \
    --host 0.0.0.0 \
    --port 5000 \
    --backend-store-uri sqlite:///mlflow.db \
    --default-artifact-root ./mlruns
</code></pre>
<p>Let's break down these parameters:</p>
<ul>
<li><p><code>--host 0.0.0.0</code>: Listen on all network interfaces</p>
</li>
<li><p><code>--port 5000</code>: Run on port 5000</p>
</li>
<li><p><code>--backend-store-uri sqlite:///mlflow.db</code>: Store experiment metadata in a SQLite database (for production, you'd use PostgreSQL or MySQL)</p>
</li>
<li><p><code>--default-artifact-root ./mlruns</code>: Store model artifacts (files) in the <code>mlruns</code> directory</p>
</li>
</ul>
<p>You should see:</p>
<pre><code class="language-python">[INFO] Starting gunicorn 21.2.0
[INFO] Listening at: http://0.0.0.0:5000
</code></pre>
<p>Now open your browser and navigate to <code>http://localhost:5000</code>. You'll see the <strong>MLflow UI</strong> – it should be empty initially since we haven't logged any experiments yet.</p>
<h3 id="heading-32-how-to-log-experiments-in-code"><strong>3.2</strong> How to Log Experiments in Code</h3>
<p>Now let's modify our training script to log everything to MLflow. Create <code>src/train_mlflow.py</code>:</p>
<pre><code class="language-python"># src/train_mlflow.py
"""
Train fraud detection model with MLflow experiment tracking.

This script demonstrates proper ML experiment tracking:
- Log all hyperparameters
- Log all metrics (train and test)
- Log the trained model as an artifact
- Register the model in the Model Registry

Compare this to train_naive.py to see the difference!
"""
import pandas as pd
import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import (
    accuracy_score, 
    precision_score, 
    recall_score, 
    f1_score,
    roc_auc_score
)
import pickle
from datetime import datetime

# Configure MLflow to use our tracking server
mlflow.set_tracking_uri("http://localhost:5000")

# Create or get the experiment
# All runs will be grouped under this experiment name
mlflow.set_experiment("fraud-detection")

def load_and_preprocess_data():
    """Load and preprocess the training and test data."""
    print("Loading data...")
    train_df = pd.read_csv("data/train.csv")
    test_df = pd.read_csv("data/test.csv")
    
    # Encode categorical feature
    encoder = LabelEncoder()
    train_df["merchant_encoded"] = encoder.fit_transform(train_df["merchant_category"])
    test_df["merchant_encoded"] = encoder.transform(test_df["merchant_category"])
    
    # Prepare features
    feature_cols = ["amount", "hour", "day_of_week", "merchant_encoded"]
    X_train = train_df[feature_cols]
    y_train = train_df["is_fraud"]
    X_test = test_df[feature_cols]
    y_test = test_df["is_fraud"]
    
    return X_train, y_train, X_test, y_test, encoder

def train_and_log_model(
    n_estimators: int = 100,
    max_depth: int = 10,
    min_samples_split: int = 2,
    min_samples_leaf: int = 1
):
    """
    Train a model and log everything to MLflow.
    
    Args:
        n_estimators: Number of trees in the forest
        max_depth: Maximum depth of each tree
        min_samples_split: Minimum samples required to split a node
        min_samples_leaf: Minimum samples required at a leaf node
    """
    X_train, y_train, X_test, y_test, encoder = load_and_preprocess_data()
    
    # Start an MLflow run - everything logged will be associated with this run
    with mlflow.start_run():
        # Add a descriptive run name
        run_name = f"rf_est{n_estimators}_depth{max_depth}_{datetime.now().strftime('%H%M%S')}"
        mlflow.set_tag("mlflow.runName", run_name)
        
        # Log all hyperparameters
        # These are the "knobs" we can tune
        mlflow.log_param("n_estimators", n_estimators)
        mlflow.log_param("max_depth", max_depth)
        mlflow.log_param("min_samples_split", min_samples_split)
        mlflow.log_param("min_samples_leaf", min_samples_leaf)
        mlflow.log_param("model_type", "RandomForestClassifier")
        
        # Log data information
        mlflow.log_param("train_samples", len(X_train))
        mlflow.log_param("test_samples", len(X_test))
        mlflow.log_param("fraud_ratio", float(y_train.mean()))
        mlflow.log_param("n_features", X_train.shape[1])
        
        # Train the model
        print(f"\nTraining model: n_estimators={n_estimators}, max_depth={max_depth}")
        model = RandomForestClassifier(
            n_estimators=n_estimators,
            max_depth=max_depth,
            min_samples_split=min_samples_split,
            min_samples_leaf=min_samples_leaf,
            random_state=42,
            n_jobs=-1
        )
        model.fit(X_train, y_train)
        
        # Evaluate and log metrics for BOTH train and test sets
        # This helps detect overfitting
        for dataset_name, X, y in [("train", X_train, y_train), ("test", X_test, y_test)]:
            y_pred = model.predict(X)
            y_prob = model.predict_proba(X)[:, 1]
            
            # Calculate all metrics
            accuracy = accuracy_score(y, y_pred)
            precision = precision_score(y, y_pred, zero_division=0)
            recall = recall_score(y, y_pred, zero_division=0)
            f1 = f1_score(y, y_pred, zero_division=0)
            roc_auc = roc_auc_score(y, y_prob)
            
            # Log metrics with dataset prefix
            mlflow.log_metric(f"{dataset_name}_accuracy", accuracy)
            mlflow.log_metric(f"{dataset_name}_precision", precision)
            mlflow.log_metric(f"{dataset_name}_recall", recall)
            mlflow.log_metric(f"{dataset_name}_f1", f1)
            mlflow.log_metric(f"{dataset_name}_roc_auc", roc_auc)
            
            print(f"  {dataset_name.upper()} - Accuracy: {accuracy:.4f}, F1: {f1:.4f}, ROC-AUC: {roc_auc:.4f}")
        
        # Log feature importance
        for feature, importance in zip(
            ["amount", "hour", "day_of_week", "merchant_encoded"],
            model.feature_importances_
        ):
            mlflow.log_metric(f"importance_{feature}", importance)
        
        # Log the model to MLflow AND register it in the Model Registry
        # This creates a new version of the model automatically
        print("\nRegistering model in MLflow Model Registry...")
        mlflow.sklearn.log_model(
            sk_model=model,
            artifact_path="model",
            registered_model_name="fraud-detection-model",
            input_example=X_train.iloc[:5]  # Example input for documentation
        )
        
        # Save and log the encoder as a separate artifact
        # We need this for inference
        with open("encoder.pkl", "wb") as f:
            pickle.dump(encoder, f)
        mlflow.log_artifact("encoder.pkl")
        
        # Get the run ID for reference
        run_id = mlflow.active_run().info.run_id
        print(f"\nMLflow Run ID: {run_id}")
        print(f"View this run: http://localhost:5000/#/experiments/1/runs/{run_id}")
        
        return model, encoder

def run_experiment_sweep():
    """
    Run multiple experiments with different hyperparameters.
    
    This demonstrates how MLflow helps compare different configurations.
    """
    print("="*60)
    print("RUNNING HYPERPARAMETER EXPERIMENT SWEEP")
    print("="*60)
    
    # Define different configurations to try
    experiments = [
        {"n_estimators": 50, "max_depth": 5},
        {"n_estimators": 100, "max_depth": 10},
        {"n_estimators": 100, "max_depth": 15},
        {"n_estimators": 200, "max_depth": 10},
        {"n_estimators": 200, "max_depth": 20},
    ]
    
    for i, params in enumerate(experiments, 1):
        print(f"\n--- Experiment {i}/{len(experiments)} ---")
        train_and_log_model(**params)
    
    print("\n" + "="*60)
    print("EXPERIMENT SWEEP COMPLETE!")
    print("="*60)
    print("\nView all experiments at: http://localhost:5000")
    print("Compare runs to find the best hyperparameters!")

if __name__ == "__main__":
    run_experiment_sweep()
</code></pre>
<p>This script:</p>
<ol>
<li><p><strong>Connects to MLflow</strong>: <code>mlflow.set_tracking_uri("</code><a href="http://localhost:5000"><code>http://localhost:5000</code></a><code>")</code></p>
</li>
<li><p><strong>Creates an experiment</strong>: <code>mlflow.set_experiment("fraud-detection")</code></p>
</li>
<li><p><strong>Logs parameters</strong>: All hyperparameters and data info</p>
</li>
<li><p><strong>Logs metrics</strong>: Accuracy, precision, recall, F1, ROC-AUC for both train and test sets</p>
</li>
<li><p><strong>Logs the model</strong>: Saves the trained model as an artifact</p>
</li>
<li><p><strong>Registers the model</strong>: Adds it to the Model Registry with automatic versioning</p>
</li>
</ol>
<p>Run the experiment sweep:</p>
<pre><code class="language-python">python src/train_mlflow.py
</code></pre>
<p>You'll see output for each experiment:</p>
<pre><code class="language-python">============================================================
RUNNING HYPERPARAMETER EXPERIMENT SWEEP
============================================================

--- Experiment 1/5 ---
Loading data...
Training model: n_estimators=50, max_depth=5
  TRAIN - Accuracy: 0.9821, F1: 0.6545, ROC-AUC: 0.9234
  TEST - Accuracy: 0.9795, F1: 0.5714, ROC-AUC: 0.8956

Registering model in MLflow Model Registry...
MLflow Run ID: abc123...

--- Experiment 5/5 ---
Training model: n_estimators=200, max_depth=20
  TRAIN - Accuracy: 0.9856, F1: 0.7123, ROC-AUC: 0.9567
  TEST - Accuracy: 0.9810, F1: 0.6667, ROC-AUC: 0.9234

============================================================
EXPERIMENT SWEEP COMPLETE!
============================================================
</code></pre>
<p>All 5 runs are now logged to MLflow with full metrics comparison available in the UI.</p>
<p>Now refresh the MLflow UI at <code>http://localhost:5000</code>. You'll see:</p>
<ol>
<li><p><strong>Experiments tab</strong>: Shows the "fraud-detection" experiment with 5 runs</p>
</li>
<li><p><strong>Each run</strong>: Shows parameters, metrics, and artifacts</p>
</li>
<li><p><strong>Compare</strong>: You can select multiple runs and compare them side-by-side</p>
</li>
<li><p><strong>Models tab</strong>: Shows "fraud-detection-model" with 5 versions</p>
</li>
</ol>
<p><strong>MLflow Tracking UI: Compare runs, metrics, and models at a glance</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771396202929/c5a7d547-31b6-4783-acea-f4e9433d81ef.png" alt="c5a7d547-31b6-4783-acea-f4e9433d81ef" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-33-how-to-use-the-model-registry"><strong>3.3</strong> How to Use the Model Registry</h3>
<p>The <strong>Model Registry</strong> provides a central hub for managing model versions and their lifecycle stages.</p>
<p>In the MLflow UI:</p>
<ol>
<li><p>Click the <strong>"Models"</strong> tab in the top navigation</p>
</li>
<li><p>Click <strong>"fraud-detection-model"</strong></p>
</li>
<li><p>You'll see all 5 versions listed with their metrics</p>
</li>
</ol>
<p><strong>Model Aliases:</strong> MLflow now uses <strong>aliases</strong> instead of stages. If you've seen older tutorials using "Staging" and "Production" stages, aliases are the newer, more flexible approach.</p>
<ul>
<li><p><strong>@champion</strong>: The production model serving live traffic</p>
</li>
<li><p><strong>@challenger</strong>: Candidate model being tested</p>
</li>
<li><p>You can create custom aliases like @baseline, @latest and so on.</p>
</li>
</ul>
<p><strong>Assign an alias:</strong></p>
<ol>
<li><p>Open MLflow UI → Models → fraud-detection-model</p>
</li>
<li><p>Click on the version you want to promote</p>
</li>
<li><p>Click <strong>"Add Alias"</strong></p>
</li>
<li><p>Enter <code>champion</code> and save</p>
</li>
</ol>
<p>Now you've assigned the <code>@champion</code> alias to your best model. Your API will load whichever version has this alias, making rollbacks as simple as moving the alias to a different version.</p>
<p><strong>Figure 3: MLflow Model Lifecycle — From Training to Production</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771396081377/da67d89f-b82d-4189-8150-ecc142ed198a.png" alt="Diagram showing the MLflow model lifecycle for a fraud detection system: a model is trained with experiment parameters, logged to MLflow tracking with metrics and artifacts, registered in the model registry as multiple versions, assigned aliases such as champion and challenger, and served in production by loading the model through the champion alias. The diagram also shows rollback by moving the alias to an earlier version and restarting the API." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-34-update-api-to-load-from-registry"><strong>3.4 Update API to Load from Registry</strong></h3>
<p>Now let's update our API to load the champion model from the MLflow Registry instead of a pickle file. Create <code>src/serve_mlflow.py</code>:</p>
<pre><code class="language-python"># src/serve_mlflow.py
"""
Serve fraud detection model from MLflow Model Registry.

This version loads the @champion model from MLflow, which means:
- Always serves the latest @champion model
- Can roll back by changing the @champion alias
- No manual file copying needed
"""
import mlflow
import mlflow.sklearn
import pickle
import os
from fastapi import FastAPI
from pydantic import BaseModel, Field

# Configure MLflow
mlflow.set_tracking_uri("http://localhost:5000")

print("Loading model from MLflow Model Registry...")

# Load the champion model from the registry
# This automatically gets whichever version has the @champion alias
try:
    model = mlflow.sklearn.load_model("models:/fraud-detection-model@champion")
    print("Successfully loaded champion model from MLflow!")
except Exception as e:
    print(f"Error loading from MLflow: {e}")
    print("Make sure you've assigned the @champion alias to a model in the MLflow UI")
    raise

# Load the encoder (saved as an artifact)
# In a real system, you might also version this in MLflow
with open("encoder.pkl", "rb") as f:
    encoder = pickle.load(f)
print("Encoder loaded successfully!")

app = FastAPI(
    title="Fraud Detection API (MLflow)",
    description="""
    Fraud detection API that loads models from MLflow Model Registry.
    
    This version always serves the model with the @champion alias.
    To update the model:
    1. Train a new model with train_mlflow.py
    2. Compare metrics in MLflow UI
    3. Promote the best model to Production
    4. Restart this API
    
    To roll back: Move the @champion alias to a previous version in MLflow UI.
    """,
    version="2.0.0"
)

class Transaction(BaseModel):
    amount: float = Field(..., description="Transaction amount in dollars", example=150.00)
    hour: int = Field(..., description="Hour of the day (0-23)", example=14)
    day_of_week: int = Field(..., description="Day of week (0=Monday, 6=Sunday)", example=3)
    merchant_category: str = Field(..., description="Type of merchant", example="online")

class PredictionResponse(BaseModel):
    is_fraud: bool
    fraud_probability: float
    model_source: str = "MLflow Production"

@app.post("/predict", response_model=PredictionResponse)
def predict(tx: Transaction):
    """Predict whether a transaction is fraudulent using the champion model."""
    data = tx.dict()
    
    try:
        data["merchant_encoded"] = encoder.transform([data["merchant_category"]])[0]
    except ValueError:
        data["merchant_encoded"] = 0
    
    X = [[data["amount"], data["hour"], data["day_of_week"], data["merchant_encoded"]]]
    
    pred = model.predict(X)[0]
    prob = model.predict_proba(X)[0][1]
    
    return PredictionResponse(
        is_fraud=bool(pred),
        fraud_probability=round(float(prob), 4),
        model_source="MLflow Production"
    )

@app.get("/health")
def health():
    return {"status": "healthy", "model_source": "MLflow Registry"}

@app.get("/model-info")
def model_info():
    """Get information about the currently loaded model."""
    return {
        "registry": "MLflow",
        "model_name": "fraud-detection-model",
        "alias": "champion",
        "tracking_uri": "http://localhost:5000"
    }
</code></pre>
<p>Stop your old API (Ctrl+C) and start this new one:</p>
<pre><code class="language-python">uvicorn src.serve_mlflow:app --reload --host 0.0.0.0 --port 8000
</code></pre>
<p>Now deploying a new model is a <strong>controlled, auditable process</strong>:</p>
<ol>
<li><p><strong>Train new model</strong> → Automatically registered as new version</p>
</li>
<li><p><strong>Compare metrics</strong> → Use MLflow UI to compare with current Production</p>
</li>
<li><p><strong>Set as champion</strong> → Assign @champion alias in MLflow UI</p>
</li>
<li><p><strong>Restart API</strong> → Loads new Production model</p>
</li>
<li><p><strong>Roll back if needed</strong> → Move @champion alias to previous version</p>
</li>
</ol>
<p><strong>Checkpoint:</strong></p>
<ul>
<li><p>MLflow UI (<code>http://localhost:5000</code>) should show the "fraud-detection" experiment with 5 runs</p>
</li>
<li><p>The "Models" tab should show "fraud-detection-model" with 5 versions</p>
</li>
<li><p>One version should have @champion alias</p>
</li>
<li><p>The API should load and serve @champion model</p>
</li>
</ul>
<h2 id="heading-4-ensure-feature-consistency-with-feast"><strong>4. Ensure Feature Consistency with Feast</strong></h2>
<p>⚠️ <strong>First time hearing about feature stores?</strong> Don't worry.<br>You don't need to master every Feast detail on the first read.<br>Focus on <em>why</em> feature consistency matters — you can revisit the implementation later.<br><strong>Key takeaway:</strong> Training and serving must compute features the same way, or your model silently fails.</p>
<p><strong>What breaks without this:</strong> Your model sees different feature values in production than it saw during training. Accuracy drops silently. This is called "training-serving skew" and it's one of the most common causes of ML system failures.</p>
<p>One subtle but critical issue in ML systems is <strong>training-serving skew</strong> – when data transformations at training time differ from inference time. Even small discrepancies can severely degrade performance.</p>
<p><strong>Why This Matters:</strong> Imagine you're computing "average transaction amount per merchant category" as a feature. During training, you compute it using pandas in a notebook. During serving, you compute it using SQL in a different system. Small differences in how these computations handle edge cases (nulls, rounding, time windows) cause the model to see different features in production than it was trained on.</p>
<p>The result? <strong>Silent failures</strong> where accuracy drops but nothing errors out. Your model is making predictions based on features it's never seen before, and you have no idea.</p>
<p>In our naive implementation, we did handle one simple case: we saved the <code>LabelEncoder</code> to ensure <code>merchant_category</code> is encoded the same way in training and serving. But imagine if we had more complex feature engineering:</p>
<ul>
<li><p>Rolling averages over time windows</p>
</li>
<li><p>User-level aggregations</p>
</li>
<li><p>Cross-feature interactions</p>
</li>
<li><p>Real-time features from streaming data</p>
</li>
</ul>
<p>Maintaining consistency manually becomes impossible.</p>
<h3 id="heading-41-what-is-feast-and-why-use-it"><strong>4.1 What is Feast and Why Use It?</strong></h3>
<p>In production ML platforms, teams use a <strong>feature store</strong> to guarantee feature consistency between training and serving. <strong>Feast</strong> is one popular open-source option.</p>
<p>In this tutorial, we use Feast not because you <em>must</em>, but because it makes the training-serving contract explicit and teachable. The principles apply whether you use Feast, Tecton, Featureform, or a custom solution.</p>
<p>Feast provides:</p>
<table>
<thead>
<tr>
<th><strong>Capability</strong></th>
<th><strong>Description</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Single source of truth</strong></td>
<td>Define features once, use everywhere</td>
</tr>
<tr>
<td><strong>Offline/online consistency</strong></td>
<td>Same features for training and serving</td>
</tr>
<tr>
<td><strong>Point-in-time correctness</strong></td>
<td>Prevents data leakage in training</td>
</tr>
<tr>
<td><strong>Low-latency serving</strong></td>
<td>Millisecond feature retrieval</td>
</tr>
<tr>
<td><strong>Feature versioning</strong></td>
<td>Track changes to feature definitions</td>
</tr>
</tbody></table>
<p><strong>How Feast works:</strong></p>
<ol>
<li><p><strong>Define features</strong> in Python code (feature definitions)</p>
</li>
<li><p><strong>Materialize features</strong> from your data sources to the online store</p>
</li>
<li><p><strong>Retrieve features</strong> using the same API for both training (offline) and serving (online)</p>
</li>
</ol>
<p>This ensures that training and serving use <strong>exactly the same feature computation logic</strong>.</p>
<h3 id="heading-42-install-and-initialize-feast"><strong>4.2 Install and Initialize Feast</strong></h3>
<p>We already installed Feast via requirements.txt. Now let's initialize a feature repository.</p>
<pre><code class="language-python"># Navigate to the feature_repo directory
cd feature_repo

# Initialize Feast (this creates template files)
feast init . --minimal

# Go back to project root
cd ..
</code></pre>
<p>This creates the basic Feast structure:</p>
<pre><code class="language-python">feature_repo/
├── feature_store.yaml    # Feast configuration
└── __init__.py
</code></pre>
<h3 id="heading-43-define-feature-definitions"><strong>4.3 Define Feature Definitions</strong></h3>
<p>First, let's create the Feast configuration file:</p>
<pre><code class="language-python"># feature_repo/feature_store.yaml
project: fraud_detection
registry: ../data/registry.db
provider: local
online_store:
  type: sqlite
  path: ../data/online_store.db
offline_store:
  type: file
entity_key_serialization_version: 3
</code></pre>
<p>This configuration:</p>
<ul>
<li><p>Names our project "fraud_detection"</p>
</li>
<li><p>Uses SQLite for the online store (for production, you'd use Redis or DynamoDB)</p>
</li>
<li><p>Uses local files for the offline store (for production, you'd use BigQuery or Snowflake)</p>
</li>
</ul>
<p>Now create the feature definitions:</p>
<pre><code class="language-python"># feature_repo/features.py
"""
Feast feature definitions for fraud detection.

This file defines:
- Entities: The keys we use to look up features (merchant_category)
- Data Sources: Where the raw feature data comes from (Parquet file)
- Feature Views: The features themselves and their schemas

The key insight: These definitions are the SINGLE SOURCE OF TRUTH.
Both training and serving use these exact definitions.
"""
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource, ValueType
from feast.types import Float32, Int64

# =============================================================================
# ENTITIES
# =============================================================================
# An entity is the "key" we use to look up features.
# For merchant-level features, the entity is merchant_category.

merchant = Entity(
    name="merchant_category",
    description="Merchant category for the transaction (for example, 'online', 'grocery')",
    value_type=ValueType.STRING,
)

# =============================================================================
# DATA SOURCES
# =============================================================================
# Data sources tell Feast where to find the raw feature data.
# For local development, we use a Parquet file.
# For production, this could be BigQuery, Snowflake, S3, etc.

merchant_stats_source = FileSource(
    name="merchant_stats_source",
    path="../data/merchant_features.parquet",  # We'll create this file
    timestamp_field="event_timestamp",       # Required for point-in-time joins
)

# =============================================================================
# FEATURE VIEWS
# =============================================================================
# A Feature View defines a group of related features.
# It specifies:
# - Which entity the features are for
# - The schema (names and types of features)
# - Where the data comes from
# - How long features are valid (TTL)

merchant_stats_fv = FeatureView(
    name="merchant_stats",
    description="Aggregated statistics per merchant category",
    entities=[merchant],
    ttl=timedelta(days=7),  # Features are valid for 7 days
    schema=[
        Field(name="avg_amount", dtype=Float32, description="Average transaction amount"),
        Field(name="transaction_count", dtype=Int64, description="Number of transactions"),
        Field(name="fraud_rate", dtype=Float32, description="Historical fraud rate"),
    ],
    source=merchant_stats_source,
    online=True,  # Enable online serving (low-latency retrieval)
)
</code></pre>
<h3 id="heading-44-materialize-features-to-online-store"><strong>4.4 Materialize Features to Online Store</strong></h3>
<p>Now we need to:</p>
<ol>
<li><p>Compute the features from our training data</p>
</li>
<li><p>Save them in a format Feast can read</p>
</li>
<li><p>Apply the Feast definitions</p>
</li>
<li><p>Materialize features to the online store</p>
</li>
</ol>
<p>Create <code>src/prepare_feast_features.py</code>:</p>
<pre><code class="language-python"># src/prepare_feast_features.py
"""
Prepare feature data for Feast.

This script:
1. Computes aggregated merchant features from training data
2. Saves them in Parquet format (Feast's offline store format)
3. Applies Feast feature definitions
4. Materializes features to the online store for low-latency serving

Run this whenever your training data changes or you want to refresh features.
"""
import pandas as pd
import numpy as np
from datetime import datetime
import subprocess
import os

def compute_merchant_features(df: pd.DataFrame) -&gt; pd.DataFrame:
    """
    Compute aggregated features by merchant category.
    
    THIS IS THE SINGLE SOURCE OF TRUTH FOR FEATURE COMPUTATION.
    
    Both training and serving will use features computed by this exact logic.
    Any change here automatically applies everywhere.
    
    Args:
        df: Transaction DataFrame with columns: amount, merchant_category, is_fraud
        
    Returns:
        DataFrame with computed features per merchant category
    """
    print("Computing merchant-level features...")
    
    # Group by merchant category and compute aggregates
    stats = df.groupby('merchant_category').agg({
        'amount': ['mean', 'count'],
        'is_fraud': 'mean'
    }).reset_index()
    
    # Flatten column names
    stats.columns = ['merchant_category', 'avg_amount', 'transaction_count', 'fraud_rate']
    
    # Add timestamp for Feast (required for point-in-time correct joins)
    stats['event_timestamp'] = datetime.now()
    
    # Convert types to match Feast schema
    stats['avg_amount'] = stats['avg_amount'].astype('float32')
    stats['transaction_count'] = stats['transaction_count'].astype('int64')
    stats['fraud_rate'] = stats['fraud_rate'].astype('float32')
    
    return stats

def main():
    print("="*60)
    print("FEAST FEATURE PREPARATION")
    print("="*60)
    
    # Load training data
    print("\n1. Loading training data...")
    train_df = pd.read_csv('data/train.csv')
    print(f"   Loaded {len(train_df):,} transactions")
    
    # Compute merchant features
    print("\n2. Computing merchant features...")
    merchant_features = compute_merchant_features(train_df)
    
    print("\n   Computed features:")
    print(merchant_features.to_string(index=False))
    
    # Save as Parquet (required format for Feast file source)
    print("\n3. Saving features to Parquet...")
    os.makedirs('data', exist_ok=True)
    output_path = 'data/merchant_features.parquet'
    merchant_features.to_parquet(output_path, index=False)
    print(f"   Saved to {output_path}")
    
    # Apply Feast feature definitions
    print("\n4. Applying Feast feature definitions...")
    try:
        result = subprocess.run(
            ['feast', 'apply'],
            cwd='feature_repo',
            capture_output=True,
            text=True,
            check=True
        )
        print("   Feature definitions applied successfully!")
        if result.stdout:
            print(f"   {result.stdout}")
    except subprocess.CalledProcessError as e:
        print(f"   Error applying Feast: {e.stderr}")
        raise
    
    # Materialize features to online store
    print("\n5. Materializing features to online store...")
    try:
        result = subprocess.run(
            ['feast', 'materialize-incremental', datetime.now().isoformat()],
            cwd='feature_repo',
            capture_output=True,
            text=True,
            check=True
        )
        print("   Features materialized successfully!")
        if result.stdout:
            print(f"   {result.stdout}")
    except subprocess.CalledProcessError as e:
        print(f"   Error materializing: {e.stderr}")
        raise
    
    print("\n" + "="*60)
    print("FEAST FEATURE PREPARATION COMPLETE!")
    print("="*60)
    print("\nYou can now:")
    print("  - Retrieve features for training: get_training_features()")
    print("  - Retrieve features for serving: get_online_features()")
    print("  - View feature stats: feast feature-views list")

if __name__ == "__main__":
    main()
</code></pre>
<p>Run the feature preparation:</p>
<pre><code class="language-python">python src/prepare_feast_features.py
</code></pre>
<p>You should see:</p>
<pre><code class="language-python">============================================================
FEAST FEATURE PREPARATION
============================================================

1. Loading training data... 8,000 transactions
2. Computing merchant features...
   grocery: avg=$31.24, fraud_rate=0.85%
   online: avg=$98.45, fraud_rate=4.87%
   restaurant: avg=$28.12, fraud_rate=0.50%
   retail: avg=$45.67, fraud_rate=1.02%
   travel: avg=$156.23, fraud_rate=4.18%
3. Saving to data/merchant_features.parquet ✓
4. Applying Feast definitions... ✓
5. Materializing to online store... ✓

FEAST FEATURE PREPARATION COMPLETE!
</code></pre>
<h3 id="heading-45-retrieve-features-for-training-and-serving"><strong>4.5 Retrieve Features for Training and Serving</strong></h3>
<p>Now let's create utilities to retrieve features consistently for both training and serving:</p>
<pre><code class="language-python"># src/feast_features.py
"""
Feast feature retrieval for training and serving.

This module provides functions to retrieve features from Feast:
- get_training_features(): For offline training (historical features)
- get_online_features(): For real-time serving (low-latency)

IMPORTANT: Both functions use the SAME feature definitions,
ensuring consistency between training and serving.
"""
import pandas as pd
from feast import FeatureStore
from datetime import datetime

# Initialize Feast store (points to our feature_repo)
store = FeatureStore(repo_path="feature_repo")

def get_training_features(df: pd.DataFrame) -&gt; pd.DataFrame:
    """
    Get features for training using Feast's offline store.
    
    Uses point-in-time correct joins to prevent data leakage.
    This means features are looked up as of the time each transaction occurred,
    not as of "now" - preventing you from accidentally using future data.
    
    Args:
        df: DataFrame with at least 'merchant_category' column
        
    Returns:
        DataFrame with original columns plus Feast features
    """
    print("Retrieving training features from Feast offline store...")
    
    # Prepare entity dataframe with timestamps
    # Each row needs: entity key(s) + event_timestamp
    entity_df = df[['merchant_category']].copy()
    entity_df['event_timestamp'] = datetime.now()  # See note below
    entity_df = entity_df.drop_duplicates()
    
    # ⚠️ Simplification: For clarity, we use the current timestamp here.
    # In real systems, this would be the actual event time of each transaction.
    
    # Retrieve historical features
    # Feast handles the point-in-time join automatically
    training_data = store.get_historical_features(
        entity_df=entity_df,
        features=[
            "merchant_stats:avg_amount",
            "merchant_stats:transaction_count",
            "merchant_stats:fraud_rate",
        ],
    ).to_df()
    
    # Merge features back with original dataframe
    result = df.merge(
        training_data[['merchant_category', 'avg_amount', 'transaction_count', 'fraud_rate']],
        on='merchant_category',
        how='left'
    )
    
    print(f"Retrieved features for {len(entity_df)} unique merchants")
    return result

def get_online_features(merchant_category: str) -&gt; dict:
    """
    Get features for real-time serving using Feast's online store.
    
    This is optimized for low-latency retrieval (milliseconds).
    Use this in your prediction API for real-time inference.
    
    Args:
        merchant_category: The merchant category to look up
        
    Returns:
        Dictionary with feature names and values
    """
    # Retrieve from online store (low-latency)
    feature_vector = store.get_online_features(
        features=[
            "merchant_stats:avg_amount",
            "merchant_stats:transaction_count",
            "merchant_stats:fraud_rate",
        ],
        entity_rows=[{"merchant_category": merchant_category}],
    ).to_dict()
    
    # Format the response
    return {
        'merchant_avg_amount': feature_vector['avg_amount'][0],
        'merchant_tx_count': feature_vector['transaction_count'][0],
        'merchant_fraud_rate': feature_vector['fraud_rate'][0],
    }

def get_online_features_batch(merchant_categories: list) -&gt; pd.DataFrame:
    """
    Get features for multiple merchants at once (batch serving).
    
    More efficient than calling get_online_features() in a loop.
    
    Args:
        merchant_categories: List of merchant categories to look up
        
    Returns:
        DataFrame with features for each merchant
    """
    feature_vector = store.get_online_features(
        features=[
            "merchant_stats:avg_amount",
            "merchant_stats:transaction_count",
            "merchant_stats:fraud_rate",
        ],
        entity_rows=[{"merchant_category": mc} for mc in merchant_categories],
    ).to_df()
    
    return feature_vector

if __name__ == "__main__":
    # Test the feature retrieval functions
    print("="*60)
    print("TESTING FEAST FEATURE RETRIEVAL")
    print("="*60)
    
    # Test offline retrieval (for training)
    print("\n1. Testing OFFLINE feature retrieval (for training)...")
    train_df = pd.read_csv('data/train.csv').head(10)
    enriched = get_training_features(train_df)
    print("\n   Sample enriched training data:")
    print(enriched[['amount', 'merchant_category', 'avg_amount', 'fraud_rate']].head())
    
    # Test online retrieval (for serving)
    print("\n2. Testing ONLINE feature retrieval (for serving)...")
    for category in ['online', 'grocery', 'travel', 'restaurant', 'retail']:
        features = get_online_features(category)
        print(f"   {category}: avg_amount=${features['merchant_avg_amount']:.2f}, "
              f"fraud_rate={features['merchant_fraud_rate']:.2%}")
    
    # Test batch retrieval
    print("\n3. Testing BATCH online retrieval...")
    batch_features = get_online_features_batch(['online', 'grocery', 'travel'])
    print(batch_features)
    
    print("\n" + "="*60)
    print("FEAST FEATURE RETRIEVAL TEST COMPLETE!")
    print("="*60)
</code></pre>
<p>Test the feature retrieval:</p>
<pre><code class="language-python">python src/feast_features.py
</code></pre>
<p>You should see:</p>
<pre><code class="language-python">============================================================
TESTING FEAST FEATURE RETRIEVAL
============================================================

1. Testing OFFLINE feature retrieval (for training)...
Retrieving training features from Feast offline store...
Retrieved features for 5 unique merchants

   Sample enriched training data:
   amount merchant_category  avg_amount  fraud_rate
    45.23           grocery       31.24      0.0085
   123.45            online       98.45      0.0487
    ...

2. Testing ONLINE feature retrieval (for serving)...
   online: avg_amount=$98.45, fraud_rate=4.87%
   grocery: avg_amount=$31.24, fraud_rate=0.85%
   travel: avg_amount=$156.23, fraud_rate=4.18%
   restaurant: avg_amount=$28.12, fraud_rate=0.50%
   retail: avg_amount=$45.67, fraud_rate=1.02%

3. Testing BATCH online retrieval...
  merchant_category  avg_amount  transaction_count  fraud_rate
               online       98.45               1234      0.0487
              grocery       31.24               2345      0.0085
               travel      156.23                478      0.0418
</code></pre>
<h3 id="heading-why-feast-over-custom-code"><strong>Why Feast Over Custom Code?</strong></h3>
<table>
<thead>
<tr>
<th><strong>Aspect</strong></th>
<th><strong>Custom Code</strong></th>
<th><strong>Feast</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Consistency</strong></td>
<td>Manual effort to keep in sync</td>
<td>Automatic - same definitions everywhere</td>
</tr>
<tr>
<td><strong>Point-in-time correctness</strong></td>
<td>Must implement yourself</td>
<td>Built-in</td>
</tr>
<tr>
<td><strong>Online serving</strong></td>
<td>Must build your own cache</td>
<td>Built-in online store</td>
</tr>
<tr>
<td><strong>Feature versioning</strong></td>
<td>Not supported</td>
<td>Built-in</td>
</tr>
<tr>
<td><strong>Scalability</strong></td>
<td>Limited</td>
<td>Production-ready (BigQuery, Redis, etc.)</td>
</tr>
<tr>
<td><strong>Team collaboration</strong></td>
<td>Difficult</td>
<td>Feature registry with documentation</td>
</tr>
<tr>
<td><strong>Monitoring</strong></td>
<td>Manual</td>
<td>Built-in feature statistics</td>
</tr>
</tbody></table>
<p>💡 <strong>Mental Model</strong>: Treat feature definitions like database schemas.<br>You wouldn't compute a column one way in your application and a different way in your reports. Features deserve the same discipline — define once, use everywhere.</p>
<p><strong>Checkpoint:</strong> After running <code>prepare_feast_</code><a href="http://features.py"><code>features.py</code></a>, you should have:</p>
<ul>
<li><p><code>data/merchant_features.parquet</code> (computed features)</p>
</li>
<li><p><code>data/registry.db</code> (Feast registry)</p>
</li>
<li><p><code>data/online_store.db</code> (SQLite online store)</p>
</li>
</ul>
<p>Running <code>python src/feast_</code><a href="http://features.py"><code>features.py</code></a> should successfully retrieve features for all merchant categories.</p>
<h2 id="heading-5-add-data-validation-with-great-expectations"><strong>5. Add Data Validation with Great Expectations</strong></h2>
<p><strong>What breaks without this:</strong> Your API accepts garbage input (negative amounts, invalid hours) and returns meaningless predictions. Worse, you have no idea it happened.</p>
<p>Recall that our API currently trusts input blindly. We saw how garbage data produces a prediction with no warning. <strong>Great Expectations</strong> is an open-source tool for data quality testing – defining rules (expectations) and testing data against them.</p>
<p><strong>Why This Matters:</strong> Data validation acts as a gatekeeper. Bad data is rejected <strong>before</strong> it can harm predictions. As the saying goes, "Garbage in, garbage out" – feeding unreliable data yields unreliable results. With validation, we transform this to "Garbage in, <strong>error out</strong>" – much better for debugging and reliability.</p>
<h3 id="heading-51-define-expectations"><strong>5.1 Define Expectations</strong></h3>
<p>What are reasonable expectations for our transaction data? Based on domain knowledge:</p>
<table>
<thead>
<tr>
<th><strong>Field</strong></th>
<th><strong>Expectation</strong></th>
<th><strong>Reason</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>amount</code></td>
<td>Positive (&gt; 0)</td>
<td>Negative transactions don't make sense</td>
</tr>
<tr>
<td><code>amount</code></td>
<td>Below $50,000</td>
<td>Extremely large amounts are outliers/errors</td>
</tr>
<tr>
<td><code>hour</code></td>
<td>0-23 inclusive</td>
<td>Valid hours in a day</td>
</tr>
<tr>
<td><code>day_of_week</code></td>
<td>0-6 inclusive</td>
<td>Valid days (Mon=0, Sun=6)</td>
</tr>
<tr>
<td><code>merchant_category</code></td>
<td>One of known categories</td>
<td>Must match training data</td>
</tr>
<tr>
<td>All fields</td>
<td>Not null</td>
<td>Required for prediction</td>
</tr>
</tbody></table>
<p>Create <code>src/data_validation.py</code>:</p>
<pre><code class="language-python"># src/data_validation.py
"""
Data validation for fraud detection.

This module provides functions to validate input data BEFORE making predictions.
Invalid data is rejected with clear error messages.

The key insight: It's better to reject bad input than to make garbage predictions.
"""
import pandas as pd
from typing import Dict, List, Any, Optional

# Define the valid merchant categories (must match training data!)
VALID_CATEGORIES = ["grocery", "restaurant", "retail", "online", "travel"]

def validate_transaction(data: Dict[str, Any]) -&gt; Dict[str, Any]:
    """
    Validate a single transaction for fraud prediction.
    
    Checks all business rules and data quality requirements.
    Returns a dictionary with 'valid' (bool) and 'errors' (list).
    
    Args:
        data: Dictionary with transaction fields
        
    Returns:
        {"valid": bool, "errors": list of error messages}
        
    Example:
        &gt;&gt;&gt; validate_transaction({"amount": -100, "hour": 25, ...})
        {"valid": False, "errors": ["amount must be positive", "hour must be 0-23"]}
    """
    errors = []
    
    # ==========================================================================
    # Amount Validation
    # ==========================================================================
    amount = data.get("amount")
    if amount is None:
        errors.append("amount is required")
    elif not isinstance(amount, (int, float)):
        errors.append(f"amount must be a number (got {type(amount).__name__})")
    elif amount &lt;= 0:
        errors.append("amount must be positive")
    elif amount &gt; 50000:
        errors.append(f"amount exceeds maximum allowed value of \(50,000 (got \){amount:,.2f})")
    
    # ==========================================================================
    # Hour Validation
    # ==========================================================================
    hour = data.get("hour")
    if hour is None:
        errors.append("hour is required")
    elif not isinstance(hour, int):
        errors.append(f"hour must be an integer (got {type(hour).__name__})")
    elif not (0 &lt;= hour &lt;= 23):
        errors.append(f"hour must be between 0 and 23 (got {hour})")
    
    # ==========================================================================
    # Day of Week Validation
    # ==========================================================================
    day = data.get("day_of_week")
    if day is None:
        errors.append("day_of_week is required")
    elif not isinstance(day, int):
        errors.append(f"day_of_week must be an integer (got {type(day).__name__})")
    elif not (0 &lt;= day &lt;= 6):
        errors.append(f"day_of_week must be between 0 (Monday) and 6 (Sunday) (got {day})")
    
    # ==========================================================================
    # Merchant Category Validation
    # ==========================================================================
    category = data.get("merchant_category")
    if category is None:
        errors.append("merchant_category is required")
    elif not isinstance(category, str):
        errors.append(f"merchant_category must be a string (got {type(category).__name__})")
    elif category not in VALID_CATEGORIES:
        errors.append(
            f"merchant_category must be one of {VALID_CATEGORIES} (got '{category}')"
        )
    
    return {
        "valid": len(errors) == 0,
        "errors": errors
    }

def validate_batch(df: pd.DataFrame) -&gt; Dict[str, Any]:
    """
    Validate a batch of transactions using Great Expectations.
    
    This is useful for validating training data or batch prediction requests.
    Uses Great Expectations for more sophisticated validation.
    
    Args:
        df: DataFrame with transaction data
        
    Returns:
        Dictionary with validation results
    """
    import great_expectations as gx
    
    # Convert to Great Expectations dataset
    ge_df = gx.from_pandas(df)
    
    results = []
    
    # Amount expectations
    r = ge_df.expect_column_values_to_be_between(
        'amount', min_value=0.01, max_value=50000, mostly=0.99
    )
    results.append(('amount_range', r.success, r.result))
    
    # Hour expectations
    r = ge_df.expect_column_values_to_be_between(
        'hour', min_value=0, max_value=23
    )
    results.append(('hour_range', r.success, r.result))
    
    # Day of week expectations
    r = ge_df.expect_column_values_to_be_between(
        'day_of_week', min_value=0, max_value=6
    )
    results.append(('day_range', r.success, r.result))
    
    # Merchant category expectations
    r = ge_df.expect_column_values_to_be_in_set(
        'merchant_category', VALID_CATEGORIES
    )
    results.append(('category_valid', r.success, r.result))
    
    # No nulls in critical fields
    for col in ['amount', 'hour', 'day_of_week', 'merchant_category']:
        r = ge_df.expect_column_values_to_not_be_null(col)
        results.append((f'{col}_not_null', r.success, r.result))
    
    # Summarize results
    passed = sum(1 for _, success, _ in results if success)
    total = len(results)
    
    return {
        'success': passed == total,
        'passed': passed,
        'total': total,
        'pass_rate': passed / total,
        'details': {name: {'passed': success, 'result': result} 
                   for name, success, result in results}
    }

if __name__ == "__main__":
    print("="*60)
    print("TESTING DATA VALIDATION")
    print("="*60)
    
    # Test single transaction validation
    print("\n1. Single Transaction Validation")
    print("-"*40)
    
    test_cases = [
        {
            "name": "Valid transaction",
            "data": {"amount": 50.0, "hour": 14, "day_of_week": 3, "merchant_category": "grocery"}
        },
        {
            "name": "Negative amount",
            "data": {"amount": -100.0, "hour": 14, "day_of_week": 3, "merchant_category": "grocery"}
        },
        {
            "name": "Invalid hour",
            "data": {"amount": 50.0, "hour": 25, "day_of_week": 3, "merchant_category": "grocery"}
        },
        {
            "name": "Unknown merchant",
            "data": {"amount": 50.0, "hour": 14, "day_of_week": 3, "merchant_category": "unknown"}
        },
        {
            "name": "Everything wrong",
            "data": {"amount": -999, "hour": 99, "day_of_week": 15, "merchant_category": "fake"}
        },
    ]
    
    for tc in test_cases:
        result = validate_transaction(tc["data"])
        status = "PASS" if result["valid"] else "FAIL"
        print(f"\n{tc['name']}: {status}")
        if result["errors"]:
            for error in result["errors"]:
                print(f"  - {error}")
    
    # Test batch validation
    print("\n\n2. Batch Validation with Great Expectations")
    print("-"*40)
    
    train_df = pd.read_csv('data/train.csv')
    results = validate_batch(train_df)
    
    print(f"\nTraining data validation: {results['passed']}/{results['total']} checks passed")
    print(f"Pass rate: {results['pass_rate']:.1%}")
    
    if not results['success']:
        print("\nFailed checks:")
        for name, detail in results['details'].items():
            if not detail['passed']:
                print(f"  - {name}")
</code></pre>
<h3 id="heading-when-to-use-which-validation-approach"><strong>When to Use Which Validation Approach</strong></h3>
<table>
<thead>
<tr>
<th><strong>Approach</strong></th>
<th><strong>Use Case</strong></th>
<th><strong>Latency</strong></th>
<th><strong>When to Use</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Custom Python</strong> (<code>validate_transaction</code>)</td>
<td>Real-time API requests</td>
<td>&lt;1ms</td>
<td>Every prediction request</td>
</tr>
<tr>
<td><strong>Great Expectations</strong></td>
<td>Batch data quality</td>
<td>Seconds</td>
<td>Training data, periodic audits, CI/CD</td>
</tr>
</tbody></table>
<p>We use <strong>both</strong> in this tutorial because they serve different purposes:</p>
<ul>
<li><p>Custom validation is your <strong>runtime gatekeeper</strong> — fast enough for every request</p>
</li>
<li><p>Great Expectations is your <strong>batch auditor</strong> — thorough checks on datasets</p>
</li>
</ul>
<h3 id="heading-52-integrate-validation-into-fastapi"><strong>5.2 Integrate Validation into FastAPI</strong></h3>
<p>Now let's update our API to reject invalid input with clear error messages:</p>
<pre><code class="language-python"># src/serve_validated.py
"""
Serve fraud detection model with input validation.

This version adds data validation BEFORE making predictions:
- Invalid inputs are rejected with HTTP 400 and clear error messages
- Valid inputs are processed and predictions returned

This is much safer than the naive version which accepted garbage.
"""
import pickle
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from src.data_validation import validate_transaction

# Load model
with open("models/model.pkl", "rb") as f:
    model, encoder = pickle.load(f)

app = FastAPI(
    title="Fraud Detection API (Validated)",
    description="""
    Fraud detection API with input validation.
    
    All inputs are validated before prediction:
    - amount: Must be positive and below $50,000
    - hour: Must be 0-23
    - day_of_week: Must be 0-6
    - merchant_category: Must be one of: grocery, restaurant, retail, online, travel
    
    Invalid inputs return HTTP 400 with detailed error messages.
    """,
    version="3.0.0"
)

class Transaction(BaseModel):
    amount: float = Field(..., description="Transaction amount (must be positive)", example=150.00)
    hour: int = Field(..., description="Hour of day (0-23)", example=14)
    day_of_week: int = Field(..., description="Day of week (0=Mon, 6=Sun)", example=3)
    merchant_category: str = Field(..., description="Merchant type", example="online")

class PredictionResponse(BaseModel):
    is_fraud: bool
    fraud_probability: float
    validation_passed: bool = True

class ValidationErrorResponse(BaseModel):
    detail: dict

@app.post("/predict", response_model=PredictionResponse, responses={400: {"model": ValidationErrorResponse}})
def predict(tx: Transaction):
    """
    Predict whether a transaction is fraudulent.
    
    Input is validated before prediction. Invalid inputs return HTTP 400.
    """
    data = tx.dict()
    
    # VALIDATE INPUT BEFORE MAKING PREDICTION
    validation = validate_transaction(data)
    
    if not validation["valid"]:
        raise HTTPException(
            status_code=400,
            detail={
                "message": "Validation failed",
                "errors": validation["errors"],
                "input": data
            }
        )
    
    # Input is valid - make prediction
    data["merchant_encoded"] = encoder.transform([data["merchant_category"]])[0]
    X = [[data["amount"], data["hour"], data["day_of_week"], data["merchant_encoded"]]]
    
    pred = model.predict(X)[0]
    prob = model.predict_proba(X)[0][1]
    
    return PredictionResponse(
        is_fraud=bool(pred),
        fraud_probability=round(float(prob), 4),
        validation_passed=True
    )

@app.get("/health")
def health():
    return {"status": "healthy", "validation": "enabled"}
</code></pre>
<p>Start the validated API:</p>
<pre><code class="language-python">uvicorn src.serve_validated:app --reload --host 0.0.0.0 --port 8000
</code></pre>
<p>Now test with bad data:</p>
<pre><code class="language-python">curl -X POST "http://localhost:8000/predict" \
  -H "Content-Type: application/json" \
  -d '{"amount": -500, "hour": 25, "day_of_week": 10, "merchant_category": "fake"}'
</code></pre>
<p>Response (HTTP 400):</p>
<pre><code class="language-python">{
  "detail": {
    "message": "Validation failed",
    "errors": [
      "amount must be positive",
      "hour must be between 0 and 23 (got 25)",
      "day_of_week must be between 0 (Monday) and 6 (Sunday) (got 10)",
      "merchant_category must be one of ['grocery', 'restaurant', 'retail', 'online', 'travel'] (got 'fake')"
    ],
    "input": {"amount": -500, "hour": 25, "day_of_week": 10, "merchant_category": "fake"}
  }
}
</code></pre>
<p><strong>This is a huge improvement!</strong> Instead of silently accepting garbage and returning meaningless predictions, we now:</p>
<ul>
<li><p>Reject invalid input immediately</p>
</li>
<li><p>Provide clear, actionable error messages</p>
</li>
<li><p>Return the original input for debugging</p>
</li>
<li><p>Use proper HTTP status codes (400 for client error)</p>
</li>
</ul>
<p><strong>Checkpoint:</strong> Your validated API should:</p>
<ul>
<li><p>Accept valid transactions and return predictions</p>
</li>
<li><p>Reject invalid transactions with HTTP 400 and detailed error messages</p>
</li>
<li><p>Show validation errors for each invalid field</p>
</li>
</ul>
<h2 id="heading-6-monitor-model-performance-and-data-drift"><strong>6. Monitor Model Performance and Data Drift</strong></h2>
<p><strong>What breaks without this:</strong> Your model's accuracy drops from 98% to 70% over two months. Nobody notices until customers complain. By then, significant damage has occurred.</p>
<p>Even with a great model and clean input data, <strong>time can be an enemy</strong>. Model performance can decline as real-world data evolves – this is known as <strong>model drift</strong> or <strong>model decay</strong>.</p>
<p><strong>Why This Matters:</strong> In traditional software, you monitor CPU, memory, error rates, and response times. In ML, you must <strong>also</strong> monitor:</p>
<ul>
<li><p>Data quality (are inputs within expected ranges?)</p>
</li>
<li><p>Model performance (is accuracy holding up?)</p>
</li>
<li><p>Data drift (has input distribution changed?)</p>
</li>
<li><p>Prediction drift (has the distribution of predictions changed?)</p>
</li>
</ul>
<p>Without monitoring, your model could be silently failing for weeks before anyone notices. By then, significant damage may have occurred – fraud slipping through, good customers blocked, revenue lost.</p>
<h3 id="heading-61-the-four-pillars-of-ml-observability"><strong>6.1 The Four Pillars of ML Observability</strong></h3>
<table>
<thead>
<tr>
<th><strong>Pillar</strong></th>
<th><strong>What to Monitor</strong></th>
<th><strong>Why It Matters</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Data Quality</strong></td>
<td>Are inputs valid? Nulls? Outliers?</td>
<td>Bad data causes bad predictions</td>
</tr>
<tr>
<td><strong>Model Performance</strong></td>
<td>Accuracy, precision, recall, F1</td>
<td>Is the model still working?</td>
</tr>
<tr>
<td><strong>Data Drift</strong></td>
<td>Has input distribution changed from training?</td>
<td>Model may not generalize to new data</td>
</tr>
<tr>
<td><strong>Prediction Drift</strong></td>
<td>Has prediction distribution changed?</td>
<td>May indicate data or concept drift</td>
</tr>
</tbody></table>
<h3 id="heading-62-build-a-drift-monitor-with-evidently"><strong>6.2 Build a Drift Monitor with Evidently</strong></h3>
<p><strong>Evidently</strong> is an open-source library specifically designed for ML monitoring. It can detect drift, generate reports, and integrate with monitoring systems.</p>
<p>Create <code>src/monitoring.py</code>:</p>
<pre><code class="language-python"># src/monitoring.py
"""
Model monitoring with Evidently.

This module provides tools to:
1. Detect data drift between training and production data
2. Generate detailed HTML reports
3. Track drift over time
4. Alert when drift exceeds thresholds

In production, you would run drift checks periodically (hourly, daily)
and alert when significant drift is detected.
"""
import pandas as pd
import numpy as np
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, TargetDriftPreset
from evidently.metrics import (
    DatasetDriftMetric,
    DataDriftTable,
    ColumnDriftMetric
)
from datetime import datetime
from typing import List, Dict, Any, Optional

class DriftMonitor:
    """
    Monitor for detecting data drift between reference (training) and current data.
    
    Implementation Note: We use two approaches here:
    1. Scipy's KS-test — A lightweight statistical method that works anywhere (our fallback)
    2. Evidently — A full-featured library with beautiful reports (our primary tool)
    
    The KS-test is included as defensive coding — if Evidently fails to generate 
    a report, we still get drift detection.
    
    Usage:
        monitor = DriftMonitor(training_data)
        result = monitor.check_drift(production_data)
        if result['drift_detected']:
            alert("Drift detected!")
    """
    
    def __init__(self, reference_data: pd.DataFrame, feature_columns: Optional[List[str]] = None):
        """
        Initialize the drift monitor with reference (training) data.
        
        Args:
            reference_data: The training data to compare against
            feature_columns: Columns to monitor (default: all numeric columns)
        """
        self.reference = reference_data
        self.feature_columns = feature_columns or reference_data.select_dtypes(
            include=[np.number]
        ).columns.tolist()
        self.history: List[Dict[str, Any]] = []
        
        print(f"Drift monitor initialized with {len(self.reference):,} reference samples")
        print(f"Monitoring columns: {self.feature_columns}")
    
    def check_drift(self, current_data: pd.DataFrame, threshold: float = 0.1) -&gt; Dict[str, Any]:
        """
        Check for drift between reference and current data.
        
        Args:
            current_data: Current/production data to check
            threshold: Drift share threshold for alerting (default 10%)
            
        Returns:
            Dictionary with drift results
        """
        from scipy import stats
        
        ref_subset = self.reference[self.feature_columns]
        cur_subset = current_data[self.feature_columns]
        
        # Simple statistical drift detection using KS test
        drifted_columns = []
        for col in self.feature_columns:
            statistic, p_value = stats.ks_2samp(
                ref_subset[col].dropna(),
                cur_subset[col].dropna()
            )
            if p_value &lt; 0.05:  # 5% significance level
                drifted_columns.append(col)
        
        n_features = len(self.feature_columns)
        n_drifted = len(drifted_columns)
        drift_share = n_drifted / n_features if n_features &gt; 0 else 0
        
        result = {
            'timestamp': datetime.now().isoformat(),
            'drift_detected': n_drifted &gt; 0,
            'drift_share': drift_share,
            'drifted_columns': drifted_columns,
            'n_features': n_features,
            'n_drifted': n_drifted,
            'current_samples': len(current_data),
            'threshold': threshold,
            'alert': drift_share &gt; threshold
        }
        
        self.history.append(result)
        
        return result
    
    def generate_report(self, current_data: pd.DataFrame, output_path: str = "drift_report.html"):
        """
        Generate a detailed HTML drift report using Evidently.
        
        Opens in browser for visual inspection of drift patterns.
        """
        ref_subset = self.reference[self.feature_columns]
        cur_subset = current_data[self.feature_columns]
        
        try:
            report = Report(metrics=[DataDriftPreset()])
            report.run(reference_data=ref_subset, current_data=cur_subset)
            
            # Save HTML report
            with open(output_path, 'w') as f:
                f.write(report.show(mode='inline').data)
            
            print(f"Drift report saved to {output_path}")
            print(f"Open this file in a browser to view detailed visualizations.")
        except Exception as e:
            print(f"Could not generate Evidently report: {e}")
            print(f"Using simplified drift detection instead.")
    
    def get_alerts(self, threshold: float = 0.1) -&gt; List[Dict[str, Any]]:
        """
        Get all alerts from history where drift exceeded threshold.
        """
        return [
            {
                'timestamp': r['timestamp'],
                'severity': 'HIGH' if r['drift_share'] &gt; 0.3 else 'MEDIUM',
                'drift_share': r['drift_share'],
                'message': f"Drift detected: {r['drift_share']:.1%} of features drifted",
                'drifted_columns': r['drifted_columns']
            }
            for r in self.history
            if r['drift_share'] &gt; threshold
        ]
    
    def summary(self) -&gt; Dict[str, Any]:
        """Get summary statistics from monitoring history."""
        if not self.history:
            return {"message": "No drift checks performed yet"}
        
        drift_shares = [r['drift_share'] for r in self.history]
        alerts = [r for r in self.history if r['alert']]
        
        return {
            'total_checks': len(self.history),
            'total_alerts': len(alerts),
            'avg_drift_share': np.mean(drift_shares),
            'max_drift_share': np.max(drift_shares),
            'first_check': self.history[0]['timestamp'],
            'last_check': self.history[-1]['timestamp']
        }


def simulate_drift_scenarios():
    """
    Demonstrate drift detection with different scenarios.
    
    This simulates what happens when production data differs from training data.
    """
    from src.generate_data import generate_transactions
    
    print("="*70)
    print("DRIFT DETECTION SIMULATION")
    print("="*70)
    
    # Load reference (training) data
    print("\n1. Loading reference data (training set)...")
    reference = pd.read_csv('data/train.csv')
    feature_cols = ['amount', 'hour', 'day_of_week']
    
    # Initialize drift monitor
    monitor = DriftMonitor(reference, feature_cols)
    
    # Scenario 1: Similar data (should show minimal drift)
    print("\n" + "-"*70)
    print("SCENARIO 1: Test data (similar distribution)")
    print("-"*70)
    test_data = pd.read_csv('data/test.csv')
    result = monitor.check_drift(test_data)
    print(f"  Drift detected: {result['drift_detected']}")
    print(f"  Drift share: {result['drift_share']:.1%}")
    print(f"  Drifted columns: {result['drifted_columns']}")
    print(f"  Alert triggered: {result['alert']}")
    
    # Scenario 2: Fraud spike (10% fraud instead of 2%)
    print("\n" + "-"*70)
    print("SCENARIO 2: Fraud spike (10% fraud rate instead of 2%)")
    print("-"*70)
    fraud_spike = generate_transactions(n_samples=2000, fraud_ratio=0.10, seed=101)
    result = monitor.check_drift(fraud_spike)
    print(f"  Drift detected: {result['drift_detected']}")
    print(f"  Drift share: {result['drift_share']:.1%}")
    print(f"  Drifted columns: {result['drifted_columns']}")
    print(f"  Alert triggered: {result['alert']}")
    
    # Scenario 3: Amount inflation (everything costs more)
    print("\n" + "-"*70)
    print("SCENARIO 3: Amount inflation (2x multiplier)")
    print("-"*70)
    inflated = test_data.copy()
    inflated['amount'] = inflated['amount'] * 2
    result = monitor.check_drift(inflated)
    print(f"  Drift detected: {result['drift_detected']}")
    print(f"  Drift share: {result['drift_share']:.1%}")
    print(f"  Drifted columns: {result['drifted_columns']}")
    print(f"  Alert triggered: {result['alert']}")
    
    # Scenario 4: Time shift (more late-night transactions)
    print("\n" + "-"*70)
    print("SCENARIO 4: Time shift (mostly late-night transactions)")
    print("-"*70)
    night_shift = test_data.copy()
    night_shift['hour'] = np.random.choice([0, 1, 2, 3, 22, 23], size=len(night_shift))
    result = monitor.check_drift(night_shift)
    print(f"  Drift detected: {result['drift_detected']}")
    print(f"  Drift share: {result['drift_share']:.1%}")
    print(f"  Drifted columns: {result['drifted_columns']}")
    print(f"  Alert triggered: {result['alert']}")
    
    # Generate detailed report for the most drifted scenario
    print("\n" + "-"*70)
    print("GENERATING DETAILED REPORT")
    print("-"*70)
    monitor.generate_report(night_shift, "drift_report.html")
    
    # Print summary
    print("\n" + "-"*70)
    print("MONITORING SUMMARY")
    print("-"*70)
    summary = monitor.summary()
    print(f"  Total checks: {summary['total_checks']}")
    print(f"  Total alerts: {summary['total_alerts']}")
    print(f"  Average drift share: {summary['avg_drift_share']:.1%}")
    print(f"  Maximum drift share: {summary['max_drift_share']:.1%}")
    
    # Print alerts
    alerts = monitor.get_alerts()
    if alerts:
        print(f"\n  Alerts ({len(alerts)}):")
        for alert in alerts:
            print(f"    [{alert['severity']}] {alert['message']}")
    
    print("\n" + "="*70)
    print("DRIFT DETECTION SIMULATION COMPLETE")
    print("="*70)
    print("\nOpen drift_report.html in your browser to see detailed visualizations!")


if __name__ == "__main__":
    simulate_drift_scenarios()
</code></pre>
<p>Run the drift simulation:</p>
<pre><code class="language-python">python src/monitoring.py
</code></pre>
<p>You'll see output showing how drift detection works in different scenarios. Then open <code>drift_report.html</code> in your browser to see beautiful visualizations of the drift patterns.</p>
<h3 id="heading-63-production-monitoring-strategy"><strong>6.3 Production Monitoring Strategy</strong></h3>
<p>In a production environment, you would:</p>
<ol>
<li><p><strong>Log all predictions</strong> to a database or data warehouse</p>
</li>
<li><p><strong>Run drift checks periodically</strong> (hourly for high-traffic systems, daily for lower traffic)</p>
</li>
<li><p><strong>Set up alerts</strong> when drift exceeds thresholds (integrate with PagerDuty, Slack, etc.)</p>
</li>
<li><p><strong>Trigger retraining</strong> if drift is severe or sustained</p>
</li>
<li><p><strong>Create dashboards</strong> to track drift over time (Grafana, Datadog, etc.)</p>
</li>
</ol>
<p><strong>Checkpoint:</strong> Running <code>python src/</code><a href="http://monitoring.py"><code>monitoring.py</code></a> should:</p>
<ul>
<li><p>Show minimal drift for similar data (test set)</p>
</li>
<li><p>Show significant drift for modified data (fraud spike, inflation, time shift)</p>
</li>
<li><p>Generate an HTML report that you can view in your browser</p>
</li>
</ul>
<h2 id="heading-7-automate-testing-and-deployment-with-cicd"><strong>7. Automate Testing and Deployment with CI/CD</strong></h2>
<p><strong>What breaks without this:</strong> A typo in your code breaks the API. You deploy on Friday at 5 PM. Nobody notices until Monday. Fraud losses spike over the weekend.</p>
<p><strong>CI/CD</strong> (Continuous Integration/Continuous Deployment) ensures reliable, repeatable releases. As JFrog notes: <em>"A strong CI/CD pipeline enables ML teams to build robust, bug-free models more quickly and efficiently."</em></p>
<p><strong>Why This Matters:</strong> In ML, changes aren't just code – they're also data and models. CI/CD ensures that when you change training logic, data preprocessing, or hyperparameters, tests verify the change doesn't break anything before it reaches production. It's the difference between deploying with confidence and deploying with crossed fingers.</p>
<h3 id="heading-71-write-tests-for-data-and-model"><strong>7.1 Write Tests for Data and Model</strong></h3>
<p>Create <code>tests/test_data_and_</code><a href="http://model.py"><code>model.py</code></a>:</p>
<pre><code class="language-python"># tests/test_data_and_model.py
"""
Tests for data quality and model performance.

These tests run in CI/CD to ensure:
1. Data meets quality requirements
2. Model meets performance thresholds
3. No regressions are introduced

Run with: pytest tests/test_data_and_model.py -v
"""
import pandas as pd
import pickle
import pytest
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score

class TestDataQuality:
    """Tests for training data quality."""
    
    @pytest.fixture
    def train_data(self):
        return pd.read_csv("data/train.csv")
    
    @pytest.fixture
    def test_data(self):
        return pd.read_csv("data/test.csv")
    
    def test_train_data_has_expected_columns(self, train_data):
        """Training data must have all required columns."""
        required_columns = {"amount", "hour", "day_of_week", "merchant_category", "is_fraud"}
        actual_columns = set(train_data.columns)
        missing = required_columns - actual_columns
        assert not missing, f"Missing columns: {missing}"
    
    def test_train_data_not_empty(self, train_data):
        """Training data must have rows."""
        assert len(train_data) &gt; 0, "Training data is empty"
        assert len(train_data) &gt;= 1000, f"Training data too small: {len(train_data)} rows"
    
    def test_no_negative_amounts(self, train_data):
        """Transaction amounts must be non-negative."""
        negative_count = (train_data["amount"] &lt; 0).sum()
        assert negative_count == 0, f"Found {negative_count} negative amounts"
    
    def test_amounts_reasonable(self, train_data):
        """Transaction amounts should be within reasonable bounds."""
        max_amount = train_data["amount"].max()
        assert max_amount &lt;= 100000, f"Max amount {max_amount} exceeds reasonable limit"
    
    def test_hours_valid(self, train_data):
        """Hours must be 0-23."""
        invalid = train_data[(train_data["hour"] &lt; 0) | (train_data["hour"] &gt; 23)]
        assert len(invalid) == 0, f"Found {len(invalid)} invalid hours"
    
    def test_days_valid(self, train_data):
        """Days of week must be 0-6."""
        invalid = train_data[(train_data["day_of_week"] &lt; 0) | (train_data["day_of_week"] &gt; 6)]
        assert len(invalid) == 0, f"Found {len(invalid)} invalid days"
    
    def test_merchant_categories_valid(self, train_data):
        """Merchant categories must be from known set."""
        valid_categories = {"grocery", "restaurant", "retail", "online", "travel"}
        actual_categories = set(train_data["merchant_category"].unique())
        invalid = actual_categories - valid_categories
        assert not invalid, f"Invalid merchant categories: {invalid}"
    
    def test_fraud_ratio_reasonable(self, train_data):
        """Fraud ratio should be realistic (between 0.1% and 50%)."""
        fraud_ratio = train_data["is_fraud"].mean()
        assert 0.001 &lt;= fraud_ratio &lt;= 0.5, f"Fraud ratio {fraud_ratio:.2%} is unrealistic"
    
    def test_no_nulls_in_critical_columns(self, train_data):
        """Critical columns must not have null values."""
        critical = ["amount", "hour", "day_of_week", "merchant_category", "is_fraud"]
        for col in critical:
            null_count = train_data[col].isnull().sum()
            assert null_count == 0, f"Column {col} has {null_count} null values"


class TestModelPerformance:
    """Tests for model performance thresholds."""
    
    @pytest.fixture
    def model_and_encoder(self):
        with open("models/model.pkl", "rb") as f:
            return pickle.load(f)
    
    @pytest.fixture
    def test_data(self):
        return pd.read_csv("data/test.csv")
    
    def test_model_loads_successfully(self, model_and_encoder):
        """Model file must load without errors."""
        model, encoder = model_and_encoder
        assert model is not None, "Model is None"
        assert encoder is not None, "Encoder is None"
    
    def test_model_can_predict(self, model_and_encoder, test_data):
        """Model must be able to make predictions."""
        model, encoder = model_and_encoder
        test_data["merchant_encoded"] = encoder.transform(test_data["merchant_category"])
        X = test_data[["amount", "hour", "day_of_week", "merchant_encoded"]]
        predictions = model.predict(X)
        assert len(predictions) == len(X), "Prediction count mismatch"
    
    def test_accuracy_threshold(self, model_and_encoder, test_data):
        """Model accuracy must be at least 90%."""
        model, encoder = model_and_encoder
        test_data["merchant_encoded"] = encoder.transform(test_data["merchant_category"])
        X = test_data[["amount", "hour", "day_of_week", "merchant_encoded"]]
        y = test_data["is_fraud"]
        accuracy = model.score(X, y)
        assert accuracy &gt;= 0.90, f"Accuracy {accuracy:.2%} below 90% threshold"
    
    def test_f1_threshold(self, model_and_encoder, test_data):
        """Model F1-score must be at least 0.3 (sanity check for imbalanced data)."""
        model, encoder = model_and_encoder
        test_data["merchant_encoded"] = encoder.transform(test_data["merchant_category"])
        X = test_data[["amount", "hour", "day_of_week", "merchant_encoded"]]
        y = test_data["is_fraud"]
        y_pred = model.predict(X)
        f1 = f1_score(y, y_pred)
        assert f1 &gt;= 0.3, f"F1-score {f1:.2f} below 0.3 threshold"
    
    def test_precision_not_zero(self, model_and_encoder, test_data):
        """Model precision must be greater than 0 (catches at least some fraud)."""
        model, encoder = model_and_encoder
        test_data["merchant_encoded"] = encoder.transform(test_data["merchant_category"])
        X = test_data[["amount", "hour", "day_of_week", "merchant_encoded"]]
        y = test_data["is_fraud"]
        y_pred = model.predict(X)
        precision = precision_score(y, y_pred, zero_division=0)
        assert precision &gt; 0, "Model has zero precision (predicts no fraud)"
    
    def test_recall_not_zero(self, model_and_encoder, test_data):
        """Model recall must be greater than 0 (catches at least some fraud)."""
        model, encoder = model_and_encoder
        test_data["merchant_encoded"] = encoder.transform(test_data["merchant_category"])
        X = test_data[["amount", "hour", "day_of_week", "merchant_encoded"]]
        y = test_data["is_fraud"]
        y_pred = model.predict(X)
        recall = recall_score(y, y_pred, zero_division=0)
        assert recall &gt; 0, "Model has zero recall (misses all fraud)"
</code></pre>
<p>Create <code>tests/test_</code><a href="http://api.py"><code>api.py</code></a>:</p>
<pre><code class="language-python"># tests/test_api.py
"""
Tests for the FastAPI prediction service.

These tests ensure the API:
1. Returns correct responses for valid inputs
2. Rejects invalid inputs with proper error messages
3. Health check works

Run with: pytest tests/test_api.py -v
Note: Requires the API to be running on localhost:8000
"""
import pytest
import httpx

BASE_URL = "http://localhost:8000"

class TestPredictionEndpoint:
    """Tests for the /predict endpoint."""
    
    def test_valid_prediction_returns_200(self):
        """Valid input should return HTTP 200 with prediction."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": 100.0,
            "hour": 14,
            "day_of_week": 3,
            "merchant_category": "online"
        }, timeout=10)
        
        assert response.status_code == 200
        data = response.json()
        assert "is_fraud" in data
        assert "fraud_probability" in data
        assert isinstance(data["is_fraud"], bool)
        assert 0 &lt;= data["fraud_probability"] &lt;= 1
    
    def test_high_risk_transaction(self):
        """High-risk transaction should have higher fraud probability."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": 500.0,
            "hour": 3,  # Late night
            "day_of_week": 1,
            "merchant_category": "online"
        }, timeout=10)
        
        assert response.status_code == 200
        data = response.json()
        # High-risk transactions should have elevated probability
        # (not asserting exact value as model may vary)
        assert data["fraud_probability"] &gt;= 0.0
    
    def test_negative_amount_rejected(self):
        """Negative amount should be rejected with 400."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": -100.0,
            "hour": 14,
            "day_of_week": 3,
            "merchant_category": "online"
        }, timeout=10)
        
        assert response.status_code == 400
        assert "errors" in response.json()["detail"]
    
    def test_invalid_hour_rejected(self):
        """Invalid hour should be rejected with 400."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": 100.0,
            "hour": 25,  # Invalid
            "day_of_week": 3,
            "merchant_category": "online"
        }, timeout=10)
        
        assert response.status_code == 400
    
    def test_invalid_merchant_rejected(self):
        """Unknown merchant category should be rejected with 400."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": 100.0,
            "hour": 14,
            "day_of_week": 3,
            "merchant_category": "unknown_category"
        }, timeout=10)
        
        assert response.status_code == 400
    
    def test_missing_field_rejected(self):
        """Missing required field should be rejected."""
        response = httpx.post(f"{BASE_URL}/predict", json={
            "amount": 100.0,
            "hour": 14
            # Missing day_of_week and merchant_category
        }, timeout=10)
        
        assert response.status_code == 422  # Pydantic validation error


class TestHealthEndpoint:
    """Tests for the /health endpoint."""
    
    def test_health_returns_200(self):
        """Health endpoint should return 200."""
        response = httpx.get(f"{BASE_URL}/health", timeout=10)
        assert response.status_code == 200
    
    def test_health_returns_healthy_status(self):
        """Health endpoint should indicate healthy status."""
        response = httpx.get(f"{BASE_URL}/health", timeout=10)
        data = response.json()
        assert data["status"] == "healthy"
</code></pre>
<p>Run tests locally:</p>
<pre><code class="language-python"># Run data and model tests (API not needed)
pytest tests/test_data_and_model.py -v

# Run API tests (requires API to be running)
pytest tests/test_api.py -v
</code></pre>
<h3 id="heading-72-github-actions-workflow"><strong>7.2 GitHub Actions Workflow</strong></h3>
<p>⚠️ <strong>Note for Production Teams</strong><br>In real ML teams, you typically don't retrain full models inside CI — it's slow and resource-intensive.<br>Here we do it to keep everything local, reproducible, and self-contained for learning.<br>Production pipelines usually separate training (scheduled jobs) from testing (CI/CD).</p>
<p>Create <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="language-python"># .github/workflows/ci.yml
name: ML Pipeline CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: 'pip'
      
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
      
      - name: Generate training data
        run: python src/generate_data.py
      
      - name: Train model
        run: python src/train_naive.py
      
      - name: Run data quality tests
        run: pytest tests/test_data_and_model.py -v --tb=short
      
      - name: Build Docker image
        run: docker build -t fraud-detection-api .
      
      - name: Run container for API tests
        run: |
          docker run -d -p 8000:8000 --name test-api fraud-detection-api
          sleep 10  # Wait for API to start
          curl -f http://localhost:8000/health || exit 1
      
      - name: Run API tests
        run: pytest tests/test_api.py -v --tb=short
      
      - name: Cleanup
        if: always()
        run: docker stop test-api || true
</code></pre>
<h3 id="heading-73-dockerize-the-application"><strong>7.3 Dockerize the Application</strong></h3>
<p>Create <code>Dockerfile</code>:</p>
<pre><code class="language-python"># Dockerfile
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    curl \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY src/ src/
COPY models/ models/
COPY data/ data/

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# Run the API
CMD ["uvicorn", "src.serve_validated:app", "--host", "0.0.0.0", "--port", "8000"]
</code></pre>
<p>Create <code>.dockerignore</code>:</p>
<pre><code class="language-python"># .dockerignore
venv/
__pycache__/
*.pyc
.git/
.github/
mlruns/
*.db
*.html
.pytest_cache/
</code></pre>
<p>Build and run locally:</p>
<pre><code class="language-python"># Build the Docker image
docker build -t fraud-detection-api .

# Run the container
docker run -p 8000:8000 fraud-detection-api

# Test it
curl http://localhost:8000/health
</code></pre>
<p><strong>Checkpoint:</strong></p>
<ul>
<li><p>All tests pass: <code>pytest tests/test_data_and_</code><a href="http://model.py"><code>model.py</code></a> <code>-v</code></p>
</li>
<li><p>Docker image builds successfully</p>
</li>
<li><p>Container runs and responds to health checks</p>
</li>
</ul>
<h2 id="heading-8-incident-response-playbook"><strong>8. Incident Response Playbook</strong></h2>
<p>When things go wrong in production (and they will), you need a plan. This section provides playbooks for common ML incidents.</p>
<h3 id="heading-scenario-false-positive-spike"><strong>Scenario: False Positive Spike</strong></h3>
<p><strong>Symptoms:</strong> Your fraud model suddenly flags 40% of legitimate transactions as fraud, blocking customers and overwhelming your manual review team.</p>
<p><strong>Severity:</strong> HIGH - Direct customer impact</p>
<p><strong>Phase 1: Mitigation (0-5 minutes)</strong></p>
<ol>
<li><p><strong>Acknowledge the incident</strong> - Notify stakeholders that you're aware and responding</p>
</li>
<li><p><strong>Roll back to previous model</strong> - In MLflow UI, move the @champion alias to the previous model version</p>
</li>
<li><p><strong>Restart the API</strong> - <code>docker restart fraud-api</code> or redeploy</p>
</li>
<li><p><strong>Verify</strong> - Check that false positive rate has returned to normal</p>
</li>
<li><p><strong>Communicate</strong> - "Issue detected and mitigated. Investigating root cause."</p>
</li>
</ol>
<p><strong>Phase 2: Diagnosis (5-60 minutes)</strong></p>
<ol>
<li><p><strong>Check drift report</strong> - Run <code>python src/</code><a href="http://monitoring.py"><code>monitoring.py</code></a> with recent production data</p>
</li>
<li><p><strong>Check data validation logs</strong> - Did upstream data format change?</p>
</li>
<li><p><strong>Check recent deployments</strong> - Was there a new model or code deployed recently?</p>
</li>
<li><p><strong>Compare metrics</strong> - What's different between the rolled-back and problematic model?</p>
</li>
</ol>
<p><strong>Example root causes:</strong></p>
<ul>
<li><p>Upstream system sent amounts in cents instead of dollars</p>
</li>
<li><p>New merchant category appeared that wasn't in training data</p>
</li>
<li><p>Holiday shopping patterns differed significantly from training data</p>
</li>
</ul>
<p><strong>Phase 3: Remediation (1-24 hours)</strong></p>
<ol>
<li><p><strong>Fix the root cause</strong> - Add validation for the edge case, or update training data</p>
</li>
<li><p><strong>Retrain if needed</strong> - Include new patterns in training data</p>
</li>
<li><p><strong>Add test case</strong> - Prevent this from happening again</p>
</li>
<li><p><strong>Document</strong> - Add to runbook for future reference</p>
</li>
</ol>
<h3 id="heading-scenario-gradual-performance-decay"><strong>Scenario: Gradual Performance Decay</strong></h3>
<p><strong>Symptoms:</strong> Monitoring shows fraud recall dropping 2% per week over a month. No sudden failures, just slow degradation.</p>
<p><strong>Severity:</strong> MEDIUM - Gradual impact, time to respond</p>
<p><strong>Response:</strong></p>
<ol>
<li><p><strong>Investigate drift report</strong> - Look for gradual distribution changes</p>
<pre><code class="language-python">python src/monitoring.py
</code></pre>
</li>
<li><p><strong>Collect recent labeled data</strong> - Get confirmed fraud cases from the past month</p>
</li>
<li><p><strong>Analyze patterns</strong> - What's different about recent fraud?</p>
<ul>
<li><p>New attack vectors?</p>
</li>
<li><p>Different time patterns?</p>
</li>
<li><p>New merchant categories?</p>
</li>
</ul>
</li>
<li><p><strong>Retrain on combined data</strong> - Include both old and new patterns</p>
<pre><code class="language-python">python src/train_mlflow.py
</code></pre>
</li>
<li><p><strong>Deploy via canary</strong> - Route 10% of traffic to the new model first</p>
<ul>
<li><p>Monitor metrics for 1-2 days</p>
</li>
<li><p>If metrics improve, increase to 50%, then 100%</p>
</li>
<li><p>If metrics worsen, roll back</p>
</li>
</ul>
</li>
<li><p><strong>Set up recurring retraining</strong> - Schedule weekly or monthly retraining</p>
</li>
</ol>
<h3 id="heading-scenario-upstream-data-schema-change"><strong>Scenario: Upstream Data Schema Change</strong></h3>
<p><strong>Symptoms:</strong> API starts returning 500 errors. Logs show <code>KeyError: 'merchant_category'</code>.</p>
<p><strong>Severity:</strong> HIGH - Service is down</p>
<p><strong>Response:</strong></p>
<ol>
<li><p><strong>Check error logs</strong> - Identify the exact error</p>
<pre><code class="language-python">KeyError: 'merchant_category'
</code></pre>
</li>
<li><p><strong>Check upstream data</strong> - Did the field name change?</p>
<ul>
<li><p><code>merchant_category</code> -&gt; <code>category</code></p>
</li>
<li><p><code>amount</code> -&gt; <code>transaction_amount</code></p>
</li>
</ul>
</li>
<li><p><strong>Immediate fix</strong> - Add field name mapping</p>
<pre><code class="language-python"># Quick fix in API
if 'category' in data and 'merchant_category' not in data:
    data['merchant_category'] = data['category']
</code></pre>
</li>
<li><p><strong>Long-term fix</strong> - Add validation that catches schema changes</p>
<pre><code class="language-python">required_fields = ['amount', 'hour', 'day_of_week', 'merchant_category']
missing = [f for f in required_fields if f not in data]
if missing:
    raise ValidationError(f"Missing fields: {missing}")
</code></pre>
</li>
<li><p><strong>Add integration test</strong> - Test with upstream system in CI/CD</p>
</li>
</ol>
<h2 id="heading-9-how-to-put-it-all-together"><strong>9.</strong> How to Put It All Together</h2>
<p>Let's step back and appreciate what we've built. Our initial naive system has transformed into a <strong>local ML platform</strong> with production-grade components.</p>
<blockquote>
<p>💡 <strong>Mental Model</strong>: Each tool in this stack is a "catch net" for a specific failure mode:</p>
<ul>
<li><p>MLflow catches "which model is this?"</p>
</li>
<li><p>Feast catches "are features consistent?"</p>
</li>
<li><p>Great Expectations catches "is this data valid?"</p>
</li>
<li><p>Evidently catches "has the world changed?"</p>
</li>
<li><p>CI/CD catches "did we break something?"</p>
</li>
</ul>
<p>Together, they form defense-in-depth for ML systems.</p>
</blockquote>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th><strong>Tool</strong></th>
<th><strong>Problem Solved</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Experiment Tracking</strong></td>
<td>MLflow</td>
<td>Every run logged, reproducible</td>
</tr>
<tr>
<td><strong>Model Registry</strong></td>
<td>MLflow</td>
<td>Versioned models, rollback capability</td>
</tr>
<tr>
<td><strong>Feature Store</strong></td>
<td>Feast</td>
<td>Consistent features, no training-serving skew</td>
</tr>
<tr>
<td><strong>Data Validation</strong></td>
<td>Great Expectations</td>
<td>Bad data rejected with clear errors</td>
</tr>
<tr>
<td><strong>Monitoring</strong></td>
<td>Evidently</td>
<td>Drift detected before it causes problems</td>
</tr>
<tr>
<td><strong>Containerization</strong></td>
<td>Docker</td>
<td>Environment consistency everywhere</td>
</tr>
<tr>
<td><strong>CI/CD</strong></td>
<td>GitHub Actions</td>
<td>Automated testing and safe deployments</td>
</tr>
</tbody></table>
<h3 id="heading-the-complete-workflow"><strong>The Complete Workflow</strong></h3>
<p>Here's how all the pieces work together in practice:</p>
<ol>
<li><p><strong>Data arrives</strong> - New transaction data comes in from upstream systems</p>
</li>
<li><p><strong>Validation gate</strong> - Great Expectations rules check data quality. Bad data is rejected with clear error messages before it can cause harm.</p>
</li>
<li><p><strong>Feature computation</strong> - Feast computes features using the same definitions for both training and serving. No more training-serving skew.</p>
</li>
<li><p><strong>Training</strong> - When you retrain, MLflow logs all parameters, metrics, and artifacts. Every experiment is reproducible and comparable.</p>
</li>
<li><p><strong>Model registry</strong> - Trained models are automatically versioned. You can compare metrics, promote the best to Production, and roll back if needed.</p>
</li>
<li><p><strong>Serving</strong> - FastAPI loads the @champion model from MLflow. Each request is validated, features are retrieved from Feast, and predictions are returned.</p>
</li>
<li><p><strong>Monitoring</strong> - Evidently checks for drift periodically. If input distributions change significantly, alerts are triggered.</p>
</li>
<li><p><strong>Retraining loop</strong> - When drift is detected, you retrain on new data, compare metrics, and promote if better. The cycle continues.</p>
</li>
<li><p><strong>CI/CD safety net</strong> - All code changes go through automated tests. Docker ensures environment consistency. Nothing reaches production without passing the pipeline.</p>
</li>
</ol>
<h2 id="heading-10-whats-next-scale-to-production"><strong>10. What's Next: Scale to Production</strong></h2>
<p>This project runs locally, but the principles and tools extend directly to production deployments. Here's how each component scales:</p>
<h3 id="heading-scaling-feast-for-production"><strong>Scaling Feast for Production</strong></h3>
<p>We used Feast with local SQLite stores. For production:</p>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th><strong>Local</strong></th>
<th><strong>Production</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Online Store</td>
<td>SQLite</td>
<td>Redis, DynamoDB, or PostgreSQL</td>
</tr>
<tr>
<td>Offline Store</td>
<td>Parquet files</td>
<td>BigQuery, Snowflake, or Redshift</td>
</tr>
<tr>
<td>Feature Server</td>
<td>Embedded</td>
<td>Dedicated Feast serving cluster</td>
</tr>
</tbody></table>
<p>Benefits at scale:</p>
<ul>
<li><p>Sub-10ms feature retrieval</p>
</li>
<li><p>Horizontal scaling for high throughput</p>
</li>
<li><p>Feature monitoring and statistics</p>
</li>
<li><p>Point-in-time joins at petabyte scale</p>
</li>
</ul>
<h3 id="heading-scaling-mlflow-for-production"><strong>Scaling MLflow for Production</strong></h3>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th><strong>Local</strong></th>
<th><strong>Production</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Backend Store</td>
<td>SQLite</td>
<td>PostgreSQL or MySQL</td>
</tr>
<tr>
<td>Artifact Store</td>
<td>Local filesystem</td>
<td>S3, GCS, or Azure Blob</td>
</tr>
<tr>
<td>Tracking Server</td>
<td>Single instance</td>
<td>Load-balanced cluster</td>
</tr>
</tbody></table>
<h3 id="heading-kubernetes-deployment"><strong>Kubernetes Deployment</strong></h3>
<p>When you outgrow Docker Compose:</p>
<ul>
<li><p><strong>KServe or Seldon</strong> for serverless model serving with auto-scaling</p>
</li>
<li><p><strong>Horizontal Pod Autoscaler</strong> to scale based on CPU/memory/custom metrics</p>
</li>
<li><p><strong>Canary deployments</strong> to safely roll out new models (route 10% traffic first)</p>
</li>
<li><p><strong>GPU scheduling</strong> for inference-heavy models</p>
</li>
</ul>
<h3 id="heading-advanced-monitoring"><strong>Advanced Monitoring</strong></h3>
<p>Expand observability with:</p>
<ul>
<li><p><strong>Prometheus + Grafana</strong> for real-time dashboards</p>
</li>
<li><p><strong>OpenTelemetry</strong> for distributed tracing</p>
</li>
<li><p><strong>PagerDuty/Slack integration</strong> for alerts</p>
</li>
<li><p><strong>Labeled data collection</strong> for continuous model evaluation</p>
</li>
</ul>
<h3 id="heading-ab-testing-and-multi-armed-bandits"><strong>A/B Testing and Multi-Armed Bandits</strong></h3>
<p>How to Use the Model Registry:</p>
<ul>
<li><p>Serve <strong>multiple models</strong> concurrently (champion vs challengers)</p>
</li>
<li><p><strong>Route traffic</strong> dynamically based on context</p>
</li>
<li><p><strong>Collect metrics</strong> for each model variant</p>
</li>
<li><p><strong>Automatically promote</strong> the best performer</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Congratulations on building a production-ready ML system on your local machine!</p>
<p>What we assembled here is a microcosm of real-world ML platforms:</p>
<ul>
<li><p>We started with just a model saved to a pickle file</p>
</li>
<li><p>We ended up with <strong>MLOps best practices</strong>: experiment tracking, model versioning, feature stores, data validation, monitoring, containerization, and CI/CD</p>
</li>
</ul>
<p><strong>The tools we used are production-grade:</strong></p>
<ul>
<li><p><strong>MLflow</strong> powers ML platforms at companies like Microsoft, Facebook, and Databricks</p>
</li>
<li><p><strong>Feast</strong> is used by companies like Gojek, Shopify, and Robinhood</p>
</li>
<li><p><strong>FastAPI</strong> is one of the fastest Python web frameworks</p>
</li>
<li><p><strong>Great Expectations</strong> is used at companies like GitHub and Shopify</p>
</li>
<li><p><strong>Evidently</strong> is used for monitoring ML in production at scale</p>
</li>
</ul>
<p><strong>The principles apply at any scale:</strong></p>
<ul>
<li><p>Always track experiments</p>
</li>
<li><p>Always version models</p>
</li>
<li><p>Always validate data</p>
</li>
<li><p>Always monitor for drift</p>
</li>
<li><p>Always containerize for consistency</p>
</li>
<li><p>Always automate testing</p>
</li>
</ul>
<h3 id="heading-next-steps-you-can-try"><strong>Next Steps You Can Try</strong></h3>
<ol>
<li><p><strong>Deploy to the cloud</strong> - Push your Docker container to AWS ECS, Google Cloud Run, or Azure Container Instances</p>
</li>
<li><p><strong>Add model explainability</strong> - Use SHAP or LIME to explain individual predictions</p>
</li>
<li><p><strong>Implement A/B testing</strong> - Serve multiple models and compare performance</p>
</li>
<li><p><strong>Add feature importance monitoring</strong> - Track how feature importance changes over time</p>
</li>
<li><p><strong>Set up real-time alerting</strong> - Connect Evidently to Slack or PagerDuty</p>
</li>
<li><p><strong>Implement continuous training</strong> - Automatically retrain when drift is detected</p>
</li>
<li><p><strong>Add bias and fairness monitoring</strong> - Ensure your model treats all groups fairly</p>
</li>
</ol>
<p>Remember that productionizing ML is an <strong>iterative process</strong>. There's always another layer of robustness to add, another edge case to handle, another metric to track. But with the foundation you've built here, you're well on your way to taking models from promising notebook experiments to deployed, monitored, and maintainable production applications.</p>
<p>Happy building, and may your models be accurate and your pipelines resilient!</p>
<h2 id="heading-get-the-complete-code">Get the Complete Code</h2>
<p>The entire project from this handbook is available as a public GitHub repository:</p>
<p><strong>🔗</strong> <a href="http://github.com/sandeepmb/freecodecamp-local-ml-platform"><strong>github.com/sandeepmb/freecodecamp-local-ml-platform</strong></a></p>
<p>The repository includes:</p>
<ul>
<li><p>All source code (<code>src/</code> directory)</p>
</li>
<li><p>Test files (<code>tests/</code> directory)</p>
</li>
<li><p>Feast feature definitions (<code>feature_repo/</code>)</p>
</li>
<li><p>Docker and CI/CD configuration</p>
</li>
<li><p>Ready-to-run scripts</p>
</li>
</ul>
<p><strong>Quick Start:</strong></p>
<pre><code class="language-bash">git clone https://github.com/sandeepmb/freecodecamp-local-ml-platform.git
cd freecodecamp-local-ml-platform
python -m venv venv &amp;&amp; source venv/bin/activate
pip install -r requirements.txt
python src/generate_data.py
python src/train_naive.py
</code></pre>
<hr>
<h2 id="heading-references"><strong>References</strong></h2>
<ul>
<li><p><a href="https://mlflow.org/docs/latest/">MLflow Documentation</a> - Experiment tracking and model registry</p>
</li>
<li><p><a href="https://docs.feast.dev/">Feast Documentation</a> - Feature store</p>
</li>
<li><p><a href="https://docs.feast.dev/getting-started/quickstart">Feast Quickstart</a> - Getting started with Feast</p>
</li>
<li><p><a href="https://fastapi.tiangolo.com/">FastAPI Documentation</a> - Modern Python web framework</p>
</li>
<li><p><a href="https://greatexpectations.io/">Great Expectations</a> - Data validation</p>
</li>
<li><p><a href="https://docs.evidentlyai.com/">Evidently AI Documentation</a> - ML monitoring</p>
</li>
<li><p><a href="https://jfrog.com/learn/mlops/cicd-for-machine-learning/">CI/CD for Machine Learning (JFrog)</a> - CI/CD best practices</p>
</li>
<li><p><a href="https://www.qwak.com/post/training-serving-skew-in-machine-learning">Training-Serving Skew Explained</a> - Understanding skew</p>
</li>
<li><p><a href="https://docs.docker.com/">Docker Documentation</a> - Containerization</p>
</li>
<li><p><a href="https://docs.github.com/en/actions">GitHub Actions Documentation</a> - CI/CD automation</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Ship a Production-Ready RAG App with FAISS (Guardrails, Evals, and Fallbacks) ]]>
                </title>
                <description>
                    <![CDATA[ Most LLM applications look great in a high-fidelity demo. Then they hit the hands of real users and start failing in very predictable yet damaging ways. They answer questions they should not, they bre ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-rag-app-faiss-fastapi/</link>
                <guid isPermaLink="false">69b841572ad6ae5184d54317</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RAG  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vector database ]]>
                    </category>
                
                    <category>
                        <![CDATA[ faiss ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidozie Managwu ]]>
                </dc:creator>
                <pubDate>Mon, 16 Mar 2026 17:43:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f9da3ad9-e285-4ce1-acb7-ad119579971c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most LLM applications look great in a high-fidelity demo. Then they hit the hands of real users and start failing in very predictable yet damaging ways.</p>
<p>They answer questions they should not, they break when document retrieval is weak, they time out due to network latency, and nobody can tell exactly what happened because there are no logs and no tests.</p>
<p>In this tutorial, you’ll build a beginner-friendly Retrieval Augmented Generation (RAG) application designed to survive production realities. This isn’t just a script that calls an API. It’s a system featuring a FastAPI backend, a persisted FAISS vector store, and essential safety guardrails (including a retrieval gate and fallbacks).</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ol>
<li><p><a href="#heading-why-rag-alone-does-not-equal-productionready">Why RAG Alone Does Not Equal Production-Ready</a></p>
</li>
<li><p><a href="#heading-the-architecture-you-are-building">The Architecture You Are Building</a></p>
</li>
<li><p><a href="#heading-project-setup-and-structure">Project Setup and Structure</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-rag-layer-with-faiss">How to Build the RAG Layer with FAISS</a></p>
</li>
<li><p><a href="#heading-how-to-add-the-llm-call-with-structured-output">How to Add the LLM Call with Structured Output</a></p>
</li>
<li><p><a href="#heading-how-to-add-guardrails-retrieval-gate-and-fallbacks">How to Add Guardrails: Retrieval Gate and Fallbacks</a></p>
</li>
<li><p><a href="#heading-fast-api-app-creating-the-answer-endpoint">FastAPI App: Creating the /answer Endpoint</a></p>
</li>
<li><p><a href="#heading-how-to-add-beginnerfriendly-evals">How to Add Beginner-Friendly Evals</a></p>
</li>
<li><p><a href="#heading-what-to-improve-next-realistic-upgrades">What to Improve Next: Realistic Upgrades</a></p>
</li>
</ol>
<h2 id="heading-why-rag-alone-does-not-equal-production-ready">Why RAG Alone Does Not Equal Production-Ready</h2>
<p>Retrieval Augmented Generation (RAG) is often hailed as the hallucination killer. By grounding the model in retrieved text, we provide it with the facts it needs to be accurate. But simply connecting a vector database to an LLM isn’t enough for a production environment.</p>
<p>Production issues usually arise from the silent failures in the system surrounding the model:</p>
<ul>
<li><p><strong>Weak retrieval:</strong> If the app retrieves irrelevant chunks of text, the model tries to bridge the gap by inventing an answer anyway. Without a designated “I do not know” path, the model is essentially forced to hallucinate.</p>
</li>
<li><p><strong>Lack of visibility:</strong> Without structured outputs and basic logging, you can’t tell if bad retrieval, a confusing prompt, or a model update caused a wrong answer.</p>
</li>
<li><p><strong>Fragility:</strong> A simple API timeout or malformed provider response becomes a user-facing outage if you don’t implement fallbacks.</p>
</li>
<li><p><strong>No regression testing:</strong> In traditional software, we have unit tests. In AI, we need evals. Without them, a small tweak to your prompt might fix one issue but break ten others without you realising it.</p>
</li>
</ul>
<p>We’ll solve each of these issues systematically in this guide.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This tutorial is beginner-friendly, but it assumes you have a few basics in place so you can focus on building a robust RAG system instead of getting stuck on setup issues.</p>
<h3 id="heading-knowledge">Knowledge</h3>
<p>You should be comfortable with:</p>
<ul>
<li><p><strong>Python fundamentals</strong> (functions, modules, virtual environments)</p>
</li>
<li><p><strong>Basic HTTP + JSON</strong> (requests, response payloads)</p>
</li>
<li><p><strong>APIs with FastAPI</strong> (what an endpoint is and how to run a server)</p>
</li>
<li><p><strong>High-level LLM concepts</strong> (prompting, temperature, structured outputs)</p>
</li>
</ul>
<h3 id="heading-tools-accounts">Tools + Accounts</h3>
<p>You’ll need:</p>
<ul>
<li><p><strong>Python 3.10+</strong></p>
</li>
<li><p>A working <strong>OpenAI-compatible API key</strong> (OpenAI or any provider that supports the same request/response shape)</p>
</li>
<li><p>A local environment where you can run a FastAPI app (Mac/Linux/Windows)</p>
</li>
</ul>
<h3 id="heading-what-this-tutorial-covers-and-what-it-doesnt">What This Tutorial Covers (and What It Doesn’t)</h3>
<p>We’ll build a production-minded baseline:</p>
<ul>
<li><p>A <strong>FAISS-backed retriever</strong> with a persisted index + metadata</p>
</li>
<li><p>A <strong>retrieval gate</strong> to prevent “forced hallucination”</p>
</li>
<li><p><strong>Structured JSON outputs</strong> so your backend is stable</p>
</li>
<li><p><strong>Fallback behavior</strong> for timeouts and provider errors</p>
</li>
<li><p>A small <strong>eval harness</strong> to prevent regressions</p>
</li>
</ul>
<p>We won’t implement advanced upgrades such as rerankers, semantic chunking, auth, background jobs beyond a roadmap at the end.</p>
<h2 id="heading-the-architecture-you-are-building">The Architecture You Are Building</h2>
<p>The flow of our application follows a disciplined path so every answer is grounded in evidence:</p>
<ol>
<li><p><strong>User query:</strong> The user submits a question via a FastAPI endpoint.</p>
</li>
<li><p><strong>Retrieval:</strong> The system embeds the question and retrieves the top-k most similar document chunks.</p>
</li>
<li><p><strong>The retrieval gate:</strong> We evaluate the similarity score. If the context is not relevant enough, we stop immediately and refuse the query.</p>
</li>
<li><p><strong>Augmentation and generation:</strong> If the gate passes, we send a context-augmented prompt to the LLM.</p>
</li>
<li><p><strong>Structured response:</strong> The model returns a JSON object containing the answer, sources used, and a confidence level.</p>
</li>
</ol>
<h2 id="heading-project-setup-and-structure">Project Setup and Structure</h2>
<p>To keep things organized and maintainable, we’ll use a modular structure. This allows you to swap out your LLM provider or your vector database without rewriting your entire core application.</p>
<h3 id="heading-project-structure">Project Structure</h3>
<pre><code class="language-python">.
├── app.py              # FastAPI entry point and API logic
├── rag.py              # FAISS index, persistence, and document retrieval
├── llm.py              # LLM API interface and JSON parsing
├── prompts.py          # Centralized prompt templates
├── data/               # Source .txt documents
├── index/              # Persisted FAISS index and metadata
└── evals/              # Evaluation dataset and runner script
    ├── eval_set.json
    └── run_evals.py
</code></pre>
<h3 id="heading-install-dependencies">Install Dependencies</h3>
<p>First, create a virtual environment to isolate your project:</p>
<pre><code class="language-python">python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install fastapi uvicorn faiss-cpu numpy pydantic requests python-dotenv
</code></pre>
<h3 id="heading-configure-the-environment">Configure the Environment</h3>
<p>Create a <code>.env</code> file in the root directory. We are targeting OpenAI-compatible providers:</p>
<pre><code class="language-python">OPENAI_API_KEY=your_actual_api_key_here
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini
</code></pre>
<p>Important note on compatibility: The code below assumes an OpenAI-style API. If you use a provider that is not compatible, you must change the URL, headers (for example <code>X-API-Key</code>), and the way you extract embeddings and final message content in <code>embed_texts()</code> and <code>call_llm()</code>.</p>
<h2 id="heading-how-to-build-the-rag-layer-with-faiss">How to Build the RAG Layer with FAISS</h2>
<p>In <code>rag.py</code>, we handle the “Retriever” part of RAG. This involves turning raw text into mathematical vectors that the computer can compare.</p>
<h3 id="heading-what-is-faiss-and-what-does-it-do">What is FAISS (and What Does It Do)?</h3>
<p><strong>FAISS</strong> (Facebook AI Similarity Search) is a fast library for vector similarity search. In a RAG system, each chunk of text becomes an embedding vector (a list of floats). FAISS stores those vectors in an index so you can quickly ask:</p>
<blockquote>
<p>“Given this question embedding, which document chunks are closest to it?”</p>
</blockquote>
<p>In this tutorial, we use <code>IndexFlatIP</code> inner product and normalise vectors with <code>faiss.normalize_L2(...)</code>. With normalised vectors, the inner product behaves like <strong>cosine similarity</strong>, giving us a stable score we can use for a retrieval gate.</p>
<h3 id="heading-chunking-strategy-with-overlap">Chunking Strategy With Overlap</h3>
<p>We’ll use chunking with overlap. If we split a document at exactly 1,000 characters, we might cut a sentence in half, losing its meaning. By using an overlap, for example, 200 characters, we ensure that the end of one chunk and the beginning of the next share context.</p>
<h3 id="heading-implementation-of-ragpy">Implementation of <code>rag.py</code></h3>
<pre><code class="language-python">import os
import faiss
import numpy as np
import requests
import json
from typing import List, Dict
from dotenv import load_dotenv

load_dotenv()

INDEX_PATH = "index/faiss.index"
META_PATH = "index/meta.json"

def chunk_text(text: str, size: int = 1000, overlap: int = 200) -&gt; List[str]:
    chunks = []
    step = max(1, size - overlap)
    for i in range(0, len(text), step):
        chunk = text[i : i + size].strip()
        if chunk:
            chunks.append(chunk)
    return chunks

def embed_texts(texts: List[str]) -&gt; np.ndarray:
    # Note: If your provider is not OpenAI-compatible, change this URL and headers
    url = f"{os.getenv('OPENAI_BASE_URL')}/embeddings"
    headers = {"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
    payload = {"input": texts, "model": "text-embedding-3-small"}

    resp = requests.post(url, headers=headers, json=payload, timeout=30)
    resp.raise_for_status()
    # If your provider uses a different response format, change the line below
    vectors = np.array([item["embedding"] for item in resp.json()["data"]], dtype="float32")
    return vectors

def build_index() -&gt; None:
    all_chunks: List[str] = []
    metadata: List[Dict] = []

    if not os.path.exists("data"):
        os.makedirs("data")
        return

    for file in os.listdir("data"):
        if not file.endswith(".txt"):
            continue

        with open(f"data/{file}", "r", encoding="utf-8") as f:
            text = f.read()

        chunks = chunk_text(text)
        all_chunks.extend(chunks)
        for c in chunks:
            metadata.append({"source": file, "text": c})

    if not all_chunks:
        return

    embeddings = embed_texts(all_chunks)
    faiss.normalize_L2(embeddings)

    dim = embeddings.shape[1]
    index = faiss.IndexFlatIP(dim)
    index.add(embeddings)

    os.makedirs("index", exist_ok=True)
    faiss.write_index(index, INDEX_PATH)

    with open(META_PATH, "w", encoding="utf-8") as f:
        json.dump(metadata, f, ensure_ascii=False)

def load_index():
    if not (os.path.exists(INDEX_PATH) and os.path.exists(META_PATH)):
        raise FileNotFoundError(
            "FAISS index not found. Add .txt files to data/ and run build_index()."
        )

    index = faiss.read_index(INDEX_PATH)
    with open(META_PATH, "r", encoding="utf-8") as f:
        metadata = json.load(f)
    return index, metadata

def retrieve(query: str, k: int = 5) -&gt; List[Dict]:
    index, metadata = load_index()

    q_emb = embed_texts([query])
    faiss.normalize_L2(q_emb)

    scores, ids = index.search(q_emb, k)
    results = []
    for score, idx in zip(scores[0], ids[0]):
        if idx == -1:
            continue
        m = metadata[idx]
        results.append(
            {"score": float(score), "source": m["source"], "text": m["text"], "id": int(idx)}
        )
    return results
</code></pre>
<h2 id="heading-how-to-add-the-llm-call-with-structured-output">How to Add the LLM Call with Structured Output</h2>
<p>A major failure point in AI apps is the “chatty” nature of LLMs. If your backend expects a list of sources but the LLM returns conversational filler, your code will crash.</p>
<p>We solve this with <strong>structured output</strong>: instruct the model to return a strict JSON object, then parse it safely.</p>
<h3 id="heading-implementation-of-llmpy">Implementation of <code>llm.py</code></h3>
<pre><code class="language-python">import json
import requests
import os
from typing import Dict, Any

def call_llm(system_prompt: str, user_prompt: str) -&gt; Dict[str, Any]:
    # Note: Change URL/Headers if using a non-OpenAI compatible provider
    url = f"{os.getenv('OPENAI_BASE_URL')}/chat/completions"
    headers = {
        "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
        "Content-Type": "application/json",
    }

    payload = {
        "model": os.getenv("OPENAI_MODEL"),
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        "response_format": {"type": "json_object"},
        "temperature": 0,
    }

    try:
        resp = requests.post(url, headers=headers, json=payload, timeout=30)
        resp.raise_for_status()
        content = resp.json()["choices"][0]["message"]["content"]

        parsed = json.loads(content)
        parsed.setdefault("answer", "")
        parsed.setdefault("refusal", False)
        parsed.setdefault("confidence", "medium")
        parsed.setdefault("sources", [])
        return parsed

    except (requests.Timeout, requests.ConnectionError):
        return {
            "answer": "The system is temporarily unavailable (network issue). Please try again.",
            "refusal": True,
            "confidence": "low",
            "sources": [],
            "error_type": "network_error",
        }
    except Exception:
        return {
            "answer": "A system error occurred while generating the answer.",
            "refusal": True,
            "confidence": "low",
            "sources": [],
            "error_type": "unknown_error",
        }
</code></pre>
<h2 id="heading-how-to-add-guardrails-retrieval-gate-and-fallbacks">How to Add Guardrails: Retrieval Gate and Fallbacks</h2>
<p>Guardrails are interceptors. They sit between the user and the model to prevent predictable failures.</p>
<h3 id="heading-the-retrieval-gate-how-it-works-and-how-to-add-it">The Retrieval Gate: How It Works and How to Add It</h3>
<p>In a standard RAG pipeline, the system always calls the LLM. If the user asks an irrelevant question, the retriever will still return the “closest” (but wrong) chunks.</p>
<p>The solution is the retrieval gate:</p>
<ol>
<li><p>Retrieve top-k chunks and get the <strong>top similarity score</strong></p>
</li>
<li><p>If the score is below a threshold (for example <code>0.30</code>), refuse immediately</p>
</li>
<li><p>Only call the LLM when retrieval is strong enough to ground the answer</p>
</li>
</ol>
<p>A threshold of <code>0.30</code> is a reasonable starting point when using normalised cosine similarity, but you should tune it using evals (next section).</p>
<h3 id="heading-fallbacks-and-why-they-matter">Fallbacks and Why They Matter</h3>
<p>Fallbacks ensure that if an API fails or times out, the user gets a helpful message instead of a crash. They also keep your API response shape consistent, which prevents frontend errors and makes logging meaningful.</p>
<p>In this tutorial, fallbacks are implemented inside <code>call_llm()</code> so your FastAPI layer stays simple.</p>
<h2 id="heading-fastapi-app-creating-the-answer-endpoint">FastAPI App: Creating the /answer Endpoint</h2>
<p>The <code>app.py</code> file is the conductor. It ties retrieval, guardrails, prompting, and generation together.</p>
<h3 id="heading-implementation-of-apppy">Implementation of <code>app.py</code></h3>
<pre><code class="language-python">from fastapi import FastAPI
from pydantic import BaseModel
from rag import retrieve
from llm import call_llm
import prompts
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rag_app")

app = FastAPI(title="Production-Ready RAG")

class QueryRequest(BaseModel):
    question: str

@app.post("/answer")
async def get_answer(req: QueryRequest):
    start_time = time.time()
    question = (req.question or "").strip()

    if not question:
        return {
            "answer": "Please provide a non-empty question.",
            "refusal": True,
            "confidence": "low",
            "sources": [],
            "latency_sec": round(time.time() - start_time, 2),
        }

    # 1) Retrieval
    results = retrieve(question, k=5)
    top_score = results[0]["score"] if results else 0.0

    logger.info("query=%r top_score=%.3f num_results=%d", question, top_score, len(results))

    # 2) Retrieval Gate (Guardrail)
    if top_score &lt; 0.30:
        return {
            "answer": "I do not have documents to answer that question.",
            "refusal": True,
            "confidence": "low",
            "sources": [],
            "latency_sec": round(time.time() - start_time, 2),
            "retrieval": {"top_score": top_score, "k": 5},
        }

    # 3) Augment
    context_text = "\n\n".join([f"Source {r['source']}: {r['text']}" for r in results])
    user_prompt = f"Context:\n{context_text}\n\nQuestion: {question}"

    # 4) Generation with Fallback
    response = call_llm(prompts.SYSTEM_PROMPT, user_prompt)

    # 5) Attach debug metadata
    response["latency_sec"] = round(time.time() - start_time, 2)
    response["retrieval"] = {"top_score": top_score, "k": 5}
    return response
</code></pre>
<h2 id="heading-centralized-prompt-template-promptspy">Centralized Prompt – Template: prompts.py</h2>
<p>A small but important habit: keep prompts centralised so they’re versionable and easy to evaluate.</p>
<h3 id="heading-example-promptspy">Example <code>prompts.py</code></h3>
<pre><code class="language-python">SYSTEM_PROMPT = """You are a RAG assistant. Use ONLY the provided Context to answer.
If the context does not contain the answer, respond with refusal=true.

Return a valid JSON object with exactly these keys:
- answer: string
- refusal: boolean
- confidence: "low" | "medium" | "high"
- sources: array of strings (source filenames you used)

Do not include any extra keys. Do not include markdown. Do not include commentary."""
</code></pre>
<h2 id="heading-how-to-add-beginner-friendly-evals">How to Add Beginner-Friendly Evals</h2>
<p>In AI systems, outputs are probabilistic. This makes testing harder than traditional software. Evals (evaluations) are a set of “golden questions” and “expected behaviours” you run repeatedly to detect regressions.</p>
<p>Instead of “does it output exactly this string,” you test:</p>
<ul>
<li><p>Should the app <strong>refuse</strong> when the retrieval is weak?</p>
</li>
<li><p>When it answers, does it include <strong>sources</strong>?</p>
</li>
<li><p>Is the behaviour stable across prompt tweaks and model changes?</p>
</li>
</ul>
<h3 id="heading-step-1-create-evalsevalsetjson">Step 1: Create <code>evals/eval_set.json</code></h3>
<p>This should contain both positive and negative cases.</p>
<pre><code class="language-json">[
  {
    "id": "in_scope_01",
    "question": "What is a retrieval gate and why is it important?",
    "expect_refusal": false,
    "notes": "Should explain gating and relate it to hallucination prevention."
  },
  {
    "id": "out_of_scope_01",
    "question": "What is the capital of France?",
    "expect_refusal": true,
    "notes": "If the knowledge base only includes our docs, the app should refuse."
  },
  {
    "id": "edge_01",
    "question": "",
    "expect_refusal": true,
    "notes": "Empty input should not call the LLM."
  }
]
</code></pre>
<h3 id="heading-step-2-create-evalsrunevalspy">Step 2: Create <code>evals/run_evals.py</code></h3>
<p>This runner calls your API endpoint (end-to-end) and checks expected behaviours.</p>
<pre><code class="language-python">import json
import requests

API_URL = "http://127.0.0.1:8000/answer"

def run():
    with open("evals/eval_set.json", "r", encoding="utf-8") as f:
        cases = json.load(f)

    passed = 0
    failed = 0

    for case in cases:
        resp = requests.post(API_URL, json={"question": case["question"]}, timeout=60)
        resp.raise_for_status()
        out = resp.json()

        got_refusal = bool(out.get("refusal", False))
        expect_refusal = bool(case["expect_refusal"])

        ok = (got_refusal == expect_refusal)

        # Beginner-friendly: if it answers, sources should exist and be a list
        if not got_refusal:
            ok = ok and isinstance(out.get("sources"), list)

        if ok:
            passed += 1
            print(f"PASS {case['id']}")
        else:
            failed += 1
            print(f"FAIL {case['id']} expected_refusal={expect_refusal} got_refusal={got_refusal}")
            print("Output:", json.dumps(out, indent=2))

    print(f"\nDone. Passed={passed} Failed={failed}")
    if failed:
        raise SystemExit(1)

if __name__ == "__main__":
    run()
</code></pre>
<h3 id="heading-how-to-use-evals-in-practice">How to Use Evals in Practice</h3>
<p>Run your server:</p>
<pre><code class="language-python">uvicorn app:app --reload
</code></pre>
<p>In another terminal, run evals:</p>
<pre><code class="language-python">python evals/run_evals.py
</code></pre>
<p>If an eval fails, you have a concrete signal that something changed in retrieval, gating, prompting, or provider behaviour.</p>
<h2 id="heading-what-to-improve-next-realistic-upgrades">What to Improve Next: Realistic Upgrades</h2>
<p>Building a reliable RAG app is iterative. Here are realistic next steps:</p>
<ul>
<li><p><strong>Semantic chunking:</strong> Break text based on meaning instead of character count.</p>
</li>
<li><p><strong>Reranking:</strong> Use a cross-encoder reranker to reorder the top-k chunks for higher precision.</p>
</li>
<li><p><strong>Metadata filtering:</strong> Filter results by category, date, or department to reduce false positives.</p>
</li>
<li><p><strong>Better citations:</strong> Store chunk IDs and show exactly which chunk(s) the answer came from.</p>
</li>
<li><p><strong>Observability:</strong> Add request IDs, structured logs, and traces so “what happened?” is answerable.</p>
</li>
<li><p><strong>Async + background indexing:</strong> Move index building to a background job and keep the API responsive.</p>
</li>
</ul>
<h2 id="heading-final-thoughts-production-ready-is-a-set-of-habits">Final Thoughts: Production-Ready Is a Set of Habits</h2>
<p>Building an AI application that survives in the real world is about building a system that is predictable, measurable, and safe.</p>
<ul>
<li><p><strong>Retrieval quality is measurable:</strong> Use similarity scores to gate your LLM.</p>
</li>
<li><p><strong>Refusal is a feature:</strong> It is better to say “I do not know” than to lie.</p>
</li>
<li><p><strong>Fallbacks are mandatory:</strong> Design for the moment the API goes down.</p>
</li>
<li><p><strong>Evals prevent regressions:</strong> Never deploy a change without running your tests.</p>
</li>
</ul>
<h2 id="heading-about-me">About Me</h2>
<p>I am Chidozie Managwu, an award-winning AI Product Architect and founder focused on helping global tech talent build real, production-ready skills. I contribute to global AI initiatives as a GAFAI Delegate and lead AI Titans Network, a community for developers learning how to ship AI products.</p>
<p>My work has been recognized with the Global Tech Hero award and featured on platforms like HackerNoon.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build End-to-End LLM Observability in FastAPI with OpenTelemetry ]]>
                </title>
                <description>
                    <![CDATA[ This article shows how to build end-to-end, code-first LLM observability in a FastAPI application using the OpenTelemetry Python SDK. Instead of relying on vendor-specific agents or opaque SDKs, we wi ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-end-to-end-llm-observability-in-fastapi-with-opentelemetry/</link>
                <guid isPermaLink="false">69b4379c6e27dd07d920f14c</guid>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jessica Patel ]]>
                </dc:creator>
                <pubDate>Fri, 13 Mar 2026 16:13:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c69a589a-2dce-46a1-ac49-a0d0e2c23c6e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This article shows how to build end-to-end, code-first LLM observability in a FastAPI application using the OpenTelemetry Python SDK.</p>
<p>Instead of relying on vendor-specific agents or opaque SDKs, we will manually design traces, spans, and semantic attributes that capture the full lifecycle of an LLM-powered request.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-introduction">Introduction</a></p>
</li>
<li><p><a href="#heading-prerequisites-and-technical-context">Prerequisites and Technical Context</a></p>
</li>
<li><p><a href="#heading-why-llm-observability-is-fundamentally-different">Why LLM Observability Is Fundamentally Different</a></p>
</li>
<li><p><a href="#heading-reference-architecture-a-traceable-rag-request">Reference Architecture: A Traceable RAG Request</a></p>
</li>
<li><p><a href="#heading-reference-architecture-explained">Reference Architecture Explained</a></p>
</li>
<li><p><a href="#heading-why-this-design-is-better-than-simpler-alternatives">Why This Design Is Better Than Simpler Alternatives</a></p>
</li>
<li><p><a href="#heading-llm-models-that-work-best-for-this-architecture">LLM Models That Work Best for This Architecture</a></p>
</li>
<li><p><a href="#heading-opentelemetry-primer-llm-relevant-concepts-only">OpenTelemetry Primer (LLM-Relevant Concepts Only)</a></p>
</li>
<li><p><a href="#heading-designing-llm-aware-spans">Designing LLM-Aware Spans</a></p>
</li>
<li><p><a href="#heading-fastapi-example-end-to-end-llm-spans-complete-and-explained">FastAPI Example: End-to-End LLM Spans (Complete and Explained)</a></p>
</li>
<li><p><a href="#heading-semantic-attributes-best-practices-for-llm-observability">Semantic Attributes: Best Practices for LLM Observability</a></p>
</li>
<li><p><a href="#heading-evaluation-hooks-inside-traces">Evaluation Hooks Inside Traces</a></p>
</li>
<li><p><a href="#heading-exporting-and-visualizing-traces-where-this-fits-with-vendor-tooling">Exporting and Visualizing Traces (Where This Fits with Vendor Tooling)</a></p>
</li>
<li><p><a href="#heading-operational-patterns-and-anti-patterns">Operational Patterns and Anti-Patterns</a></p>
</li>
<li><p><a href="#heading-extending-the-system">Extending the System</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>Large Language Models (LLMs) are rapidly becoming a core component of modern software systems. Applications that once relied on deterministic APIs are now incorporating LLM-powered features such as conversational assistants, document summarization, intelligent search, and retrieval-augmented generation (RAG).</p>
<p>While these capabilities unlock new user experiences, they also introduce operational complexity that traditional monitoring approaches were never designed to handle.</p>
<p>Unlike conventional software services, LLM systems are probabilistic by nature. The same request may produce slightly different responses depending on factors such as prompt structure, model configuration, retrieval context, and sampling parameters such as temperature or top-p.</p>
<p>In addition, LLM workloads introduce entirely new operational dimensions such as token consumption, prompt construction latency, inference cost, context window limits, and response quality.</p>
<p>These factors mean that a request can appear technically successful from an infrastructure perspective while still producing an incorrect, hallucinated, or low-quality result.</p>
<p>Traditional observability tools typically focus on infrastructure-level signals such as latency, error rate, and throughput. While these metrics remain important, they are insufficient for understanding how an LLM application behaves in production.</p>
<p>Engineers must also understand what prompt was constructed, which documents were retrieved, how many tokens were consumed, which model configuration was used, and how the final response was evaluated. Without this visibility, debugging LLM behavior becomes extremely difficult and operational costs can quickly spiral out of control.</p>
<p>This is where LLM observability becomes essential. Observability for LLM systems extends beyond infrastructure monitoring. It captures the full lifecycle of an AI-driven request — from user input and context retrieval to prompt construction, model inference, post-processing, and quality evaluation.</p>
<p>When implemented correctly, observability allows teams to answer why the model generated a particular response, which retrieval results influenced the output, how much a request cost in terms of tokens, where latency occurred within the request pipeline, and whether the response passed basic quality or safety checks.</p>
<p>This article demonstrates how to implement end-to-end LLM observability in a FastAPI application using OpenTelemetry. Instead of relying on proprietary monitoring agents or opaque vendor SDKs, we take a code-first approach to instrumentation. By explicitly designing traces, spans, and semantic attributes, we gain precise control over how LLM interactions are observed and analyzed.</p>
<p>Throughout the guide, we will walk through a practical architecture for tracing a retrieval-augmented generation (RAG) workflow, where each stage of the request lifecycle is represented as a trace span. We will explore how to design meaningful span boundaries, capture prompt and model metadata safely, record token usage and cost signals, and attach evaluation results directly to traces.</p>
<p>The article also explains how this instrumentation can be exported to any OpenTelemetry-compatible backend such as Jaeger, Grafana Tempo, or LLM-specific platforms like Phoenix.</p>
<p>By the end of this guide, you will understand how to:</p>
<ul>
<li><p>Structure traces so that each user request maps to a single end-to-end LLM interaction</p>
</li>
<li><p>Design span hierarchies that reflect the logical stages of an LLM pipeline</p>
</li>
<li><p>Capture prompt metadata, model configuration, and token usage safely</p>
</li>
<li><p>Attach evaluation and quality signals to traces for deeper analysis</p>
</li>
<li><p>Export observability data to different backends without changing instrumentation</p>
</li>
</ul>
<p>Most importantly, the goal of this article is not simply to demonstrate how to add telemetry to an application. Instead, it aims to show how to think about observability when building LLM-powered systems.</p>
<p>When LLM operations are treated as first-class components within a distributed system, traces become a powerful tool for debugging, optimization, cost management, and continuous improvement of model behavior.</p>
<h2 id="heading-prerequisites-and-technical-context">Prerequisites and Technical Context</h2>
<p>Before following this guide, you should be familiar with the Python programming language, basic web API concepts, and general microservice architecture. Below are some key tools and concepts used in this article.</p>
<h3 id="heading-fastapi-web-framework">FastAPI (Web Framework)</h3>
<p>FastAPI is used as the primary web framework for the application. It is a modern Python framework designed for building high-performance APIs using standard Python type hints. FastAPI simplifies request validation, serialization, and API documentation while remaining lightweight and fast.</p>
<h3 id="heading-large-language-models-llms">Large Language Models (LLMs)</h3>
<p>Large Language Models (LLMs) are the computational core of the example system. An LLM is a model trained on vast amounts of text data to generate or transform language in ways that resemble human communication. In production environments, LLMs are commonly used for tasks such as conversational interfaces, summarization, and question answering.</p>
<h3 id="heading-observability-concept">Observability (Concept)</h3>
<p>Observability is the overarching concept that connects all the technical pieces in this article. At a high level, observability refers to the ability to understand a system's internal behavior by examining the data it produces during execution. Rather than asking whether a system is simply "up" or "down," observability helps answer deeper questions about why a request behaved a certain way, where latency was introduced, or how different components interacted.</p>
<h3 id="heading-opentelemetry-instrumentation-standard">OpenTelemetry (Instrumentation Standard)</h3>
<p>OpenTelemetry is the mechanism used to implement observability within the application. It is an open, vendor-neutral standard for generating telemetry data such as traces, metrics, and logs. By instrumenting key parts of the LLM workflow, we can observe how requests flow through the system, how long each step takes, and what contextual data influenced the final outcome. OpenTelemetry serves as the foundation for collecting this information in a consistent and portable way, independent of any specific monitoring backend.</p>
<h2 id="heading-why-llm-observability-is-fundamentally-different">Why LLM Observability Is Fundamentally Different</h2>
<p>Traditional observability assumes deterministic behavior: the same input produces the same output. LLM systems violate this assumption. The same request can vary due to prompt template changes, retrieval differences, sampling parameters (temperature, top-p), model version upgrades, and context window truncation.​</p>
<p>As a result, teams need visibility into what the model saw, how it was configured, what it retrieved, how long it took, and how much it cost, all correlated to a single user request. Logs alone are insufficient, and metrics lack dimensionality. Distributed traces are the backbone of LLM observability.</p>
<h2 id="heading-reference-architecture-a-traceable-rag-request">Reference Architecture: A Traceable RAG Request</h2>
<p>A typical FastAPI-based RAG service follows this flow:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6979762ba2442d262dacf388/50e7fda4-7407-43d6-8f12-045b8e73c7eb.png" alt="FastAPI Based RAG Service" style="display:block;margin:0 auto" width="936" height="330" loading="lazy">

<p>Each step is observable, but only if we deliberately instrument it. The goal is one trace per user request, with child spans representing each logical LLM step.</p>
<h2 id="heading-reference-architecture-explained">Reference Architecture Explained</h2>
<h3 id="heading-client-sends-a-request-to-chat">Client Sends a Request to /chat</h3>
<p>The architecture begins when a client sends a request to the <code>/chat</code> endpoint. This request typically contains the user's query along with any session or conversation context required by the application.</p>
<p>Keeping the client interface minimal and well-defined is intentional: it ensures the backend receives a predictable input shape and prevents application-specific logic from leaking into downstream LLM processing.</p>
<p>From an observability perspective, this request marks the start of a single end-to-end trace, allowing every subsequent operation to be correlated back to the original user action.</p>
<h3 id="heading-fastapi-validates-input-and-authenticates-the-user">FastAPI Validates Input and Authenticates the User</h3>
<p>Once the request reaches the service, FastAPI performs schema validation and authentication. Validation guarantees that only well-formed inputs proceed through the pipeline, while authentication ensures that expensive LLM operations are only executed for authorized users.</p>
<p>Placing this step early reduces unnecessary computation and protects the system from abuse. It also improves trace quality by ensuring that all observed requests represent legitimate execution paths rather than malformed or rejected traffic.</p>
<h3 id="heading-retriever-queries-the-vector-database">Retriever Queries the Vector Database​</h3>
<p>After validation, the system queries a vector database to retrieve documents relevant to the user's request. This retrieval step is the foundation of retrieval-augmented generation (RAG). By grounding the LLM in external knowledge, the system improves factual accuracy and reduces hallucinations.</p>
<p>Separating retrieval from generation allows teams to tune similarity thresholds, embedding models, and top-k values independently, and it makes it easier to diagnose whether poor responses are caused by bad retrieval or model behavior.</p>
<h3 id="heading-prompt-is-assembled-using-retrieved-documents">Prompt Is Assembled Using Retrieved Documents</h3>
<p>With relevant documents in hand, the system constructs the final prompt that will be sent to the LLM. This step combines the user query, retrieved context, system instructions, and formatting rules into a single structured prompt.</p>
<p>Making prompt assembly an explicit stage enables prompt versioning, experimentation, and observability. It also provides a natural place to detect issues such as context window overflows or excessive prompt size before invoking the model.</p>
<h3 id="heading-llm-api-is-invoked">LLM API Is Invoked</h3>
<p>The LLM API call is the most expensive and non-deterministic operation in the pipeline, which is why it occurs only after all preparatory work is complete. At this stage, the model receives a fully constructed prompt and produces a response based on its configuration parameters.</p>
<p>This step is the primary focus of latency, cost, and reliability controls such as retries, timeouts, and circuit breakers. From an observability standpoint, this span becomes the anchor for token usage, cost attribution, and prompt-level debugging.</p>
<h3 id="heading-response-is-post-processed-and-returned">Response Is Post-Processed and Returned</h3>
<p>After the LLM returns a response, the system performs post-processing before sending the result back to the client. This may include formatting, filtering, validation, or enrichment of the output. Post-processing acts as a final safeguard against malformed or low-quality responses and ensures consistency with application requirements. It also provides a clean boundary for attaching evaluation signals, such as response length, relevance scores, or truncation indicators, before the request completes.</p>
<h2 id="heading-why-this-design-is-better-than-simpler-alternatives">Why This Design Is Better Than Simpler Alternatives</h2>
<p>This architecture intentionally avoids coupling responsibilities together. Validation, retrieval, prompt construction, model execution, and response handling are all distinct steps. This separation makes the system easier to test, easier to observe, and easier to evolve. When something fails, engineers can identify <em>where</em> and <em>why</em> rather than treating the LLM as a black box.​</p>
<p>Compared to a monolithic "send user input directly to the LLM" approach, this design offers better correctness, lower cost, and higher resilience. It also aligns naturally with distributed tracing, since each block maps cleanly to a trace span with a clear semantic purpose. As the system grows, additional features such as caching, fallback models, or policy enforcement can be added without destabilizing the entire flow.​</p>
<p>Most importantly, this architecture treats the LLM as one component in a larger system, not the system itself. That mindset is essential for building reliable production applications.</p>
<h2 id="heading-llm-models-that-work-best-for-this-architecture">LLM Models That Work Best for This Architecture</h2>
<p>This architecture is model-agnostic, but certain model characteristics work particularly well with retrieval-augmented workflows.</p>
<p>Models with strong instruction-following and reasoning capabilities tend to perform best, especially when prompts include structured context from retrieved documents. General-purpose models such as GPT-4-class systems perform well when accuracy and reasoning depth are critical.</p>
<p>For lower-latency or cost-sensitive use cases, smaller instruction-tuned models can be effective when paired with high-quality retrieval. Open-source models such as LLaMA-derived or Mistral-based systems also fit well into this architecture, particularly when deployed behind a private inference endpoint.​</p>
<p>The key requirement is not the model itself, but how it is used. Models that can reliably ground their responses in provided context, respect system instructions, and produce stable outputs under varying prompts integrate most cleanly into this design. Because retrieval and prompt construction are explicit stages, models can be swapped or compared without changing the overall system structure.</p>
<h2 id="heading-opentelemetry-primer-llm-relevant-concepts-only">OpenTelemetry Primer (LLM-Relevant Concepts Only)</h2>
<p>OpenTelemetry defines three core types of telemetry data: traces, metrics, and logs. For LLM systems, traces are the most important. To make them useful, you need to understand a few building blocks:</p>
<ul>
<li><p>a <strong>trace</strong> represents a single end-to-end request</p>
</li>
<li><p>a <strong>span</strong> is a timed operation within that trace</p>
</li>
<li><p><strong>attributes</strong> are key–value metadata attached to spans</p>
</li>
<li><p><strong>events</strong> are time-stamped annotations</p>
</li>
<li><p><strong>context propagation</strong> ensures child spans attach to the correct parent.</p>
</li>
</ul>
<p>FastAPI’s async nature makes correct context propagation essential, but OpenTelemetry’s Python SDK handles this as long as spans are created correctly.</p>
<p>With those concepts in place, the next step is to wire OpenTelemetry into the app. Start by configuring the OpenTelemetry SDK in FastAPI: define a <code>TracerProvider</code>, attach a <code>Resource</code> (service name and environment), configure an exporter (Jaeger, Tempo, Phoenix, and so on), and enable FastAPI auto-instrumentation.</p>
<h2 id="heading-designing-llm-aware-spans">Designing LLM-Aware Spans</h2>
<h3 id="heading-span-taxonomy">Span Taxonomy</h3>
<p>A clean span hierarchy is critical. In this guide, a single <code>http.request</code> span (usually auto-generated) acts as the root, and it contains child spans such as <code>rag.retrieval</code>, <code>rag.prompt.build</code>, <code>llm.call</code>, <code>llm.postprocess</code>, and, optionally, <code>llm.eval</code>. Each of these spans represents a logical unit of work rather than an implementation detail.</p>
<h3 id="heading-span-boundaries">Span Boundaries</h3>
<p>Getting span boundaries right is just as important as picking the right span names. Avoid extremes like wrapping the entire LLM workflow in one giant span, creating a separate span for every token, or dumping all data into logs.</p>
<p>Instead, aim for a few coarse-grained spans that each represent a meaningful step in the request, enrich them with well-chosen attributes, and use events to mark important milestones within a span rather than splitting everything into smaller spans.</p>
<h3 id="heading-instrumenting-the-llm-call">Instrumenting the LLM Call</h3>
<p>When instrumenting the LLM call, treat it as the most critical span in the trace. Whether you are calling OpenAI, Anthropic, or another provider, start the span immediately before the API request and end it only after the full response (or stream) is complete.</p>
<p>Within that span, capture retries, timeouts, and errors so it becomes the central place for latency analysis, cost attribution, and prompt debugging.</p>
<p>For streaming responses, you can emit events for each chunk to track progress, but avoid creating separate child spans unless you truly need fine-grained timing.</p>
<h2 id="heading-fastapi-example-end-to-end-llm-spans-complete-and-explained">FastAPI Example: End-to-End LLM Spans (Complete and Explained)</h2>
<pre><code class="language-python">from fastapi import FastAPI, Request
from opentelemetry import trace
from opentelemetry.trace import Tracer
from typing import List
import asyncio
import hashlib

# Obtain a tracer instance from OpenTelemetry.
# All spans created with this tracer will be part of the same distributed
# tracing system and exported to the configured backend.
tracer: Tracer = trace.get_tracer(__name__)

# Initialize the FastAPI application.
app = FastAPI()

# Helper functions used by the observable endpoint
async def retrieve_documents(query: str) -&gt; List[str]:
    """
    Simulate document retrieval (e.g., vector search or knowledge base lookup).
    This function represents the retrieval stage in a RAG pipeline.
    In a real system, this might query a vector database or search index.
    """
    await asyncio.sleep(0.05)  # Simulate I/O latency
    return [
        "FastAPI enables high-performance async APIs.",
        "OpenTelemetry provides vendor-neutral observability.",
        "LLM observability requires tracing prompts and tokens.",
    ]


def build_prompt(query: str, documents: List[str]) -&gt; str:
    """
    Construct the final prompt from retrieved documents and the user query.
    Prompt construction is kept separate so it can be observed or modified
    independently if needed (for example, to measure prompt assembly latency).
    """
    context = "\n".join(documents)
    return f"""
Context:
{context}

Question:
{query}
"""


class LLMResponse:
    """
    Minimal abstraction for an LLM response.
    This keeps the example self-contained while still allowing us to attach
    token usage and other metadata for observability.
    """

    def __init__(self, text: str, prompt_tokens: int, completion_tokens: int):
        self.text = text
        self.prompt_tokens = prompt_tokens
        self.completion_tokens = completion_token
    
    @property
    def total_tokens(self) -&gt; int:
        return self.prompt_tokens + self.completion_tokens

async def call_llm(prompt: str) -&gt; LLMResponse:
    """
    Simulate an LLM API call.
    In a real implementation, this would call OpenAI, Anthropic, or another
    provider. The artificial delay represents model latency.
    """
    await asyncio.sleep(0.2)  # Simulate inference time
    response_text = "FastAPI and OpenTelemetry enable end-to-end LLM observability."
    # Token count is approximated here for demonstration purposes.
    prompt_tokens = len(prompt.split())
    completion_tokens = len(response_text.split())
    return LLMResponse(response_text, prompt_tokens, completion_tokens)


def summarize_response(response: LLMResponse) -&gt; str:
    """
    Example post-processing step.
    Post-processing is separated into its own phase so any additional latency
    or errors are not incorrectly attributed to the LLM itself.
    """
    return response.text


# Observable FastAPI endpoint
@app.post("/query")
async def rag_query(request: Request, query: str):
    """
    Handle a single RAG-style request with explicit OpenTelemetry spans.
    This endpoint demonstrates how to create one trace per request, with child
    spans for retrieval, LLM invocation, and post-processing.
    """

    # Create a top-level span for the HTTP request.
    # Even if FastAPI auto-instrumentation is enabled, defining this explicitly
    # allows us to attach domain-specific metadata.
    with tracer.start_as_current_span("http.request") as http_span:
        http_span.set_attribute("http.method", "POST")
        http_span.set_attribute("http.route", "/query")

        # Retrieval phase
        # This span isolates the retrieval step so that relevance issues can be
        # debugged independently of LLM behavior.
        with tracer.start_as_current_span("rag.retrieval") as retrieval_span:
            retrieval_span.set_attribute("rag.top_k", 5)
            retrieval_span.set_attribute("rag.similarity_threshold", 0.8)
            documents = await retrieve_documents(query)

            # Record how many documents were returned.
            # This is a key signal when diagnosing hallucinations
            # or missing context in the final response.
            retrieval_span.set_attribute(
                "rag.documents_returned",
                len(documents),
            )

        # LLM invocation phase
        # This span wraps the actual LLM call and is the primary anchor for
        # latency, cost, and prompt-related analysis.
        with tracer.start_as_current_span("llm.call") as llm_span:
            llm_span.set_attribute("llm.provider", "example")
            llm_span.set_attribute("llm.model", "example-llm")
            llm_span.set_attribute("llm.temperature", 0.7)
            llm_span.set_attribute("llm.prompt_template_id", "rag_v1")

            # Build the final prompt using retrieved context.
            # The raw prompt is intentionally not stored as a span attribute.
            prompt = build_prompt(query, documents)
            
            # Prompt metadata
            prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()
            llm_span.set_attribute("llm.prompt_hash", prompt_hash)
            llm_span.set_attribute("llm.prompt_length", len(prompt))

            response = await call_llm(prompt)

            # Hash the response instead of storing raw text.
            # This allows correlation across traces without exposing content.
            response_hash = hashlib.sha256(
                response.text.encode()
            ).hexdigest()
            llm_span.set_attribute("llm.response_hash", response_hash)

            # Record token usage to enable cost attribution
            # and capacity planning.
            llm_span.set_attribute("llm.usage.prompt_tokens", response.prompt_tokens)
            llm_span.set_attribute("llm.usage.completion_tokens", response.completion_tokens)
            llm_span.set_attribute("llm.usage.total_tokens", response.total_tokens)
            
            # example price per token
            estimated_cost = response.total_tokens * 0.000002
            llm_span.set_attribute("llm.cost_estimated_usd", estimated_cost)

        # Post-processing phase
        # Any transformation after the LLM response is captured here,
        # ensuring inference latency is not overstated.
        with tracer.start_as_current_span("llm.postprocess") as post_span:
            summary = summarize_response(response)
            post_span.set_attribute(
                "llm.summary_length",
                len(summary),
            )

    # Return the final response to the client.
    # All spans above belong to the same distributed trace.
    return {"summary": summary}
</code></pre>
<p>Before examining the full code example, it helps to understand how the instrumentation relates to the observability principles described earlier in this article.</p>
<p>The goal of the example is not simply to show how to create spans, but to demonstrate how a single user request can be represented as a structured trace containing meaningful metadata about each stage of the LLM pipeline.</p>
<p>At a high level, the code follows three key design ideas:</p>
<ol>
<li><p>One trace per user request</p>
</li>
<li><p>One span per logical LLM workflow stage</p>
</li>
<li><p>Semantic attributes attached to spans for debugging, cost tracking, and analysis</p>
</li>
</ol>
<p>Each of these concepts directly corresponds to the observability practices discussed earlier.</p>
<h3 id="heading-top-level-request-span">Top-Level Request Span</h3>
<p>The FastAPI endpoint begins by creating a top-level span called <code>http.request</code>. This span represents the entire lifecycle of the incoming request and serves as the root span for the trace.</p>
<pre><code class="language-python">with tracer.start_as_current_span("http.request") as http_span:
</code></pre>
<p>Although FastAPI can generate HTTP spans automatically through OpenTelemetry auto-instrumentation, explicitly creating this span allows the application to attach domain-specific metadata such as route names or user identifiers.</p>
<p>Attributes such as the HTTP method and route are attached here:</p>
<pre><code class="language-python">http_span.set_attribute("http.method", "POST")
http_span.set_attribute("http.route", "/query")
</code></pre>
<p>This ensures that every trace can be easily filtered by endpoint when analyzing production traffic.</p>
<h3 id="heading-retrieval-span">Retrieval Span</h3>
<p>The next span captures the retrieval phase of the RAG pipeline:</p>
<pre><code class="language-python">with tracer.start_as_current_span("rag.retrieval") as retrieval_span:
</code></pre>
<p>This span isolates the vector search or knowledge retrieval step from the rest of the pipeline. If users report irrelevant answers, engineers can inspect this span to determine whether the issue originates from poor retrieval results rather than model behavior.</p>
<p>Several semantic attributes are attached here:</p>
<ul>
<li><p><code>rag.top_k</code> – number of documents requested</p>
</li>
<li><p><code>rag.similarity_threshold</code> – similarity cutoff used for filtering results</p>
</li>
<li><p><code>rag.documents_returned</code> – number of documents actually retrieved</p>
</li>
</ul>
<p>These attributes align with the RAG observability signals discussed in the earlier section of the article.</p>
<h3 id="heading-llm-invocation-span">LLM Invocation Span</h3>
<p>The most important span in the trace is the <code>llm.call</code> span, which wraps the actual model invocation.</p>
<pre><code class="language-python">with tracer.start_as_current_span("llm.call") as llm_span:
</code></pre>
<p>This span captures the latency, configuration, and token usage associated with the LLM request. In production systems, it becomes the primary location for analyzing model behavior and cost.</p>
<p>Key attributes recorded in this span include:</p>
<ul>
<li><p><code>llm.provider</code> – the model provider (OpenAI, Anthropic, etc.)</p>
</li>
<li><p><code>llm.model</code> – the specific model version</p>
</li>
<li><p><code>llm.temperature</code> – sampling parameter controlling response randomness</p>
</li>
<li><p><code>llm.prompt_template_id</code> – identifier for the prompt template used</p>
</li>
</ul>
<p>These attributes make it possible to correlate changes in model configuration with downstream quality or cost changes.</p>
<h3 id="heading-prompt-handling-and-privacy">Prompt Handling and Privacy</h3>
<p>Instead of storing the full prompt or response text directly in the trace, the example demonstrates a safer practice: hashing sensitive data.</p>
<pre><code class="language-python">response_hash = hashlib.sha256(response.text.encode()).hexdigest()
</code></pre>
<p>The resulting hash is stored as a span attribute:</p>
<pre><code class="language-python">llm_span.set_attribute("llm.response_hash", response_hash)
</code></pre>
<p>This approach allows engineers to correlate repeated responses across traces without exposing potentially sensitive content in observability systems.</p>
<h3 id="heading-token-usage-tracking">Token Usage Tracking</h3>
<p>The <code>llm.call</code> span also records token usage:</p>
<pre><code class="language-python">llm_span.set_attribute(
    "llm.usage.total_tokens",
    response.total_tokens
)
</code></pre>
<p>Capturing token usage at the span level is critical for monitoring cost and efficiency, since token consumption directly determines billing for most LLM providers.</p>
<h3 id="heading-post-processing-span">Post-Processing Span</h3>
<p>Finally, the example includes a <code>llm.postprocess</code> span:</p>
<pre><code class="language-python">with tracer.start_as_current_span("llm.postprocess") as post_span:
</code></pre>
<p>This span represents any transformation applied after the model generates its response. Separating post-processing from the LLM call ensures that additional latency — such as formatting, filtering, or validation — is not incorrectly attributed to the model itself.</p>
<p>An attribute such as response length is recorded here:</p>
<pre><code class="language-python">post_span.set_attribute("llm.summary_length", len(summary))
</code></pre>
<p>This can be useful when diagnosing issues such as unexpectedly short or truncated outputs.</p>
<h3 id="heading-how-the-spans-form-a-complete-trace">How the Spans Form a Complete Trace</h3>
<p>When the request finishes, all spans belong to the same distributed trace:</p>
<pre><code class="language-plaintext">http.request
 ├── rag.retrieval
 ├── llm.call
 └── llm.postprocess
</code></pre>
<p>This hierarchy reflects the logical workflow of a retrieval-augmented LLM system. Because each span contains structured metadata, engineers can quickly answer questions such as:</p>
<ul>
<li><p>Was the latency caused by retrieval or model inference?</p>
</li>
<li><p>How many documents influenced the prompt?</p>
</li>
<li><p>Which model configuration produced the response?</p>
</li>
<li><p>How many tokens were consumed?</p>
</li>
<li><p>Was the response post-processed or truncated?</p>
</li>
</ul>
<p>This structured trace design is what transforms observability from simple monitoring into a practical debugging and optimization tool for LLM systems.</p>
<h2 id="heading-semantic-attributes-best-practices-for-llm-observability">Semantic Attributes: Best Practices for LLM Observability</h2>
<p>The goal is not to capture every possible detail, but to record the minimal set of stable, high-signal attributes that enable effective debugging, cost control, and quality analysis in production. Poor attribute design leads to noisy traces, privacy risks, and dashboards that are impossible to reason about.</p>
<h3 id="heading-prompt-response-and-model-metadata">Prompt, Response, and Model Metadata​</h3>
<p>Storing raw prompts is often unsafe and expensive, so it is better to record minimal, structured metadata instead. In practice, this means attaching a stable template identifier with <code>llm.prompt_template_id</code>, a hashed version of the final prompt using <code>llm.prompt_hash</code> (to avoid storing raw text), and a size indicator such as <code>llm.prompt_length</code>, which captures the number of tokens or characters.</p>
<p>You should also always record key inference parameters: <code>llm.provider</code> (for example, "openai" or "anthropic"), <code>llm.model</code> (for example, "gpt-4.1"), <code>llm.temperature</code> and <code>llm.top_p</code> (sampling parameters), <code>llm.max_tokens</code> (the maximum tokens allowed), and <code>llm.stream</code> to indicate whether streaming was enabled, while staying within your organization’s privacy and compliance requirements.</p>
<pre><code class="language-python">
with tracer.start_as_current_span("llm.call") as llm_span:
            llm_span.set_attribute("llm.provider", "example")
            llm_span.set_attribute("llm.model", "example-llm")
            llm_span.set_attribute("llm.temperature", 0.7)
            llm_span.set_attribute("llm.top_p", 0.9)
            llm_span.set_attribute("llm.max_tokens", 512)
            llm_span.set_attribute("llm.stream", False)
            llm_span.set_attribute("llm.prompt_template_id", "rag_v1")

            # Build the final prompt using retrieved context.
            # The raw prompt is intentionally not stored as a span attribute.
            prompt = build_prompt(query, documents)
            
            # Prompt metadata
            prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()
            llm_span.set_attribute("llm.prompt_hash", prompt_hash)
            llm_span.set_attribute("llm.prompt_length", len(prompt))
</code></pre>
<h3 id="heading-token-usage-and-cost-why-this-matters-in-practice">Token Usage and Cost (Why This Matters in Practice)</h3>
<p>Token usage is one of the most common blind spots in LLM systems. Many teams monitor latency and error rates but discover runaway costs only after invoices spike. Because token consumption varies significantly by prompt structure, retrieved context, and model configuration, it must be captured explicitly at the span level.​</p>
<p>The most important practice is to record token usage at the end of the LLM span, once the model has completed inference. This ensures that the values reflect the full request rather than partial or streamed output.</p>
<p>At minimum, capture the attributes:​<code>llm.usage.prompt_tokens</code> ,<code>llm.usage.completion_tokens</code> and <code>llm.usage.total_tokens</code>​.</p>
<pre><code class="language-python">def __init__(self, text: str, prompt_tokens: int, completion_tokens: int):
        self.text = text
        self.prompt_tokens = prompt_tokens
        self.completion_tokens = completion_token
    
    @property
    def total_tokens(self) -&gt; int:
        return self.prompt_tokens + self.completion_tokens

async def call_llm(prompt: str) -&gt; LLMResponse:
    """
    Simulate an LLM API call.
    In a real implementation, this would call OpenAI, Anthropic, or another
    provider. The artificial delay represents model latency.
    """
    await asyncio.sleep(0.2)  # Simulate inference time
    response_text = "FastAPI and OpenTelemetry enable end-to-end LLM observability."
    # Token count is approximated here for demonstration purposes.
    prompt_tokens = len(prompt.split())
    completion_tokens = len(response_text.split())
    return LLMResponse(response_text, prompt_tokens, completion_tokens)
</code></pre>
<p>These values allow you to distinguish between requests that are expensive because of large prompts (often caused by excessive retrieval or poor prompt construction) versus those that are expensive because of long model-generated outputs.</p>
<p>*Where possible, also attach an estimated cost:*​ <code>llm.cost_estimated_usd</code>​</p>
<pre><code class="language-python">    # example price per token
    estimated_cost = response.total_tokens * 0.000002
    llm_span.set_attribute("llm.cost_estimated_usd", estimated_cost)
</code></pre>
<p>This value is typically derived by multiplying token counts by the model's published pricing. Even if the estimate is approximate, it enables powerful analysis. For example, you can identify which endpoints, prompt templates, or user flows are responsible for the highest cumulative cost, rather than relying on coarse, account-level billing dashboards.</p>
<p>Once spans carry the right attributes, the next step is to connect them to output quality, not just system health.</p>
<h2 id="heading-evaluation-hooks-inside-traces">Evaluation Hooks Inside Traces</h2>
<p>This section describes an additional pattern you can layer on top of the core instrumentation in this guide. It is optional and not implemented in the sample code, but it shows how to attach quality signals directly to your traces.</p>
<p>Observability is not just about whether the system stayed up, it is also about whether the model produced a useful answer. Evaluation hooks inside traces let you attach lightweight quality signals directly to the same spans you use for latency and cost.</p>
<p>Inline evaluations are the simplest approach. You can run quick checks synchronously and record the results as span attributes, such as <code>llm.eval.passed</code> for a simple boolean check, <code>llm.eval.relevance_score</code> for an optional numerical score, or flags like <code>llm.eval.hallucination_detected</code> and <code>llm.eval.refusal_detected</code>. These attributes travel with the trace, so you can filter and aggregate on them in your observability backend just like any other field.</p>
<p>For higher accuracy, you can introduce model-based evaluation as a separate step. In this pattern, an evaluator LLM runs asynchronously on the original prompt and response, and its work is captured in a child span (for example, <code>llm.eval</code>) that shares the same trace ID as the main <code>llm.call</code> span. You then attach scores such as relevance, faithfulness, or toxicity to that evaluation span.</p>
<p>Because the evaluation span shares the same trace ID, you can correlate quality regressions with changes in prompts or retrieval.</p>
<h2 id="heading-exporting-and-visualizing-traces-where-this-fits-with-vendor-tooling">Exporting and Visualizing Traces (Where This Fits with Vendor Tooling)</h2>
<p>This code-first observability design is vendor-agnostic. Once traces are emitted using OpenTelemetry, they can be exported to different backends without changing instrumentation.</p>
<p>General-purpose tracing systems like Jaeger and Grafana Tempo help engineers debug latency, errors, and request flow across retrieval, prompting, and model calls, answering how the system behaved. LLM-focused platforms such as Arize Phoenix use the same data but add model-specific insights like prompt clustering, token analysis, and quality correlation.</p>
<p>Because instrumentation stays OpenTelemetry-native, you maintain full control over attributes and trace structure while still using vendor dashboards, and you can switch backends as your needs evolve without touching the application code.</p>
<h2 id="heading-operational-patterns-and-anti-patterns">Operational Patterns and Anti-Patterns</h2>
<p>Effective LLM observability requires disciplined practices. High-volume systems should sample traces to limit overhead, and prompts or responses should be hashed by default to reduce storage and privacy risk. Traces must be treated as production data, with proper access control and retention policies.</p>
<p>Common pitfalls include relying only on vendor SDK traces, logging prompts without trace correlation, or ignoring evaluation signals. These issues fragment visibility and hide quality regressions, especially when observability focuses only on agents instead of full application context.</p>
<h2 id="heading-extending-the-system">Extending the System</h2>
<p>Once traces are reliable, they support advanced capabilities. Metrics like p95 latency can be derived from spans, logs can be linked using trace IDs, and historical traces can power offline evaluation or prompt testing.​</p>
<p>By following OpenTelemetry conventions, the observability stack also stays aligned with emerging LLM semantic standards, keeping the system flexible and future-proof.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>End-to-end LLM observability is not achieved by installing another agent. It is achieved through intentional span design, meaningful semantic attributes, and, where needed, lightweight evaluation hooks.​</p>
<p>By treating LLM calls as first-class operations within distributed traces, you gain faster debugging, controlled costs, safer deployments, and measurable quality improvements. The backend — Jaeger, Tempo, Phoenix — is interchangeable. The instrumentation strategy is not.​</p>
<p>A well-designed trace is the most valuable artifact in a production LLM system.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use WebSockets: From Python to FastAPI ]]>
                </title>
                <description>
                    <![CDATA[ Real-time data powers much of modern software: live stock prices, chat applications, sports scores, collaborative tools. And to build these systems, you'll need to understand how real-time communicati ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-websockets-from-python-to-fastapi/</link>
                <guid isPermaLink="false">69b206806c896b0519d2a308</guid>
                
                    <category>
                        <![CDATA[ websockets ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Backend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Real Time ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nneoma Uche ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2026 00:19:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8acb0854-7289-4794-8f97-242ec5d8ca61.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Real-time data powers much of modern software: live stock prices, chat applications, sports scores, collaborative tools. And to build these systems, you'll need to understand how real-time communication actually works—which isn’t always straightforward.</p>
<p>I ran into this firsthand while trying to build a live options dashboard. HTTP requests weren't going to cut it, and everything I was reading seemed overly complex until I went back to the basics. This article is the result of that process.</p>
<p>We'll cover Python's <code>websockets</code> library from scratch, then move into FastAPI, where many Python backends live. It's worth noting that WebSockets aren't the only solution for real-time communication. WebRTC may be a better fit depending on your use case, but understanding WebSockets is the right starting point before exploring further.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ol>
<li><p><a href="#heading-websocket-connections-and-methods">WebSocket Connections and Methods</a></p>
</li>
<li><p><a href="#heading-how-to-build-your-first-websocket-in-python">How to Build Your First WebSocket in Python</a></p>
</li>
<li><p><a href="#heading-file-transfer-over-websockets">File Transfer Over WebSockets</a></p>
</li>
<li><p><a href="#heading-how-to-connect-to-an-external-websocket">How to Connect to an External WebSocket</a></p>
</li>
<li><p><a href="#heading-websockets-in-fastapi">WebSockets in FastAPI</a></p>
</li>
<li><p><a href="#heading-how-to-handle-websocket-disconnections-in-fastapi">How to Handle WebSocket Disconnections in FastAPI</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-websocket-connections-and-methods">WebSocket Connections and Methods</h2>
<p>A WebSocket connection enables bi-directional communication between a client and a server. Once a connection is established, both sides can communicate freely without either having to ask first. This is different from a regular HTTP request, where the client always has to ask before the server can respond.</p>
<p>It looks something like this:</p>
<pre><code class="language-plaintext">        CLIENT&nbsp; &lt;===== open connection =====&gt;&nbsp; SERVER
</code></pre>
<p>Note that a WebSocket URL is not a regular web page, so you can't "visit it" like a website. You need a client to talk to it.</p>
<p>Different frameworks provide different methods for handling WebSocket connections. With Python’s <code>websockets</code> library, for instance, a connection is automatically accepted the moment a client connects. With frameworks like FastAPI, you have to explicitly call <code>await websocket.accept()</code>, otherwise the connection gets rejected.</p>
<p>Let’s look at the core methods provided by Python’s <code>websockets</code> library:</p>
<ol>
<li><p><code>websockets.serve(...)</code>:&nbsp; starts a WebSocket server.</p>
</li>
<li><p><code>websockets.connect(...)</code>: connects to a WebSocket server.</p>
</li>
<li><p><code>websockets.send(...)</code>: sends a message from either side.</p>
</li>
<li><p><code>websockets.recv()</code>: receives a message from client or server.</p>
</li>
</ol>
<p><code>recv()</code> takes no arguments because it's purely a waiting operation. It waits for the next message and returns it:</p>
<pre><code class="language-python">message = await websocket.recv()
</code></pre>
<h2 id="heading-how-to-build-your-first-websocket-in-python">How to Build Your First WebSocket in Python</h2>
<p>Before we dive into frameworks, let’s explore Python’s <code>websockets</code> library. You’ll set up a simple server and client, and exchange messages over a WebSocket connection, giving you a solid foundation for understanding WebSockets under the hood.</p>
<h3 id="heading-environment-setup">Environment Setup</h3>
<p>Run the following in your virtual environment to install or verify the WebSockets package:</p>
<pre><code class="language-python">pip install websockets
# or, to check if it's already installed:
pip show websockets
</code></pre>
<h3 id="heading-create-the-websocket-server">Create the WebSocket Server</h3>
<p>Create <code>server.py</code> in your project folder, and paste this:</p>
<pre><code class="language-python">import asyncio
import websockets

async def handler(connection):
    print("Client connected")

    message = await connection.recv()
    print("Received from client:", message)
    await connection.send("Hello client!")


async def main():
    async with websockets.serve(handler, "localhost", 8000):
        print("Server running at ws://localhost:8000")
        #await asyncio.Future()  # runs forever
        await asyncio.sleep(30)

asyncio.run(main())
</code></pre>
<p>When this line executes:</p>
<pre><code class="language-python">async with websockets.serve(handler, "localhost", 8000):
</code></pre>
<p>The library opens a TCP socket on the specified host and port and waits for incoming clients. When one connects, it creates a connection object and passes it into your handler function.</p>
<p>The handler is required because it defines what the server does with each connection. The <code>host</code> and <code>port</code> arguments are also important. Both default to <code>None</code> – passing neither raises an error because the OS cannot bind a network server without a port.</p>
<p>You could pass <code>port=0</code> to let the OS assign a free port automatically, but then you'd need an extra step to figure out which port was chosen, so the client can connect:</p>
<pre><code class="language-python">server.sockets[0].getsockname()
</code></pre>
<p>It’s simpler to specify both host and port explicitly, so the client knows exactly where the server is running.</p>
<h3 id="heading-set-up-the-client">Set Up the Client</h3>
<p>Create <code>client.py</code> in the same folder and add this:</p>
<pre><code class="language-python">import asyncio
import websockets

async def client():
    async with websockets.connect("ws://localhost:8000") as websocket:
        await websocket.send("Hello server!")
        response = await websocket.recv()
        print("Server replied:", response)

asyncio.run(client())
</code></pre>
<h3 id="heading-test-the-connection">Test the Connection</h3>
<p>First, open a terminal and run <code>server.py</code>. You should see:</p>
<pre><code class="language-plaintext">Server running at ws://localhost:8000
</code></pre>
<p>In a second terminal, run <code>client.py</code>. Messages should appear in both terminals confirming that the connection is active and both sides are communicating.</p>
<p>Note that the server must be running before you start the client – otherwise the client has nothing to connect to, and the connection will fail.</p>
<h4 id="heading-keeping-the-server-alive-a-note-on-asynciofuture">Keeping the server alive: a note on asyncio.Future()</h4>
<p>In <code>server.py</code>, there’s a line currently commented out:</p>
<pre><code class="language-python">await asyncio.Future()
</code></pre>
<p>This keeps the server running indefinitely. For local development and testing however, <code>await asyncio.sleep(30)</code> is a simpler alternative. It keeps the server alive for a fixed period without running forever.</p>
<h2 id="heading-file-transfer-over-websockets">File Transfer Over WebSockets</h2>
<p>WebSockets aren't limited to text. They support raw bytes too, which means you can send files directly over the connection. Here’s how a client can send a file to a server over a WebSocket connection:</p>
<h3 id="heading-update-serverpy">Update <code>server.py</code></h3>
<pre><code class="language-python">async def file_handler(ws):
    print("Client connected, waiting for file...")
    file_bytes = await ws.recv()  # receive bytes
    with open("received_file.png", "wb") as f:
        f.write(file_bytes)
    print("File received and saved!")
    await ws.send("File received successfully!")

async def main():
    async with websockets.serve(file_handler, "localhost", 8000):
        print("Server running on ws://localhost:8000")
        await asyncio.sleep(50)  # keep server alive

asyncio.run(main())
</code></pre>
<p>The handler waits for incoming bytes with <code>await ws.recv()</code>; the <code>websockets</code> library automatically detects whether the incoming message is text or bytes, so no extra configuration is needed. Once received, the file is written to disk in binary mode (<code>"wb"</code>) and the server sends a confirmation message back to the client.</p>
<h3 id="heading-update-clientpy">Update <code>client.py</code></h3>
<pre><code class="language-python">import asyncio
import websockets

async def send_file():
    uri = "ws://localhost:8000"
    async with websockets.connect(uri) as ws:
        with open("portfolio-image.png", "rb") as f:  #open file in binary mode
            file_bytes = f.read()
        await ws.send(file_bytes)  # send bytes
        response = await ws.recv()
        print("Server response:", response)

asyncio.run(send_file())
</code></pre>
<p>The client opens the image in binary mode (<code>"rb"</code>), reads the entire file into memory as bytes, and sends it in a single <code>ws.send()</code> call. It then waits for the server's confirmation before closing the connection.</p>
<h3 id="heading-test-it">Test it</h3>
<p>Add an image to your project folder and make sure the filename in <code>client.py</code> matches. Run <code>server.py</code> first, then <code>client.py</code> in a second terminal.</p>
<p>Once the transfer completes, the server saves the file as <code>received_file.png</code> in the same directory. You should see it appear in your workspace immediately.</p>
<p>This approach loads the entire file into memory before sending. For large files, it’s better to read and send them in chunks. But this is the easiest way to understand WebSocket byte transfer.</p>
<h2 id="heading-how-to-connect-to-an-external-websocket">How to Connect to an External WebSocket</h2>
<p>So far you've been connecting to servers you built yourself. But WebSocket clients can also connect to public servers. For example, a client can connect to Postman’s echo server:</p>
<pre><code class="language-python">import asyncio
import websockets

async def connect_external():
    uri = "wss://ws.postman-echo.com/raw"  # public WebSocket server
    async with websockets.connect(uri) as ws:
        print("Connected to external server!")

        # Send a message
        await ws.send("Hello external server!")
        print("Message sent")

        # Receive response
        response = await ws.recv()
        print("Received from server:", response)
asyncio.run(connect_external())
</code></pre>
<p>Notice the client connects to Postman’s echo server using the <code>wss://</code> URI scheme instead of <code>ws://</code>. This indicates the connection is encrypted using TLS, similar to how <code>https://</code> secures regular web requests.</p>
<p>An echo server returns exactly what you send it. So "Hello external server!" comes straight back as the response. It's a useful sandbox for testing your client-side WebSocket code without needing your own server.</p>
<h2 id="heading-websockets-in-fastapi">WebSockets in FastAPI</h2>
<p>FastAPI provides a WebSocket object (via Starlette under the hood) to manage real-time connections. You can define WebSocket endpoints just like HTTP routes, while Uvicorn handles the event loop – no manual asyncio server management needed. This makes FastAPI a natural fit for real-time projects, from chat apps to live dashboards and data feeds.</p>
<p>Before jumping into code, here's a quick reference of the core methods you'll be working with.</p>
<p><strong>Accepting:</strong></p>
<ul>
<li><code>await websocket.accept()</code>: the <code>accept()</code> method must be called first, before anything else. Skip it and the connection gets rejected.</li>
</ul>
<p><strong>Sending:</strong></p>
<ul>
<li><p><code>await websocket.send_text(data)</code>: sends a string.</p>
</li>
<li><p><code>await websocket.send_bytes(data)</code>: sends binary data.</p>
</li>
<li><p><code>await websocket.send_json(data)</code>: serializes and sends JSON.</p>
</li>
</ul>
<p><strong>Receiving:</strong></p>
<ul>
<li><p><code>await websocket.receive_text()</code>: waits for a text message.</p>
</li>
<li><p><code>await websocket.receive_bytes()</code>: waits for binary data.</p>
</li>
<li><p><code>await websocket.receive_json()</code>: receives and deserializes JSON.</p>
</li>
<li><p><code>async for msg in websocket.iter_text()</code>: iterates over incoming messages, exits cleanly on disconnect.</p>
</li>
</ul>
<p><strong>Closing:</strong></p>
<ul>
<li><code>await websocket.close(code=1000)</code>: standard code for a normal closure. It accepts an optional “reason” argument.</li>
</ul>
<p>Here's what the WebSocket lifecycle looks like in FastAPI:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6426acfa5bc738c37852b5bd/b61b6b25-e027-47a1-8efc-b921e93b8521.png" alt="WebSocket in FastAPI" style="display:block;margin:0 auto" width="689" height="583" loading="lazy">

<h3 id="heading-building-a-simple-echo-server-with-fastapi">Building a Simple Echo Server with FastAPI</h3>
<p>As you saw with the Postman example, an echo server sends back the message a client provides. Let's build one with FastAPI.</p>
<h4 id="heading-1-install-fastapi">1. Install FastAPI:</h4>
<pre><code class="language-python">pip install "fastapi[standard]"
</code></pre>
<h4 id="heading-2-update-serverpy">2. Update <code>server.py</code>:</h4>
<pre><code class="language-python">from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    data = await websocket.receive_text()
   
    await websocket.send_text(f"You said: {data}")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
</code></pre>
<p>A few things to note here compared to the plain <code>websockets</code> library:</p>
<ul>
<li><p>WebSocket endpoints are defined with <code>@app.websocket("/ws")</code> just like an HTTP route.</p>
</li>
<li><p><code>await websocket.accept()</code> is required before anything else. FastAPI won't accept connections without it.</p>
</li>
<li><p>Uvicorn handles the event loop and server startup for you via the <code>if name == "__main__"</code> block. No <code>asyncio.run()</code> or <code>asyncio.Future()</code> needed.</p>
</li>
</ul>
<h4 id="heading-3-update-clientpy">3. Update client.py:</h4>
<pre><code class="language-python">async def test_client():
    uri = "ws://127.0.0.1:8000/ws"
    async with websockets.connect(uri) as ws:
        await ws.send("Hello FastAPI server!")
        response = await ws.recv()
        print("Server replied:", response)

asyncio.run(test_client())
</code></pre>
<p>Since the FastAPI server isn't secured with TLS, the client URI uses <code>ws://</code> instead of <code>wss://</code>. Make sure to match the host and port from your server code.</p>
<h4 id="heading-4-interact-with-the-echo-server">4. Interact with the echo server:</h4>
<p>Start <code>server.py</code>, then run <code>client.py</code> in another terminal. The server terminal should show the echoed message.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6426acfa5bc738c37852b5bd/88629d79-eb91-4af5-9752-f9596ff5e5a4.png" alt="88629d79-eb91-4af5-9752-f9596ff5e5a4" style="display:block;margin:0 auto" width="495" height="101" loading="lazy">

<h2 id="heading-how-to-handle-websocket-disconnections-in-fastapi">How to Handle WebSocket Disconnections in FastAPI</h2>
<p>Clients will inevitably disconnect in real-time applications, sometimes intentionally, sometimes unexpectedly. If not handled properly, this can crash your server or leave it in a broken state.</p>
<p>The <code>WebSocketDisconnect</code> exception in FastAPI is raised whenever a client unexpectedly closes the connection, allowing the server to handle disconnects gracefully, log the event, and clean up resources without crashing.</p>
<p>Here’s an example:</p>
<pre><code class="language-python">@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    await ws.accept()
    try:
        while True:
            data = await ws.receive_text()
   
            if "bye" in data or "quit" in data:
                await ws.send_text("Closing connection")
                await ws.close(code=1000, reason="Server requested close")  
                break
            await ws.send_text(f"I got your request: {data}")
    except WebSocketDisconnect:
        print("Client disconnected")  # connection already closed
</code></pre>
<p>The server runs a continuous loop waiting for messages. If the client message contains "bye" or "quit", the server responds, calls <code>await ws.close(code=1000)</code>, and breaks out of the loop cleanly.</p>
<p>But if the client disconnects unexpectedly, <code>WebSocketDisconnect</code> is caught by the except block and the server moves on without crashing. At this point the connection is already closed on the client side, so calling <code>ws.close()</code> inside the except block is unnecessary.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>WebSockets make real-time communication possible by keeping a persistent connection open between client and server. Starting with Python’s <code>websockets</code> library helps clarify how the protocol works under the hood, while frameworks like FastAPI provide the structure needed for production applications.</p>
<p>The parts that trip most people up early on are <code>asyncio</code> and FastAPI's explicit <code>websocket.accept()</code>. With <code>asyncio</code>, the question is usually why it's needed and why the server dies instantly without something keeping it alive. And it's easy to ignore <code>websocket.accept()</code> if you're coming from the plain <code>websockets library</code> where that happens automatically. Once those click, everything else follows naturally.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build and Deploy a Blog-to-Audio Service Using OpenAI ]]>
                </title>
                <description>
                    <![CDATA[ Turning written blog posts into audio is a simple way to reach more people. Many users prefer listening during travel or workouts. Others enjoy having both reading and listening options.  With OpenAI’s text-to-speech models, you can build a clean ser... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-and-deploy-blog-to-audio-openai/</link>
                <guid isPermaLink="false">69671ceac3577e1210128477</guid>
                
                    <category>
                        <![CDATA[ Accessibility ]]>
                    </category>
                
                    <category>
                        <![CDATA[ openai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 14 Jan 2026 04:34:50 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768359861591/69bc8279-f882-4af1-9375-5576f7043b48.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Turning written blog posts into audio is a simple way to reach more people. Many users prefer listening during travel or workouts. Others enjoy having both reading and listening options. </p>
<p>With OpenAI’s <a target="_blank" href="https://platform.openai.com/docs/guides/text-to-speech">text-to-speech</a> models, you can build a clean service that takes a blog URL or pasted text and produces a natural-sounding audio file. </p>
<p>In this article, you’ll learn how to build this system end-to-end. You will learn how to fetch blog content, send it to OpenAI’s audio API, save the output as an MP3 file, and serve everything through a small <a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a> app. </p>
<p>At the end, you’ll also build a minimal user interface and deploy it to Sevalla so that anyone can upload text and download audio without touching code.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-understanding-the-core-idea">Understanding the Core Idea</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-your-project">How to Set Up Your Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-fetch-and-clean-blog-content">How to Fetch and Clean Blog Content</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-send-text-to-openai-for-audio">How to Send Text to OpenAI for Audio</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-a-fastapi-backend">How to Build a FastAPI Backend</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-add-a-simple-user-interface">How to Add a Simple User Interface</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-your-service-to-sevalla">How to Deploy Your Service to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-understanding-the-core-idea">Understanding the Core Idea</h2>
<p>A blog-to-audio service has only three important parts. The first part takes a blog link or text and cleans it. The second part sends the clean text to OpenAI’s text-to-speech model. The third part gives the final MP3 file back to the user.</p>
<p>OpenAI’s speech generation is simple to use. You send text, choose a voice, and get audio back. The quality is high and works well even for long posts. This means you do not need to worry about training models or tuning voices.</p>
<p>The only job left is to make the system easy to use. That is where FastAPI and a small HTML form help. They wrap your code into a web service so anyone can try it.</p>
<h2 id="heading-how-to-set-up-your-project">How to Set Up Your Project</h2>
<p>Create a folder for your project. Inside it, create a file called <code>main.py</code>. You will also need a basic HTML file later.</p>
<p>Install the libraries you need with pip:</p>
<pre><code class="lang-python">pip install fastapi uvicorn requests beautifulsoup4 python-multipart
</code></pre>
<p>FastAPI gives you a simple backend. Requests module helps download blog pages. <a target="_blank" href="https://pypi.org/project/beautifulsoup4/">BeautifulSoup</a> helps remove HTML tags and extract readable text. Python-multipart helps upload form data.</p>
<p>You must also install the OpenAI client:</p>
<pre><code class="lang-python">pip install openai
</code></pre>
<p>Make sure you have your OpenAI API key ready. Set it in your terminal before running the app:</p>
<pre><code class="lang-python">export OPENAI_API_KEY=<span class="hljs-string">"your-key"</span>
</code></pre>
<p>On Windows, you can do:</p>
<pre><code class="lang-python">setx OPENAI_API_KEY <span class="hljs-string">"your-key"</span>
</code></pre>
<h2 id="heading-how-to-fetch-and-clean-blog-content">How to Fetch and Clean Blog Content</h2>
<p>To convert a blog into audio, you must first extract the main article text. You can fetch the page with requests and parse it with BeautifulSoup. </p>
<p>Below is a simple function that does this. </p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> bs4 <span class="hljs-keyword">import</span> BeautifulSoup

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">extract_text_from_url</span>(<span class="hljs-params">url: str</span>) -&gt; str:</span>
    response = requests.get(url, timeout=<span class="hljs-number">10</span>)
    html = response.text
    soup = BeautifulSoup(html, <span class="hljs-string">"html.parser"</span>)
    paragraphs = soup.find_all(<span class="hljs-string">"p"</span>)
    text = <span class="hljs-string">" "</span>.join(p.get_text(strip=<span class="hljs-literal">True</span>) <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> paragraphs)
    <span class="hljs-keyword">return</span> text
</code></pre>
<p>Here is what happens step by step. </p>
<ul>
<li><p>The function downloads the page. </p>
</li>
<li><p>BeautifulSoup reads the HTML and finds all paragraph tags. </p>
</li>
<li><p>It pulls out the text in each paragraph and joins them into one long string. </p>
</li>
<li><p>This gives you a clean version of the blog post without ads or layout code.</p>
</li>
</ul>
<p>If the user pastes text instead of a URL, you can skip this part and use the text as it is.</p>
<h2 id="heading-how-to-send-text-to-openai-for-audio">How to Send Text to OpenAI for Audio</h2>
<p>OpenAI’s text-to-speech API makes this part of the work very easy. You send a message with text and select a voice such as Alloy or Verse. The API returns raw audio bytes. You can save these bytes as an MP3 file.</p>
<p>Here is a helper function to convert text into audio:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> openai <span class="hljs-keyword">import</span> OpenAI
client = OpenAI()

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">text_to_audio</span>(<span class="hljs-params">text: str, output_path: str</span>):</span>
    audio = client.audio.speech.create(
        model=<span class="hljs-string">"gpt-4o-mini-tts"</span>,
        voice=<span class="hljs-string">"alloy"</span>,
        input=text
    )
    <span class="hljs-keyword">with</span> open(output_path, <span class="hljs-string">"wb"</span>) <span class="hljs-keyword">as</span> f:
        f.write(audio.read())
</code></pre>
<p>This function calls the OpenAI client and passes the text, model name, and voice choice. The <code>.read()</code> method extracts the binary audio stream. Writing this to an MP3 file completes the process.</p>
<p>If the blog post is very long, you may want to limit text length or chunk the text and join the audio files later. But for most blogs, the model can handle the entire text in one request.</p>
<h2 id="heading-how-to-build-a-fastapi-backend">How to Build a FastAPI Backend</h2>
<p>Now you can wrap both steps into a simple FastAPI server. This server will accept either a URL or pasted text. It will convert the content into audio and return the MP3 file as a response.</p>
<p>Here is the full backend code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, Form
<span class="hljs-keyword">from</span> fastapi.responses <span class="hljs-keyword">import</span> FileResponse
<span class="hljs-keyword">import</span> uuid
<span class="hljs-keyword">import</span> os

app = FastAPI()
<span class="hljs-meta">@app.post("/convert")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">convert</span>(<span class="hljs-params">url: str = Form(<span class="hljs-params">None</span>), text: str = Form(<span class="hljs-params">None</span>)</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> url <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> text:
        <span class="hljs-keyword">return</span> {<span class="hljs-string">"error"</span>: <span class="hljs-string">"Please provide a URL or text"</span>}
    <span class="hljs-keyword">if</span> url:
        <span class="hljs-keyword">try</span>:
            text_content = extract_text_from_url(url)
        <span class="hljs-keyword">except</span> Exception:
            <span class="hljs-keyword">return</span> {<span class="hljs-string">"error"</span>: <span class="hljs-string">"Could not fetch the URL"</span>}
    <span class="hljs-keyword">else</span>:
        text_content = text
    file_id = uuid.uuid4().hex
    output_path = <span class="hljs-string">f"audio_<span class="hljs-subst">{file_id}</span>.mp3"</span>
    text_to_audio(text_content, output_path)
    <span class="hljs-keyword">return</span> FileResponse(output_path, media_type=<span class="hljs-string">"audio/mpeg"</span>)
</code></pre>
<p>Here is how it works. The user sends form data with either <code>url</code> or <code>text</code>. The server checks which one exists. </p>
<p>If there is a URL, it extracts text with the earlier function. If there is no URL, it uses the provided text directly. A unique file name is created for every request. Then the audio file is generated and returned as an MP3 download.</p>
<p>You can run the server like this:</p>
<pre><code class="lang-python">uvicorn main:app --reload
</code></pre>
<p>Open your browser at <code>http://localhost:8000</code>. You will not see the UI yet, but the API endpoint is working. You can test it using a tool like Postman or by building the front end next.</p>
<h2 id="heading-how-to-add-a-simple-user-interface">How to Add a Simple User Interface</h2>
<p>A service is much easier to use when it has a clean UI. Below is a simple HTML page that sends either a URL or text to your FastAPI backend. Save this file as <code>index.html</code> in the same folder:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Blog to Audio<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
        <span class="hljs-selector-tag">body</span> { <span class="hljs-attribute">font-family</span>: Arial, padding: <span class="hljs-number">40px</span>; <span class="hljs-attribute">max-width</span>: <span class="hljs-number">600px</span>; <span class="hljs-attribute">margin</span>: auto; }
        <span class="hljs-selector-tag">input</span>, <span class="hljs-selector-tag">textarea</span> { <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>; <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>; <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">10px</span>; }
        <span class="hljs-selector-tag">button</span> { <span class="hljs-attribute">padding</span>: <span class="hljs-number">12px</span> <span class="hljs-number">20px</span>; <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">20px</span>; <span class="hljs-attribute">cursor</span>: pointer; }
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Convert Blog to Audio<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/convert"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Blog URL<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Enter a blog link"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>or paste text below<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"10"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Paste blog text here"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Convert to Audio<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>This page gives the user two options. They can type a URL or paste text. The form sends the data to <code>/convert</code> using a POST request. The response will be the MP3 file, so the browser will download it.</p>
<p>To serve the HTML file, add this route to your <code>main.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi.responses <span class="hljs-keyword">import</span> HTMLResponse

<span class="hljs-meta">@app.get("/")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">home</span>():</span>
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">"index.html"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
        html = f.read()
    <span class="hljs-keyword">return</span> HTMLResponse(html)
</code></pre>
<p>Now, when you visit the main URL, you will see a clean form.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191346855/7ac2b182-7c19-408b-8af9-5b696bad8cec.png" alt="Blog to Audio UI" class="image--center mx-auto" width="1000" height="352" loading="lazy"></p>
<p>When you submit a URL, the server will process your request and give you an audio file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191378838/3fedbbba-0ae0-45a4-a0af-5565a78a0884.png" alt="Blog to Audio Result" class="image--center mx-auto" width="1000" height="409" loading="lazy"></p>
<p>Great. Our text to audio service is working. Now let’s get it into production.</p>
<h2 id="heading-how-to-deploy-your-service-to-sevalla">How to Deploy Your Service to Sevalla</h2>
<p>You can choose any cloud provider, like AWS, DigitalOcean, or others, to host your service. I will be using Sevalla for this example.</p>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a developer-friendly PaaS provider. It offers application hosting, database, object storage, and static site hosting for your projects.</p>
<p>Every platform will charge you for creating a cloud resource. Sevalla comes with a $50 credit for us to use, so we won’t incur any costs for this example.</p>
<p>Let’s push this project to GitHub so that we can connect our repository to Sevalla. We can also enable auto-deployments so that any new change to the repository is automatically deployed.</p>
<p>You can also <a target="_blank" href="https://github.com/manishmshiva/blog-to-audio">fork my repository</a> from here.</p>
<p><a target="_blank" href="https://app.sevalla.com/login">Log in</a> to Sevalla and click on Applications -&gt; Create new application. You can see the option to link your GitHub repository to create a new application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191422806/85b3398b-9be7-4956-be4e-05c72b5dd6ae.png" alt="Sevalla Create Application" class="image--center mx-auto" width="1000" height="620" loading="lazy"></p>
<p>Use the default settings. Click “Create application”. Now we have to add our OpenAI API key to the environment variables. Click on the “Environment variables” section once the application is created, and save the <code>OPENAI_API_KEY</code> value as an environment variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191454748/2c19f048-74e3-46d0-90e2-44128be19201.png" alt="Sevalla Environment Variables" class="image--center mx-auto" width="1000" height="293" loading="lazy"></p>
<p>Now we are ready to deploy our application. Click on “Deployments” and click “Deploy now”. It will take 2–3 minutes for the deployment to complete.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191493335/cb789b5e-ff51-4ffb-b398-3b1ccd6bc137.png" alt="Sevalla Deployment" class="image--center mx-auto" width="1000" height="520" loading="lazy"></p>
<p>Once done, click on “Visit app”. You will see the application served via a URL ending with <code>sevalla.app</code> . This is your new root URL. You can replace <code>localhost:8000</code> with this URL and start using it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768191518487/591394e4-de93-43bf-ac5a-6492e45f1e60.png" alt="Application UI" class="image--center mx-auto" width="902" height="586" loading="lazy"></p>
<p>Congrats! Your blog-to-audio service is now live. You can extend this by adding other capabilities and pushing your code to GitHub. Sevalla will automatically deploy your application to production.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now know how to build a full blog-to-audio service using OpenAI. You learned how to fetch blog text, convert it into speech, and serve it with FastAPI. You also learned how to create a simple user interface, allowing people to try it with no setup. </p>
<p>With this foundation, you can turn any written content into smooth, natural audio. This can help creators reach a wider audience, enhance accessibility, and provide users with more ways to enjoy content.</p>
<p><em>Hope you enjoyed this article. Signup for my free newsletter</em> <a target="_blank" href="https://www.turingtalks.ai/"><strong><em>TuringTalks.ai</em></strong></a> <em>for more hands-on tutorials on AI. You can also</em> <a target="_blank" href="https://manishshivanandhan.com/"><strong><em>visit my website</em></strong></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build and Deploy an AI Agent with LangChain, FastAPI, and Sevalla ]]>
                </title>
                <description>
                    <![CDATA[ Artificial intelligence is changing how we build software. Just a few years ago, writing code that could talk, decide, or use external data felt hard. Today, thanks to new tools, developers can build smart agents that read messages, reason about them... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-ai-agent-with-langchain-fastapi-and-sevalla/</link>
                <guid isPermaLink="false">6960413b864205dd1936a070</guid>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ langchain ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 08 Jan 2026 23:43:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767915474046/728b3bd5-2dfe-45a3-a2a9-c682e4719d7d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Artificial intelligence is changing how we build software. Just a few years ago, writing code that could talk, decide, or use external data felt hard.</p>
<p>Today, thanks to new tools, developers can build smart agents that read messages, reason about them, and call functions on their own.</p>
<p>One such platform that makes this easy is <a target="_blank" href="https://github.com/langchain-ai/langchain">LangChain</a>. With LangChain, you can link language models, tools, and apps together. You can also wrap your agent inside a FastAPI server, then push it to a cloud platform for deployment.</p>
<p>This article will walk you through building your first AI agent. You will learn what LangChain is, how to build an agent, how to serve it through FastAPI, and how to deploy it on Sevalla.</p>
<h2 id="heading-what-well-cover">What We’ll Cover</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-langchain">What is LangChain?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-your-first-agent-with-langchain">How to Build Your First Agent with LangChain</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wrapping-your-agent-with-fastapi">Wrapping Your Agent with FastAPI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-your-ai-agent-to-sevalla">How to Deploy Your AI Agent to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-langchain">What is LangChain?</h2>
<p>LangChain is a framework for working with large language models. It helps you build apps that think, reason, and act.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629343581/a7f55a7e-f9fa-4d34-9ce5-666adf9cb93d.jpeg" alt="Langchain" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>A model on its own only gives text replies, but LangChain lets it do more. It lets a model call functions, use tools, connect with databases, and follow workflows.</p>
<p>Think of LangChain as a bridge. On one side is the language model. On the other side are your tools, data sources, and business logic. LangChain tells the model what tools exist, when to use them, and how to reply. This makes it ideal for building agents that answer questions, automate tasks, or handle complex flows.</p>
<p>Many developers use LangChain because it is flexible. It supports many AI models. It fits well with Python.</p>
<p>Langchain also makes it easier to move from prototype to production. Once you learn how to create an agent, you can reuse the pattern for more advanced use cases.</p>
<p>I have recently published a detailed <a target="_blank" href="https://www.turingtalks.ai/p/langchain-tutorial">langchain tutorial</a> here.</p>
<h2 id="heading-how-to-build-your-first-agent-with-langchain">How to Build Your First Agent with LangChain</h2>
<p>Let’s make our first agent. It will respond to user questions and <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-your-first-mcp-server-using-fastmcp/">call a tool</a> when needed.</p>
<p>We’ll give it a simple weather tool, then ask it about the weather in a city. Before this, create a file called <code>.env</code> and add your OpenAI api key. Langchain will automatically use it when making requests to OpenAI.</p>
<pre><code class="lang-python">OPENAI_API_KEY=&lt;key&gt;
</code></pre>
<p>Here is the code for our agent:</p>
<pre><code class="lang-python">
<span class="hljs-keyword">from</span> langchain.agents <span class="hljs-keyword">import</span> create_agent
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-comment"># load environment variables</span>
load_dotenv()

<span class="hljs-comment"># defining the tool that LLM can call</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_weather</span>(<span class="hljs-params">city: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""Get weather for a given city."""</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">f"It's always sunny in <span class="hljs-subst">{city}</span>!"</span>

<span class="hljs-comment"># Creating an agent</span>
agent = create_agent(
    model=<span class="hljs-string">"gpt-4o"</span>,
    tools=[get_weather],
    system_prompt=<span class="hljs-string">"You are a helpful assistant"</span>,
)

result = agent.invoke({<span class="hljs-string">"messages"</span>:[{<span class="hljs-string">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-string">"content"</span>:<span class="hljs-string">"What is the weather in san francisco?"</span>}]})
</code></pre>
<p>This small program shows the power of LangChain agents.</p>
<p>First, we import <code>create_agent</code>, which helps us build the agent. Then we write a function called <code>get_weather</code>. It takes a city name and returns a friendly sentence.</p>
<p>The function acts as our tool. A tool is something the agent can use. In real projects, tools might fetch prices, store notes, or call APIs.</p>
<p>Next, we call <code>create_agent</code>. We give it three things. We pass the model we want to use. We list the tools we want it to call. And we give a system prompt. The system prompt tells the agent who it is and how it should behave.</p>
<p>Finally, we run the agent. We call <code>invoke</code> with a message.</p>
<p>The user asks for the weather in San Francisco. The agent reads this message. It sees that the question needs the weather function. So it calls our tool <code>get_weather</code>, passes the city, and returns an answer.</p>
<p>Even though this example is tiny, it captures the main idea. The agent reads natural language, figures out what tool to use, and sends a reply.</p>
<p>Later, you can add more tools or replace the weather function with one that connects to a real API. But this is enough for us to wrap and deploy.</p>
<h2 id="heading-wrapping-your-agent-with-fastapi">Wrapping Your Agent with FastAPI</h2>
<p>The next step is to serve our agent. <a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a> helps us expose our agent through an HTTP endpoint. That way, users and systems can call it through a URL, send messages, and get replies.</p>
<p>To begin, you install FastAPI and write a simple file like <code>main.py</code>. Inside it, you import FastAPI, load the agent, and write a route.</p>
<p>When someone posts a question, the API forwards it to the agent and returns the answer. The flow is simple.</p>
<p>The user talks to FastAPI. FastAPI talks to your agent. The agent thinks and replies. Here is the FAST API wrapper for your agent.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel
<span class="hljs-keyword">import</span> uvicorn
<span class="hljs-keyword">from</span> langchain.agents <span class="hljs-keyword">import</span> create_agent
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv
<span class="hljs-keyword">import</span> os

load_dotenv()

<span class="hljs-comment"># defining the tool that LLM can call</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_weather</span>(<span class="hljs-params">city: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""Get weather for a given city."""</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">f"It's always sunny in <span class="hljs-subst">{city}</span>!"</span>

<span class="hljs-comment"># Creating an agent</span>
agent = create_agent(
    model=<span class="hljs-string">"gpt-4o"</span>,
    tools=[get_weather],
    system_prompt=<span class="hljs-string">"You are a helpful assistant"</span>,
)

app = FastAPI()

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatRequest</span>(<span class="hljs-params">BaseModel</span>):</span>
    message: str

<span class="hljs-meta">@app.get("/")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">root</span>():</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"message"</span>: <span class="hljs-string">"Welcome to your first agent"</span>}

<span class="hljs-meta">@app.post("/chat")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">chat</span>(<span class="hljs-params">request: ChatRequest</span>):</span>
    result = agent.invoke({<span class="hljs-string">"messages"</span>:[{<span class="hljs-string">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-string">"content"</span>:request.message}]})
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"reply"</span>: result[<span class="hljs-string">"messages"</span>][<span class="hljs-number">-1</span>].content}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    port = int(os.getenv(<span class="hljs-string">"PORT"</span>, <span class="hljs-number">8000</span>))
    uvicorn.run(app, host=<span class="hljs-string">"0.0.0.0"</span>, port=port)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<p>Here, FastAPI defines a <code>/chat</code> endpoint. When someone sends a message, the server calls our agent. The agent processes it as before. Then FastAPI returns a clean JSON reply. The API layer hides the complexity inside a simple interface.</p>
<p>At this point, you have a working agent server. You can run it on your machine, call it with Postman or cURL, and check responses. When this works, you are ready to deploy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629386493/e5699447-d82e-4c73-87f8-87cec2d7dac2.png" alt="Postman Result" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-deploy-your-ai-agent-to-sevalla">How to Deploy Your AI Agent to Sevalla</h2>
<p>You can choose any cloud provider, like AWS, DigitalOcean, or others to host your agent. I will be using Sevalla for this example.</p>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a developer-friendly PaaS provider. It offers application hosting, database, object storage, and static site hosting for your projects.</p>
<p>Every platform will charge you for creating a cloud resource. Sevalla comes with a $50 credit for us to use, so we won’t incur any costs for this example.</p>
<p>Let’s push this project to GitHub so that we can connect our repository to Sevalla. We can also enable auto-deployments so that any new change to the repository is automatically deployed.</p>
<p>You can also <a target="_blank" href="https://github.com/manishmshiva/first-agent-with-fastapi">fork my repository</a> from here.</p>
<p><a target="_blank" href="https://app.sevalla.com/login">Log in</a> to Sevalla and click on Applications -&gt; Create new application. You can see the option to link your GitHub repository to create a new application</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629443568/85e00d7f-c296-4bed-94ba-8e2e5bbdb0ba.png" alt="Create application" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Use the default settings. Click “Create application”. Now we have to add our openai api key to the environment variables. Click on the “Environment variables” section once the application is created, and save the <code>OPENAI_API_KEY</code> value as an environment variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629507196/0ae254e2-00f6-46a1-8535-c3af006022c6.png" alt="Sevalla Environment Variables" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now we are ready to deploy our application. Click on “Deployments” and click “Deploy now”. It will take 2–3 minutes for the deployment to complete.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629546289/cbdc2f5d-4902-4799-aed4-2177695748bc.png" alt="Sevalla Deployment" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once done, click on “Visit app”. You will see the application served via a URL ending with <code>sevalla.app</code> . This is your new root URL. You can replace <code>localhost:8000</code> with this URL and test in Postman.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767629568646/e849222d-0cb5-433f-a399-0e8a63d891d1.png" alt="Postman Response" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Congrats! Your first AI agent with tool calling is now live. You can extend this by adding more tools and other capabilities, and pushing your code to GitHub, and Sevalla will automatically deploy your application to production.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building AI agents is no longer a task for experts. With LangChain, you can write a few lines and create reasoning tools that respond to users and call functions on their own.</p>
<p>By wrapping the agent with FastAPI, you give it a doorway that apps and users can access. Finally, Sevalla makes it easy to push your agent live, monitor it, and run it in production.</p>
<p>This journey from agent idea to deployed service shows what modern AI development looks like. You start small. You explore tools. You wrap them and deploy them.</p>
<p>Then you iterate, add more capability, improve logic, and plug in real tools. Before long, you have a smart, living agent online. That is the power of this new wave of technology.</p>
<p><em>Hope you enjoyed this article. Signup for my free newsletter</em> <a target="_blank" href="https://www.turingtalks.ai/"><strong><em>TuringTalks.ai</em></strong></a> <em>for more hands-on tutorials on AI. You can also</em> <a target="_blank" href="https://manishshivanandhan.com/"><strong><em>visit my website</em></strong></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Dependency Injection in FastAPI ]]>
                </title>
                <description>
                    <![CDATA[ Several languages and frameworks depend on dependency injection—no pun intended. Go, Angular, NestJS, and Python's FastAPI all use it as a core pattern. If you've been working with FastAPI, you've likely encountered dependencies in action. Perhaps yo... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-implement-dependency-injection-in-fastapi/</link>
                <guid isPermaLink="false">691740a91f4fa448325a55f9</guid>
                
                    <category>
                        <![CDATA[ dependency injection ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nneoma Uche ]]>
                </dc:creator>
                <pubDate>Fri, 14 Nov 2025 14:46:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763131442081/76eff35b-be68-49c1-9743-d78ebc87b292.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Several languages and frameworks depend on dependency injection—no pun intended. Go, Angular, NestJS, and Python's FastAPI all use it as a core pattern.</p>
<p>If you've been working with FastAPI, you've likely encountered dependencies in action. Perhaps you saw <code>Depends()</code> in a tutorial or the docs and were confused for a minute. I certainly was. That confusion sparked weeks of experimenting with this system. The truth is, you can't avoid dependency injection when building backend services with FastAPI. It's baked into the framework's DNA, powering everything from authentication and database connections to request validation.</p>
<p>FastAPI's docs describe its dependency injection system as 'powerful but intuitive.' That’s accurate, once you understand how it works. This article breaks it down, covering function dependencies, class dependencies, dependency scopes, as well as practical examples.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-dependencies-and-dependency-injection-in-fastapi">Dependencies and Dependency Injection in FastAPI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started-environment-setup">Getting Started: Environment Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-dependencies-in-fastapi">Types of Dependencies in FastAPI</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-use-function-dependencies-in-fastapi">How to Use Function Dependencies in FastAPI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-class-dependencies-in-fastapi">How to Use Class Dependencies in FastAPI</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-dependency-scope">Dependency Scope</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-path-operation-level">Path Operation Level</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-router-level">Router Level</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-application-level">Application Level</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-common-use-cases-for-dependency-injection">Common Use Cases for Dependency Injection</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this article, you should have:</p>
<ul>
<li><p>Working knowledge of Python.</p>
</li>
<li><p>Ability to create and activate virtual environments.</p>
</li>
<li><p>Basic understanding of FastAPI.</p>
</li>
<li><p>Familiarity with Object-Oriented Programming (OOP) concepts.</p>
</li>
</ul>
<h2 id="heading-dependencies-and-dependency-injection-in-fastapi">Dependencies and Dependency Injection in FastAPI</h2>
<p>A dependency is a reusable piece of logic, like authentication, database connection, or validation, that your path operations require. Dependency injection (DI) is how FastAPI delivers these dependencies to specific parts of your application: you declare them using <code>Depends()</code> and FastAPI automatically executes them when the associated route receives a request.</p>
<p>Think of it as requesting the tools your application needs. You declare dependencies once and FastAPI provides them wherever needed, with no repetitive setup across routes.</p>
<p>This makes for modular, scalable applications. Without DI, you would have to repeat the same setup code on every endpoint, making updates tedious and bugs more likely.</p>
<h2 id="heading-getting-started-environment-setup">Getting Started: Environment Setup</h2>
<p>Let's set up your development environment to work through the examples in this guide.</p>
<p>Start by creating a project folder, then:</p>
<p>Create and activate a virtual environment:</p>
<pre><code class="lang-bash">python -m venv deps
<span class="hljs-built_in">source</span> deps/bin/activate          <span class="hljs-comment">#on Mac</span>
deps\Scripts\activate             <span class="hljs-comment"># On Windows</span>
</code></pre>
<p>Install FastAPI with all dependencies:</p>
<pre><code class="lang-python">pip install <span class="hljs-string">'fastapi[all]'</span>
</code></pre>
<p>Organize your project as follows:</p>
<pre><code class="lang-python">fastapi-deps/
├── deps/                 <span class="hljs-comment"># Virtual environment</span>
├── function_deps.py
├── class_deps.py
├── router_deps.py
├── app.py
└── requirements.txt
</code></pre>
<h2 id="heading-types-of-dependencies-in-fastapi">Types of Dependencies in FastAPI</h2>
<p>In FastAPI, a dependency is a callable object that retrieves or verifies information before a route executes. Dependencies can be implemented as either functions or classes.</p>
<p><strong>Function dependencies</strong> are the most straightforward approach and work well for most use cases, including validation, authentication, and data retrieval. <strong>Class dependencies</strong> can handle the same tasks but are particularly useful when you need stateful logic, multiple instances with different configurations, or prefer object-oriented patterns.</p>
<h3 id="heading-how-to-use-function-dependencies-in-fastapi">How to Use Function Dependencies in FastAPI</h3>
<p>A function dependency is a helper function (such as for authentication or data retrieval) that can be injected into path operations. To demonstrate, we'll create a simple user authentication dependency using an in-memory database—a list of dictionaries.</p>
<p>Recall the folder structure from earlier? We’ll write this code in <code>fastapi-deps/function_deps.py</code>.</p>
<p>Start by importing the required modules:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, Depends, HTTPException
<span class="hljs-keyword">import</span> uvicorn
</code></pre>
<p>You bring in <code>FastAPI</code> to create the app instance, <code>Depends</code> for dependency injection, and <code>HTTPException</code> to handle errors gracefully. <code>uvicorn</code> will be used to run the application later.</p>
<p>Next, instantiate the FastAPI application:</p>
<pre><code class="lang-python">app = FastAPI()
</code></pre>
<p><code>app = FastAPI()</code> creates your application instance: the object that will hold all your endpoints and dependencies.</p>
<p>Next, create an in-memory database. Define a list of dictionaries to act as your temporary database. Each dictionary represents a user entry containing a name and a password.</p>
<pre><code class="lang-python">users = [
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Ore"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"jkzvdgwya12"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Uche"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"lga546"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Seke"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"SK99!"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Afi"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"Afi@144"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Sam"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"goTiger72*"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Ozi"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"xx%hI"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Ella"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"Opecluv18"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Claire"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"cBoss@14G"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Sena"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"SenDaBoss5"</span>},
    {<span class="hljs-string">"name"</span>: <span class="hljs-string">"Ify"</span>, <span class="hljs-string">"password"</span>: <span class="hljs-string">"184Norab"</span>}  
]
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This type of database isn’t persistent; any data stored therein is lost when the application restarts.</div>
</div>

<p>Then, define a dependency function for user validation. The simple helper function below checks whether a username and password provided by the user match an existing user in the database.</p>
<pre><code class="lang-python"><span class="hljs-comment">#the dependency function</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_dep</span>(<span class="hljs-params">name: str, password: str</span>):</span>
    <span class="hljs-keyword">for</span> u <span class="hljs-keyword">in</span> users:
        <span class="hljs-keyword">if</span> u[<span class="hljs-string">"name"</span>] == name <span class="hljs-keyword">and</span> u[<span class="hljs-string">"password"</span>] == password:
            <span class="hljs-keyword">return</span> {<span class="hljs-string">"name"</span>: name, <span class="hljs-string">"valid"</span>: <span class="hljs-literal">True</span>}
</code></pre>
<p>This function expects two string parameters, <code>name</code> and <code>password</code>, from the incoming request. If it finds a match in the <code>users</code> database, it returns a dictionary confirming the user’s validity. FastAPI automatically converts this dictionary into a JSON response.</p>
<p>Next, inject the dependency into a path function:</p>
<pre><code class="lang-python"><span class="hljs-comment">#the web endpoint</span>
<span class="hljs-meta">@app.get("/users/{user}")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_user</span>(<span class="hljs-params">user = Depends(<span class="hljs-params">user_dep</span>)</span>) -&gt; dict:</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> user:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">401</span>, detail=<span class="hljs-string">"Invalid username or password"</span>)
    <span class="hljs-keyword">return</span> user
</code></pre>
<p>The <code>user_dep</code> function is injected into the path operation using <code>Depends()</code>. When an HTTP request is made to this endpoint, FastAPI executes the dependency first, validates the input, and passes its return value to the <code>user</code> parameter.</p>
<p>The <code>-&gt; dict:</code> annotation indicates that the function returns a dictionary, which FastAPI auto-converts to JSON. If no matching record is found, an <code>HTTPException</code> with a 401 status code is raised; otherwise, the verified user data is returned.</p>
<p>Now you’ll start the FastAPI server. To start the server, open your terminal in the project directory and run:</p>
<pre><code class="lang-python">uvicorn function_deps:app --reload
</code></pre>
<ul>
<li><p><code>function_deps</code> is the name of your Python file (without the <strong>.py</strong> extension).</p>
</li>
<li><p><code>--reload</code> automatically restarts the server whenever you save changes.</p>
</li>
</ul>
<p>Once it starts, you’ll see an output similar to the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762651145390/479b187c-f455-4617-aa7f-e075bf668ee5.jpeg" alt="uvicorn output in terminal" class="image--center mx-auto" width="597" height="37" loading="lazy"></p>
<p>Now you can test the endpoint. Open your browser or the Postman desktop app to validate the user <strong>“Seke”</strong>. Paste this URL into your browser: <em>http://127.0.0.1:8000/users/{user}?name=Seke&amp;password=SK99!</em></p>
<p>Alternatively, you can test the endpoint using FastAPI’s built-in docs at: http://127.0.0.1:8000/docs</p>
<p>In the Swagger UI:</p>
<ul>
<li><p>Click on the <strong>Get User</strong> endpoint</p>
</li>
<li><p>Click <strong>Try it out</strong></p>
</li>
<li><p>Enter “Seke” in the name field and “SK99!” in the password field</p>
</li>
<li><p>Click <strong>Execute</strong></p>
</li>
</ul>
<p>You should get a 200 status code, with the payload in this image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762651845087/9495107e-1ab8-4349-a701-04e5de461fb6.jpeg" alt="payload for get_user endpoint" class="image--center mx-auto" width="402" height="246" loading="lazy"></p>
<p>You can also test the endpoint with usernames or passwords that don’t exist in the database. Each time, you should see a <strong>401</strong> error like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762652045213/c8dc8bb1-e2c4-456f-92f5-911dddae73eb.jpeg" alt="unauthorized error output in FastAPI docs" class="image--center mx-auto" width="417" height="197" loading="lazy"></p>
<h3 id="heading-how-to-use-class-dependencies-in-fastapi">How to Use Class Dependencies in FastAPI</h3>
<p>While functions are the most common way to define dependencies, FastAPI also supports class-based dependencies. Classes are useful when you need reusable instances with configurable state or prefer object-oriented patterns.</p>
<p>Class dependencies inject the same way: through the <code>Depends</code> function in your path operation.</p>
<p>Let's convert the <code>user_dep</code> function dependency to a class. It will authenticate users, grant access to valid credentials, and raise exceptions for unauthorized attempts. We'll apply it to a user dashboard endpoint to ensure only authenticated users access their resources.</p>
<pre><code class="lang-python"><span class="hljs-comment">#Dependency class for user authentication</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserAuth</span>():</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, name: str, password: str</span>):</span>
        self.name = name
        self.password = password

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__call__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment">#check if name and password entered correspond to any row in the db</span>
        <span class="hljs-keyword">for</span> user <span class="hljs-keyword">in</span> users:
            <span class="hljs-keyword">if</span> user[<span class="hljs-string">"name"</span>] == self.name <span class="hljs-keyword">and</span> user[<span class="hljs-string">"password"</span>] == self.password:
                <span class="hljs-keyword">pass</span>
        <span class="hljs-comment">#If no match found, raise an error</span>
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">401</span>, detail=<span class="hljs-string">"Invalid username or password"</span>)
</code></pre>
<p>The <code>__init</code>__ method receives the parameters from the request (<code>name</code> and <code>password</code>) and stores them as instance attributes. These can then be accessed in the <code>__call__</code> method, which contains the dependency logic.</p>
<p>Note that <code>__call__</code> doesn't return a value in this example. It simply raises an <code>HTTPException</code> if authentication fails. The <code>__call__</code> method makes the class instance callable, allowing FastAPI to invoke it like a regular function.</p>
<p>Here’s how to inject <code>UserAuth</code> into a path function:</p>
<pre><code class="lang-python"><span class="hljs-comment">#Injecting the class dependency into a path operation</span>
<span class="hljs-meta">@app.get("/user/dashboard")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_dashboard</span>(<span class="hljs-params">user: UserAuth = Depends(<span class="hljs-params">UserAuth</span>)</span>):</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"message"</span>: <span class="hljs-string">f"Access granted to <span class="hljs-subst">{user.name}</span>"</span>}
</code></pre>
<p><strong>What's happening here:</strong></p>
<p>When a client requests the <code>/user/dashboard</code> endpoint, FastAPI executes the dependency first. Recognizing <code>UserAuth</code> as a class, FastAPI automatically creates an instance and populates it with values from the query parameters.</p>
<p>Here’s the execution flow to help you understand:</p>
<ul>
<li><p><code>Depends(UserAuth)</code> tells FastAPI: “Before running this route, create a <code>UserAuth</code> instance.”</p>
</li>
<li><p>FastAPI extracts name and password from the request URL (for example, <em>/user/dashboard?name=Seke&amp;password=SK99!</em>).</p>
</li>
<li><p>It then calls <code>UserAuth(name=”Seke”, password=”SK99!”)</code> to create the instance.</p>
</li>
</ul>
<ul>
<li><p>The <code>UserAuth</code> instance, with its stored name and password attributes, is passed to the <code>user</code> parameter in <code>get_dashboard</code>.</p>
</li>
<li><p>The route function can access <code>user.name</code> and <code>user.password</code> directly.</p>
</li>
<li><p>If <code>__call__</code> raises an exception, the route never executes.</p>
</li>
</ul>
<p>Test the endpoint with valid credentials from the users list, and you should see output like this: </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762655384549/ac5ab413-0f75-4711-8166-4c99bcca9d7c.jpeg" alt="class dependency output" class="image--center mx-auto" width="557" height="132" loading="lazy"></p>
<p>A closer look at <a target="_blank" href="https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/#use-it">FastAPI’s official documentation</a> provides an alternative approach to classes as dependencies. However, using the <code>__call__</code> method, in my opinion, is the most straightforward and self-contained approach. It keeps your authentication logic modular without adding extra code to the path operation.</p>
<p>The trade-off is that class dependencies are more verbose than helper functions, but cleaner for complex logic.</p>
<h2 id="heading-dependency-scope">Dependency Scope</h2>
<p>FastAPI offers two ways to inject dependencies into a path operation: as a <strong>function parameter</strong> or via the <strong>path decorator</strong>. When you include a dependency as a function parameter, the dependency's return value is available within the function. But when injected into the decorator, the dependency executes without passing a return value to the path function.</p>
<p>Beyond single endpoints, FastAPI lets you inject dependencies at the router or global level. Let’s examine these scopes in more detail.</p>
<h3 id="heading-path-operation-level">Path Operation Level</h3>
<p>While the first example injected dependencies into path function parameters, you can also inject them directly into the decorator using the <code>dependencies</code> parameter. This approach is useful for side-effects (for example, authentication guards, rate limiting or request logging) where the return data is not required in the path operation.</p>
<p>Replace the previous code in <code>fastapi-deps/function_deps.py</code> with this:</p>
<pre><code class="lang-python"><span class="hljs-comment">#dep function to pass in decorator</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_dep</span>(<span class="hljs-params">name: str, password: str</span>):</span>
    <span class="hljs-keyword">for</span> u <span class="hljs-keyword">in</span> users:
        <span class="hljs-keyword">if</span> u[<span class="hljs-string">"name"</span>] == name <span class="hljs-keyword">and</span> u[<span class="hljs-string">"password"</span>] == password:
            <span class="hljs-keyword">return</span>
    <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">401</span>, detail=<span class="hljs-string">"Invalid username or password"</span>)

<span class="hljs-comment">#path function</span>
<span class="hljs-meta">@app.get("/users/{user}", dependencies=[Depends(user_dep)])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_user</span>() -&gt; dict:</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"message"</span> : <span class="hljs-string">"Access granted!"</span>}
</code></pre>
<p>This decorator-based dependency acts as a pre-check before the endpoint executes. It validates credentials without passing any values to the path function. On authentication failure, FastAPI raises an HTTPException and prevents the path operation from running.</p>
<p>If you test this using a valid name and password from the in-memory database, your output should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762656537394/06fc80cf-a8b2-44d2-8955-ec914be699ba.jpeg" alt="path decorator dependency output" class="image--center mx-auto" width="455" height="241" loading="lazy"></p>
<h3 id="heading-router-level">Router Level</h3>
<p>Injecting dependencies at the router level allows multiple endpoints to share common logic without repeating the dependency in each route.</p>
<p>We'll use the same <code>user_dep</code> function but inject it at the router level. Add these imports to <code>fastapi-deps/router_deps.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> APIRouter, Depends

<span class="hljs-comment">#import the dependency function</span>
<span class="hljs-keyword">from</span> function_deps <span class="hljs-keyword">import</span> user_dep
</code></pre>
<p>Then, create an <code>APIRouter</code> instance, passing your dependency to the <code>dependencies</code> parameter. This makes the dependency run automatically for every route you define under this router. </p>
<p>In this example, <code>user_dep</code> executes before <code>get_user()</code> and any other endpoints you add to the router, eliminating the need to declare it on each route.</p>
<pre><code class="lang-python">router = APIRouter(prefix=<span class="hljs-string">"/users"</span>, dependencies=[Depends(user_dep)])

<span class="hljs-comment">#define the routes with or without additional dependencies</span>
<span class="hljs-meta">@router.get("/{user}")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_user</span>() -&gt; dict:</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"message"</span> : <span class="hljs-string">"Access granted!"</span>}
</code></pre>
<p>In your main application file (<code>app.py</code>), import the router and register it with your FastAPI application using <code>include_router()</code>. This makes all routes defined in the router accessible through your application.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
<span class="hljs-keyword">import</span> uvicorn
<span class="hljs-keyword">from</span> router_deps <span class="hljs-keyword">import</span> router <span class="hljs-keyword">as</span> user_router

app = FastAPI()
app.include_router(user_router)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    uvicorn.run(<span class="hljs-string">"app:app"</span>, reload=<span class="hljs-literal">True</span>)
</code></pre>
<p>Start your server and test the route using a valid name–password pair from the users list, then try a mismatched one. You should get a <strong>200</strong> status for the correct credentials and <strong>401</strong> for invalid ones.</p>
<h3 id="heading-application-level">Application Level</h3>
<p>Application-level dependencies (also called <em>global dependencies</em>) are defined when instantiating the FastAPI app and apply to every route in your application. Unlike router-level dependencies that target specific endpoint groups, app-level dependencies extend across the entire application. Any dependency injected into the FastAPI app object will automatically execute for all path functions.</p>
<p>Let's inject a simple <em>logging</em> dependency alongside the <em>user authentication</em> dependency we've used throughout this article. </p>
<p>Update <code>fastapi-deps/app.py</code> with this code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, Depends
<span class="hljs-keyword">import</span> uvicorn
<span class="hljs-keyword">from</span> function_deps <span class="hljs-keyword">import</span> user_dep
<span class="hljs-keyword">from</span> router_deps <span class="hljs-keyword">import</span> router <span class="hljs-keyword">as</span> user_router
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-comment">#Basic logging dependency</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log_request</span>():</span>
    print(<span class="hljs-string">f"[<span class="hljs-subst">{datetime.now()}</span>] Request received."</span>)

app = FastAPI(dependencies=[Depends(log_request), Depends(user_dep)])
app.include_router(user_router)

<span class="hljs-meta">@app.get("/home")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_main</span>():</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Welcome back!!!"</span>


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    uvicorn.run(<span class="hljs-string">"app:app"</span>, reload=<span class="hljs-literal">True</span>)
</code></pre>
<p>When you send a request to any endpoint within this application, <code>log_request</code> acknowledges it and outputs what time the request was made. Since we aren’t sending the logs to any database in particular, it will just print to the terminal (or console) like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762673203094/d1c43e1b-0cc2-46e5-ae54-ee4849d1af66.jpeg" alt="logging dependency output in console" class="image--center mx-auto" width="529" height="53" loading="lazy"></p>
<p>Request the endpoint with valid credentials using your browser, cURL, Postman, or the Swagger UI. You should get this response:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762673465276/28d90221-4feb-4467-8c6c-8557dd54de03.jpeg" alt="Server response for API request to home page" class="image--center mx-auto" width="399" height="236" loading="lazy"></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Although the same authentication and logging logic apply to all registered routers, the specific message users see depends on what you program into each router.</div>
</div>

<h2 id="heading-common-use-cases-for-dependency-injection">Common Use Cases for Dependency Injection</h2>
<p>Dependency injection solves several common challenges in API development. Here are the most frequent use cases where you'll apply this pattern.</p>
<ol>
<li><p><strong>Database Connections:</strong> Reusing connection logic across multiple endpoints prevents connection leaks, and ensures each request has an isolated session.</p>
</li>
<li><p><strong>Authentication &amp; Authorization:</strong> Dependencies help validate tokens and verify user roles across protected routes. </p>
</li>
<li><p><strong>Logging &amp; Monitoring:</strong> A logging dependency can automatically record each request to your monitoring system or database. It is beneficial for debugging and tracking API usage.</p>
</li>
<li><p><strong>Rate Limiting:</strong> You can control request frequency and prevent API abuse by injecting rate-limiting logic in path functions.</p>
</li>
<li><p><strong>Configuration &amp; Settings:</strong> FastAPI’s dependency injection system simplifies configuration management by letting you inject settings such as API keys or environment variables wherever needed, keeping your code consistent.</p>
</li>
<li><p><strong>Pagination &amp; Filtering:</strong> Injecting common parameters like page_size and limit standardize data retrieval patterns across endpoints. </p>
</li>
</ol>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>FastAPI's dependency injection system helps you manage shared logic and resources efficiently while adhering to <em>DRY</em> principles. However, knowing when to inject a dependency versus when to skip it is a skill that comes with practice.</p>
<p>Dependency injection isn't needed for simple, standalone logic. But for resources requiring lifecycle management, shared logic, or modularity, FastAPI's dependency injection system simplifies checks and app operations—with or without return values.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy Your FastAPI + PostgreSQL App on Render: A Beginner's Guide ]]>
                </title>
                <description>
                    <![CDATA[ This guide is a comprehensive roadmap for deploying a FastAPI backend connected to a PostgreSQL database using Render, a cloud platform that supports hosting Python web apps and managed PostgreSQL databases.   You can find the complete source code he... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/deploy-fastapi-postgresql-app-on-render/</link>
                <guid isPermaLink="false">682f4900bcc94cb9bccbf905</guid>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ render.com ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Preston Osoro ]]>
                </dc:creator>
                <pubDate>Thu, 22 May 2025 15:55:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747923566699/58fc1283-d2f5-4964-acfa-b5dcad0f3d4f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This guide is a comprehensive roadmap for deploying a FastAPI backend connected to a PostgreSQL database using <a target="_blank" href="https://render.com/">Render</a>, a cloud platform that supports hosting Python web apps and managed PostgreSQL databases.  </p>
<p>You can find the complete source code <a target="_blank" href="https://github.com/preston-56/FastAPI">here</a>.</p>
<h2 id="heading-deployment-context">Deployment Context</h2>
<p>When deploying a FastAPI app connected to PostgreSQL, you need to select a platform that supports Python web applications and managed databases. This guide uses Render as the example platform because it provides both web hosting and a PostgreSQL database service in one environment, making it straightforward to connect your backend with the database.</p>
<p>You can apply the concepts here to other cloud providers as well, but the steps will differ depending on the platform’s specifics.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-project-structure">Project Structure for a Real-World FastAPI App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-youll-need-before-you-start">What You'll Need Before You Start</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deployment-steps">Deployment Steps</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-local-postgresql-database">Step 1: Set Up Local PostgreSQL Database</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-set-up-your-database-connection">Step 2: Set Up Your Database Connection</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-configure-your-fastapi-main-application">Step 3: Configure Your FastAPI Main Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-create-a-requirements-file">Step 4: Create a Requirements File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-provision-a-postgresql-database-on-render">Step 5: Provision a PostgreSQL Database on Render</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-deploy-your-fastapi-app-on-render">Step 6: Deploy Your FastAPI App on Render</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-test-your-api-endpoints">Step 7: Test Your API Endpoints</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-local-development-workflow">Local Development Workflow</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-and-tips">Best Practices and Common Troubleshooting Tips</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-issues-and-solutions">Common Issues and Solutions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-project-structure">Project Structure</h2>
<p>If you’re building a real-world API with <a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a> you’ll quickly outgrow a single <code>main.py</code> file. That’s when modular project structure becomes essential for maintainability.</p>
<p>Here’s an example structure we’ll use throughout this guide:</p>
<pre><code class="lang-python">FastAPI/
├── database/
│   ├── base.py
│   ├── database.py
│   └── __init__.py
├── fastapi_app/
│   └── main.py
├── items/
│   ├── models/
│   │   ├── __init__.py
│   │   └── item.py
│   ├── routes/
│   │   ├── __init__.py
│   │   └── item.py
│   └── schemas/
│       ├── __init__.py
│       └── item.py
├── models/
│   └── __init__.py
├── orders/
│   ├── models/
│   │   ├── __init__.py
│   │   └── order.py
│   ├── routes/
│   │   ├── __init__.py
│   │   └── order.py
│   └── schemas/
│       ├── __init__.py
│       └── order.py
└── users/
    ├── models/
    │   ├── __init__.py
    │   └── user.py
    ├── routes/
    │   ├── __init__.py
    │   └── user.py
    └── schemas/
        ├── __init__.py
        └── user.py
</code></pre>
<h2 id="heading-what-youll-need-before-you-start">What You'll Need Before You Start</h2>
<p>Before diving in, make sure you've got:</p>
<ul>
<li><p>A free <a target="_blank" href="https://render.com/">Render</a> account (sign up if you don't have one)</p>
</li>
<li><p>A GitHub or GitLab repository for your FastAPI project</p>
</li>
<li><p>Basic familiarity with Python, FastAPI, and Git</p>
</li>
<li><p>Your project structure set up similarly to the example above</p>
</li>
</ul>
<h2 id="heading-deployment-steps">Deployment Steps</h2>
<h3 id="heading-step-1-set-up-local-postgresql-database">Step 1: Set Up Local PostgreSQL Database</h3>
<p>For local development, you'll need to set up PostgreSQL on your machine like this:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- 1. Log in as superuser</span>
psql -U postgres

<span class="hljs-comment">-- 2. Create a new database</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> your_db;

<span class="hljs-comment">-- 3. Create a user with password</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> your_user <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">PASSWORD</span> <span class="hljs-string">'your_secure_password'</span>;

<span class="hljs-comment">-- 4. Grant all privileges on the database</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DATABASE</span> your_db <span class="hljs-keyword">TO</span> your_user;

<span class="hljs-comment">-- 5. (Optional) Allow the user to create tables</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">USER</span> your_user CREATEDB;

<span class="hljs-comment">-- 6. Exit</span>
\q
</code></pre>
<p>After setting up your local database, create a <code>.env</code> file in your project root:</p>
<pre><code class="lang-bash">DATABASE_URL=postgresql://your_user:your_secure_password@localhost:5432/your_db
</code></pre>
<h3 id="heading-step-2-set-up-your-database-connection">Step 2: Set Up Your Database Connection</h3>
<p>Create <code>database/database.py</code> to manage your PostgreSQL connection with SQLAlchemy:</p>
<p>This file is crucial as it creates the database engine, defines session management, and provides a dependency function for your routes.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sqlalchemy <span class="hljs-keyword">import</span> create_engine
<span class="hljs-keyword">from</span> sqlalchemy.orm <span class="hljs-keyword">import</span> sessionmaker
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

load_dotenv()

DATABASE_URL = os.getenv(<span class="hljs-string">"DATABASE_URL"</span>)
<span class="hljs-string">"""
The engine manages the connection to the database and handles query execution.
"""</span>
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=<span class="hljs-literal">False</span>, autoflush=<span class="hljs-literal">False</span>, bind=engine)

<span class="hljs-comment"># Database dependency for routes</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_db</span>():</span>
    db = SessionLocal()
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">yield</span> db
    <span class="hljs-keyword">finally</span>:
        db.close()
</code></pre>
<p>And add <code>database/base.py</code> for the base class:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sqlalchemy.ext.declarative <span class="hljs-keyword">import</span> declarative_base
Base = declarative_base()
</code></pre>
<h3 id="heading-step-3-configure-your-fastapi-main-application">Step 3: Configure Your FastAPI Main Application</h3>
<p>Create main FastAPI application file <code>fastapi_app/main.py</code> to import all your route modules:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, APIRouter
<span class="hljs-keyword">from</span> fastapi.openapi.utils <span class="hljs-keyword">import</span> get_openapi
<span class="hljs-keyword">from</span> fastapi.security <span class="hljs-keyword">import</span> OAuth2PasswordBearer
<span class="hljs-keyword">import</span> uvicorn
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-comment"># Load environment variables</span>
load_dotenv()

<span class="hljs-comment"># Database imports</span>
<span class="hljs-keyword">from</span> database <span class="hljs-keyword">import</span> Base, engine

<span class="hljs-comment"># Import models to ensure they're registered with SQLAlchemy</span>
<span class="hljs-keyword">import</span> models

<span class="hljs-comment"># Import router modules</span>
<span class="hljs-keyword">from</span> items.routes <span class="hljs-keyword">import</span> item_router
<span class="hljs-keyword">from</span> orders.routes <span class="hljs-keyword">import</span> order_router
<span class="hljs-keyword">from</span> users.routes <span class="hljs-keyword">import</span> user_router

<span class="hljs-comment"># Initialize FastAPI app</span>
app = FastAPI(
    title=<span class="hljs-string">"Store API"</span>,
    version=<span class="hljs-string">"1.0.0"</span>,
    description=<span class="hljs-string">"API documentation for Store API"</span>
)

<span class="hljs-comment"># Create database tables on startup</span>
Base.metadata.create_all(bind=engine)

<span class="hljs-comment"># Root endpoint</span>
<span class="hljs-meta">@app.get("/")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">root</span>():</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"message"</span>: <span class="hljs-string">"Welcome to FastAPI Store"</span>}

<span class="hljs-comment"># Setup versioned API router and include module routers</span>
api_router = APIRouter(prefix=<span class="hljs-string">"/v1"</span>)
api_router.include_router(item_router)
api_router.include_router(order_router)
api_router.include_router(user_router)

<span class="hljs-comment"># Register the master router with the app</span>
app.include_router(api_router)

<span class="hljs-comment"># Setup OAuth2 scheme for Swagger UI login flow</span>
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=<span class="hljs-string">"/v1/auth/login"</span>)

<span class="hljs-comment"># Custom OpenAPI schema with security configuration</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">custom_openapi</span>():</span>
    <span class="hljs-keyword">if</span> app.openapi_schema:
        <span class="hljs-keyword">return</span> app.openapi_schema

    openapi_schema = get_openapi(
        title=app.title,
        version=app.version,
        description=app.description,
        routes=app.routes,
    )

    <span class="hljs-comment"># Add security scheme</span>
    openapi_schema[<span class="hljs-string">"components"</span>][<span class="hljs-string">"securitySchemes"</span>] = {
        <span class="hljs-string">"BearerAuth"</span>: {
            <span class="hljs-string">"type"</span>: <span class="hljs-string">"http"</span>,
            <span class="hljs-string">"scheme"</span>: <span class="hljs-string">"bearer"</span>,
            <span class="hljs-string">"bearerFormat"</span>: <span class="hljs-string">"JWT"</span>,
        }
    }

    <span class="hljs-comment"># Apply global security requirement</span>
    openapi_schema[<span class="hljs-string">"security"</span>] = [{<span class="hljs-string">"BearerAuth"</span>: []}]

    app.openapi_schema = openapi_schema
    <span class="hljs-keyword">return</span> app.openapi_schema

app.openapi = custom_openapi

<span class="hljs-comment"># Run the app using Uvicorn when executed directly</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    port = os.environ.get(<span class="hljs-string">"PORT"</span>)
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> port:
        <span class="hljs-keyword">raise</span> EnvironmentError(<span class="hljs-string">"PORT environment variable is not set"</span>)
    uvicorn.run(<span class="hljs-string">"fastapi_app.main:app"</span>, host=<span class="hljs-string">"0.0.0.0"</span>, port=int(port), reload=<span class="hljs-literal">False</span>)
</code></pre>
<h3 id="heading-step-4-create-a-requirements-file">Step 4: Create a Requirements File</h3>
<p>In your project root, create a <code>requirements.txt</code> file that includes all the necessary dependencies:</p>
<pre><code class="lang-python">fastapi&gt;=<span class="hljs-number">0.68</span><span class="hljs-number">.0</span>
uvicorn&gt;=<span class="hljs-number">0.15</span><span class="hljs-number">.0</span>
sqlalchemy&gt;=<span class="hljs-number">1.4</span><span class="hljs-number">.23</span>
psycopg2-binary&gt;=<span class="hljs-number">2.9</span><span class="hljs-number">.1</span>
python-dotenv&gt;=<span class="hljs-number">0.19</span><span class="hljs-number">.0</span>
pydantic&gt;=<span class="hljs-number">1.8</span><span class="hljs-number">.2</span>
</code></pre>
<h3 id="heading-step-5-provision-a-postgresql-database-on-render">Step 5: Provision a PostgreSQL Database on Render</h3>
<p>Log in to your Render dashboard at <a target="_blank" href="https://dashboard.render.com/login">dashboard.render.com</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747782796468/e7564ed7-66cd-4466-a1d0-913b93dc9a77.png" alt="Render dashboard" class="image--center mx-auto" width="2564" height="1672" loading="lazy"></p>
<p>Then click "<strong>New +</strong>" in the top right and select "<strong>PostgreSQL</strong>".</p>
<p>Fill in the details:</p>
<ul>
<li><p>Name: <code>your-app-db</code> (choose a descriptive name)</p>
</li>
<li><p>Database: <code>your_app</code> (this will be your database name)</p>
</li>
<li><p>User: leave default (auto-generated)</p>
</li>
<li><p>Region: Choose the closest to your target users</p>
</li>
<li><p>Plan: Free tier</p>
</li>
</ul>
<p>Save and note the Internal Database URL shown after creation, which will look something like this:</p>
<pre><code class="lang-bash">postgres://user:password@postgres-instance.render.com/your_app
</code></pre>
<h3 id="heading-step-6-deploy-your-fastapi-app-on-render">Step 6: Deploy Your FastAPI App on Render</h3>
<p>With your database provisioned, it's time to deploy your API. You can do that by following these steps:</p>
<ol>
<li><p>In Render dashboard, click "<strong>New +</strong>" and select "<strong>Web Service</strong>"</p>
</li>
<li><p>Connect your GitHub/GitLab repository</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747813206325/5338209e-eb5c-4ba2-b28a-511296220935.png" alt="Connect to GitHub/GitLab" class="image--center mx-auto" width="1847" height="341" loading="lazy"></p>
</li>
<li><p>Name your service</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747813320278/e21998cc-317b-4ea6-8dec-d52493e2969f.png" alt="Naming your service" class="image--center mx-auto" width="1840" height="964" loading="lazy"></p>
</li>
<li><p><strong>Then configure the build settings</strong>:</p>
<ul>
<li><p>Environment: <code>Python 3</code></p>
</li>
<li><p>Build Command: <code>pip install -r requirements.txt</code></p>
</li>
<li><p>Start Command: <code>python3 -m fastapi_app.main</code></p>
</li>
</ul>
</li>
<li><p><strong>Add your environment variables</strong>:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747813450598/6b0913b0-3081-44c4-b746-6b28549a2dd0.png" alt="Adding environment variables" class="image--center mx-auto" width="1494" height="509" loading="lazy"></p>
<ul>
<li><p>Click "Environment" tab</p>
</li>
<li><p>Add your database URL:</p>
<ul>
<li><p>Key: <code>DATABASE_URL</code></p>
</li>
<li><p>Value: Paste the <strong>Internal Database URL</strong> from your PostgreSQL service</p>
</li>
</ul>
</li>
<li><p>Add any other environment variables your application needs</p>
</li>
</ul>
</li>
<li><p>Finally, click <strong>Deploy Web Service</strong>.</p>
<ul>
<li><p>Render will start building and deploying your application</p>
</li>
<li><p>This process takes a few minutes. You can monitor logs during build and deployment in real-time</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-step-7-test-your-api-endpoints">Step 7: Test Your API Endpoints</h3>
<p>Once deployed, access your API’s URL (for example, <a target="_blank" href="https://your-app-name.onrender.com"><code>https://your-app-name.onrender.com</code></a>).</p>
<p>Navigate to <code>/docs</code> to open the interactive Swagger UI, where you can test your endpoints directly:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747783210993/95ea29a5-d2aa-430f-a107-ef25c8ab4e24.png" alt="Test endpoints in Swagger" width="1511" height="790" loading="lazy"></p>
<ul>
<li><p>Expand an endpoint</p>
</li>
<li><p>Click <strong>Try it out</strong></p>
</li>
<li><p>Provide any required input</p>
</li>
<li><p>Click <strong>Execute</strong></p>
</li>
<li><p>View the response</p>
</li>
</ul>
<h2 id="heading-local-development-workflow">Local Development Workflow</h2>
<p>While your app is deployed, you'll still need to work on it locally. Here's how to maintain a smooth development workflow:</p>
<p>First, create a local <code>.env</code> file (don't commit this to Git):</p>
<pre><code class="lang-python">DATABASE_URL=postgresql://username:password@localhost:<span class="hljs-number">5432</span>/your_local_db
</code></pre>
<p>Then install your dependencies in a virtual environment:</p>
<pre><code class="lang-bash">python3 -m venv venv
<span class="hljs-built_in">source</span> venv/bin/activate  <span class="hljs-comment"># Windows: venv\Scripts\activate</span>
pip install -r requirements.txt
</code></pre>
<p>Next, run your local server:</p>
<pre><code class="lang-bash">python3 -m fastapi_app.main
</code></pre>
<p>This command triggers the <code>__main__</code> block in <code>fastapi_app/main.py</code>, which starts the FastAPI app using Uvicorn. It reads the <code>PORT</code> from your environment, so ensure it's set (e.g., via a <code>.env</code> file).</p>
<p>Then make changes to your code and test locally before pushing to GitHub/GitLab. You can push your changes to automatically trigger a new deployment on Render.</p>
<h2 id="heading-best-practices-and-tips">Best Practices and Tips</h2>
<ol>
<li><p><strong>Use database migrations</strong>: Add Alembic to your project for managing schema changes</p>
<pre><code class="lang-bash"> pip install alembic
 alembic init migrations
</code></pre>
</li>
<li><p><strong>Separate development and production configurations</strong>:</p>
<pre><code class="lang-python"> <span class="hljs-keyword">if</span> os.environ.get(<span class="hljs-string">"ENVIRONMENT"</span>) == <span class="hljs-string">"production"</span>:
     <span class="hljs-comment"># Production settings</span>
 <span class="hljs-keyword">else</span>:
     <span class="hljs-comment"># Development settings</span>
</code></pre>
</li>
<li><p><strong>Monitor your application</strong>:</p>
<ul>
<li>Render provides logs and metrics for your application. You can set up alerts for errors or high resource usage.</li>
</ul>
</li>
<li><p><strong>Optimize database queries</strong>:</p>
<ul>
<li><p>Use SQLAlchemy's relationship loading options.</p>
</li>
<li><p>Consider adding indexes to frequently queried fields.</p>
</li>
</ul>
</li>
<li><p><strong>Scale when needed</strong>:</p>
<ul>
<li>Render allows you to upgrade your plan as your application grows. Consider upgrading your database plan for production applications.</li>
</ul>
</li>
</ol>
<h2 id="heading-common-issues-and-solutions">Common Issues and Solutions</h2>
<p>When deploying a Python web app on Render, a few issues can commonly occur. Here's a more detailed look at them and how you can resolve each one.</p>
<h3 id="heading-database-connection-errors"><strong>Database connection errors</strong>:</h3>
<p>If your app can’t connect to the database, first double-check that your <code>DATABASE_URL</code> environment variable is correctly set in your Render dashboard. Make sure the URL includes the right username, password, host, port, and database name.</p>
<p>Also, confirm that your SQLAlchemy models match the actual schema in your database. A mismatch here can lead to errors during migrations or app startup. If you're using Postgres, ensure that the database user has permission to read/write tables and perform migrations.</p>
<h3 id="heading-deployment-fails-entirely"><strong>Deployment fails entirely:</strong></h3>
<p>When deployment fails, Render usually provides helpful logs under the “Events” tab. Check there for any error messages. A few common culprits include:</p>
<ul>
<li><p>A missing <code>requirements.txt</code> file or forgotten dependencies.</p>
</li>
<li><p>A bad <code>start</code> command in the Render settings. Double-check that it points to your correct entry point (for example, <code>gunicorn app:app</code> or <code>uvicorn main:app --host=0.0.0.0 --port=10000</code>).</p>
</li>
<li><p>Improper Python version. You can specify this in a <code>runtime.txt</code> file (for example, <code>python-3.11.1</code>).</p>
</li>
</ul>
<h3 id="heading-api-returns-500-internal-server-errors"><strong>API returns 500 Internal Server errors</strong>:</h3>
<p>Internal server errors can happen for several reasons. To debug:</p>
<ul>
<li><p>Open your Render logs and look for Python tracebacks or unhandled exceptions.</p>
</li>
<li><p>Try to reproduce the issue locally using the same request and data.</p>
</li>
<li><p>Add <code>try/except</code> blocks around critical logic to capture and log errors more gracefully.</p>
</li>
</ul>
<p>Even better, set up structured logging or error tracking (for example, with Sentry) to catch these before your users do.</p>
<h3 id="heading-slow-response-times"><strong>Slow response times</strong>:</h3>
<p>If your app is slow or intermittently timing out, check:</p>
<ul>
<li><p>Whether you're still on the free Render tier, which has limited CPU and memory. Consider upgrading if you’re handling production-level traffic.</p>
</li>
<li><p>If you're running heavy or unoptimized database queries, tools like SQLAlchemy’s <code>.explain()</code> or Django Debug Toolbar can help.</p>
</li>
<li><p>If you’re frequently fetching the same data, try caching it using a lightweight in-memory cache like <code>functools.lru_cache</code> or a Redis instance.</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Deploying a FastAPI app connected to PostgreSQL on Render is straightforward with the right structure and setup. While this guide used Render as an example, the concepts apply broadly across cloud platforms.</p>
<p>With this setup, you can develop, test, and deploy robust Python APIs backed by PostgreSQL databases efficiently.</p>
<p>The free tier on Render has some limitations, including PostgreSQL databases that expire after 90 days unless upgraded. For production applications, consider upgrading to a paid plan for better performance and reliability.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Use the FARM Stack to Develop Full Stack Apps ]]>
                </title>
                <description>
                    <![CDATA[ The FARM stack is a modern web development stack that combines three powerful technologies: FastAPI, React, and MongoDB. This full-stack solution provides developers with a robust set of tools to build scalable, efficient, and high-performance web ap... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/use-the-farm-stack-to-develop-full-stack-apps/</link>
                <guid isPermaLink="false">66eadbde43610a404a1d5ab9</guid>
                
                    <category>
                        <![CDATA[ MongoDB ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 18 Sep 2024 13:55:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726760870442/726967ee-7f01-432d-a73e-9f73f037f942.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The FARM stack is a modern web development stack that combines three powerful technologies: FastAPI, React, and MongoDB. This full-stack solution provides developers with a robust set of tools to build scalable, efficient, and high-performance web applications.</p>
<p>In this article, I'll be giving you an introduction to each of the key technologies, and then we'll build a project using the FARM stack and Docker so you can see how everything works together.</p>
<p>This article is based on a course I created <a target="_blank" href="https://www.youtube.com/watch?v=PWG7NlUDVaA">on the freeCodeCamp.org YouTube channel</a>. Watch it here:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/PWG7NlUDVaA" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h1 id="heading-introduction-to-the-farm-stack">Introduction to the FARM Stack</h1>
<p>The FARM in FARM stack stands for:</p>
<ul>
<li><p>F: FastAPI (Backend)</p>
</li>
<li><p>R: React (Frontend)</p>
</li>
<li><p>M: MongoDB (Database)</p>
</li>
</ul>
<p>The FARM stack is designed to leverage the strengths of each component, allowing developers to create feature-rich applications with a smooth development experience.</p>
<h3 id="heading-components-of-farm-stack">Components of FARM Stack</h3>
<ol>
<li><p><strong>FastAPI:</strong> FastAPI is a modern, high-performance Python web framework for building APIs. It's designed to be easy to use, fast to code, and ready for production environments. FastAPI is built on top of Starlette for the web parts and Pydantic for the data parts, making it a powerful choice for building robust backend services.</p>
</li>
<li><p><strong>React</strong>: React is a popular JavaScript library for building user interfaces. Developed and maintained by Facebook, React allows developers to create reusable UI components that efficiently update and render as data changes. Its component-based architecture and virtual DOM make it an excellent choice for building dynamic and responsive frontend applications.</p>
</li>
<li><p><strong>MongoDB:</strong> MongoDB is a document-oriented NoSQL database. It stores data in flexible, JSON-like documents, meaning fields can vary from document to document and data structure can be changed over time. This flexibility makes MongoDB an ideal choice for applications that need to evolve quickly and handle diverse data types.</p>
</li>
</ol>
<h3 id="heading-advantages-of-using-farm-stack">Advantages of using FARM Stack</h3>
<ol>
<li><p>High Performance: FastAPI is one of the fastest Python frameworks available, while React's virtual DOM ensures efficient UI updates. MongoDB's document model allows for quick reads and writes.</p>
</li>
<li><p>Scalability: All components of the FARM stack are designed to scale. FastAPI can handle concurrent requests efficiently, React applications can manage complex UIs, and MongoDB can distribute data across multiple servers.</p>
</li>
<li><p>Community and Ecosystem: All three technologies have large, active communities and rich ecosystems of libraries and tools.</p>
</li>
<li><p>Flexibility: The FARM stack is flexible enough to accommodate various types of web applications, from simple CRUD apps to complex, data-intensive systems.</p>
</li>
</ol>
<p>By combining these technologies, the FARM stack provides a comprehensive solution for building modern web applications. It allows developers to create fast, scalable backends with FastAPI, intuitive and responsive frontends with React, and flexible, efficient data storage with MongoDB. This stack is particularly well-suited for applications that require real-time updates, complex data models, and high performance.</p>
<h1 id="heading-project-overview-todo-application">Project Overview: Todo Application</h1>
<p>In the video course, I cover more about each individual technology in the FARM Stack. But in this article, we are going to jump right into a project to put everything together.</p>
<p>We will be creating a todo application to help us understand the FARM stack. Before we start creating the applicaiton, let’s discuss more about the features and software architecture.</p>
<h3 id="heading-features-of-the-todo-application">Features of the todo application</h3>
<p>Our FARM stack todo application will include the following features:</p>
<ol>
<li><p>Multiple Todo Lists:</p>
<ul>
<li><p>Users can create, view, update, and delete multiple todo lists.</p>
</li>
<li><p>Each list has a name and contains multiple todo items.</p>
</li>
</ul>
</li>
<li><p>Todo Items:</p>
<ul>
<li><p>Within each list, users can add, view, update, and delete todo items.</p>
</li>
<li><p>Each item has a label, a checked/unchecked status, and belongs to a specific list.</p>
</li>
</ul>
</li>
<li><p>Real-time Updates:</p>
<ul>
<li>The UI updates in real-time when changes are made to lists or items.</li>
</ul>
</li>
<li><p>Responsive Design:</p>
<ul>
<li>The application will be responsive and work well on both desktop and mobile devices.</li>
</ul>
</li>
</ol>
<h3 id="heading-system-architecture">System architecture</h3>
<p>Our todo application will follow a typical FARM stack architecture:</p>
<ol>
<li><p>Frontend (React):</p>
<ul>
<li><p>Provides the user interface for interacting with todo lists and items.</p>
</li>
<li><p>Communicates with the backend via RESTful API calls.</p>
</li>
</ul>
</li>
<li><p>Backend (FastAPI):</p>
<ul>
<li><p>Handles API requests from the frontend.</p>
</li>
<li><p>Implements business logic for managing todo lists and items.</p>
</li>
<li><p>Interacts with the MongoDB database for data persistence.</p>
</li>
</ul>
</li>
<li><p>Database (MongoDB):</p>
<ul>
<li><p>Stores todo lists and items.</p>
</li>
<li><p>Provides efficient querying and updating of todo data.</p>
</li>
</ul>
</li>
<li><p>Docker:</p>
<ul>
<li>Containerizes each component (frontend, backend, database) for easy development and deployment.</li>
</ul>
</li>
</ol>
<h3 id="heading-data-model-design">Data model design</h3>
<p>Our MongoDB data model will consist of two main structures:</p>
<ol>
<li>Todo List:</li>
</ol>
<pre><code class="lang-json">   {
     <span class="hljs-attr">"_id"</span>: ObjectId,
     <span class="hljs-attr">"name"</span>: String,
     <span class="hljs-attr">"items"</span>: [
       {
         <span class="hljs-attr">"id"</span>: String,
         <span class="hljs-attr">"label"</span>: String,
         <span class="hljs-attr">"checked"</span>: Boolean
       }
     ]
   }
</code></pre>
<ol start="2">
<li>List Summary (for displaying in the list of all todo lists):</li>
</ol>
<pre><code class="lang-json">   {
     <span class="hljs-attr">"_id"</span>: ObjectId,
     <span class="hljs-attr">"name"</span>: String,
     <span class="hljs-attr">"item_count"</span>: Integer
   }
</code></pre>
<h3 id="heading-api-endpoint-design">API endpoint design</h3>
<p>Our FastAPI backend will expose the following RESTful endpoints:</p>
<ol>
<li><p>Todo Lists:</p>
<ul>
<li><p>GET /api/lists: Retrieve all todo lists (summary view)</p>
</li>
<li><p>POST /api/lists: Create a new todo list</p>
</li>
<li><p>GET /api/lists/{list_id}: Retrieve a specific todo list with all its items</p>
</li>
<li><p>DELETE /api/lists/{list_id}: Delete a specific todo list</p>
</li>
</ul>
</li>
<li><p>Todo Items:</p>
<ul>
<li><p>POST /api/lists/{list_id}/items: Add a new item to a specific list</p>
</li>
<li><p>PATCH /api/lists/{list_id}/checked_state: Update the checked state of an item</p>
</li>
<li><p>DELETE /api/lists/{list_id}/items/{item_id}: Delete a specific item from a list</p>
</li>
</ul>
</li>
</ol>
<p>This project will provide a solid foundation in FARM stack development and Docker containerization, which you can then expand upon for more complex applications in the future.</p>
<p>So let's get started with the project.</p>
<h1 id="heading-project-tutorial">Project Tutorial</h1>
<h2 id="heading-project-setup-and-backend-development">Project Setup and Backend Development</h2>
<p>Step 1: Set up the project structure</p>
<p>Create a new directory for your project:</p>
<pre><code class="lang-csharp">   mkdir farm-stack-todo
   cd farm-stack-todo
</code></pre>
<p>Create subdirectories for the backend and frontend:</p>
<pre><code class="lang-csharp">   mkdir backend frontend
</code></pre>
<p>Step 2: Set up the backend environment</p>
<p>Navigate to the backend directory:</p>
<pre><code class="lang-csharp">   cd backend
</code></pre>
<p>Create a virtual environment and activate it:</p>
<pre><code class="lang-csharp">   python -m venv venv
   source venv/bin/activate  <span class="hljs-meta"># On Windows, use: venv\Scripts\activate</span>
</code></pre>
<p>Create the following files in the backend directory:</p>
<ol start="3">
<li><ul>
<li><p>Dockerfile</p>
<ul>
<li>pyproject.toml</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>    In your terminal, install the required packages:</p>
<pre><code class="lang-powershell">pip install <span class="hljs-string">"fastapi[all]"</span> <span class="hljs-string">"motor[srv]"</span> beanie aiostream
</code></pre>
<p>Generate the requirements.txt file:</p>
<pre><code class="lang-powershell">pip freeze &gt; requirements.txt
</code></pre>
<p>After creating the requirements.txt file (either through pip-compile or manually), you can install the dependencies using:</p>
<pre><code class="lang-csharp">   pip install -r requirements.txt
</code></pre>
<p>Add the following content to Dockerfile:</p>
<pre><code class="lang-csharp">   FROM python:<span class="hljs-number">3</span>

   WORKDIR /usr/src/app
   COPY requirements.txt ./

   RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

   EXPOSE <span class="hljs-number">3001</span>

   CMD [ <span class="hljs-string">"python"</span>, <span class="hljs-string">"./src/server.py"</span> ]
</code></pre>
<p>Add the following content to pyproject.toml:</p>
<pre><code class="lang-csharp">   [<span class="hljs-meta">tool.pytest.ini_options</span>]
   pythonpath = <span class="hljs-string">"src"</span>
</code></pre>
<p>Step 4: Set up the backend structure</p>
<p>Create a src directory inside the backend directory:</p>
<pre><code class="lang-csharp">   mkdir src
</code></pre>
<p>Create the following files inside the src directory:</p>
<ol start="2">
<li><ul>
<li><p><a target="_blank" href="http://server.py">server.py</a></p>
<ul>
<li><a target="_blank" href="http://dal.py">dal.py</a></li>
</ul>
</li>
</ul>
</li>
</ol>
<p>Step 5: Implement the Data Access Layer (DAL)</p>
<p>Open src/<a target="_blank" href="http://dal.py">dal.py</a> and add the following content:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> bson <span class="hljs-keyword">import</span> ObjectId
<span class="hljs-keyword">from</span> motor.motor_asyncio <span class="hljs-keyword">import</span> AsyncIOMotorCollection
<span class="hljs-keyword">from</span> pymongo <span class="hljs-keyword">import</span> ReturnDocument

<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel

<span class="hljs-keyword">from</span> uuid <span class="hljs-keyword">import</span> uuid4

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ListSummary</span>(<span class="hljs-params">BaseModel</span>):</span>
  id: str
  name: str
  item_count: int

<span class="hljs-meta">  @staticmethod</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">from_doc</span>(<span class="hljs-params">doc</span>) -&gt; "ListSummary":</span>
      <span class="hljs-keyword">return</span> ListSummary(
          id=str(doc[<span class="hljs-string">"_id"</span>]),
          name=doc[<span class="hljs-string">"name"</span>],
          item_count=doc[<span class="hljs-string">"item_count"</span>],
      )

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoListItem</span>(<span class="hljs-params">BaseModel</span>):</span>
  id: str
  label: str
  checked: bool

<span class="hljs-meta">  @staticmethod</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">from_doc</span>(<span class="hljs-params">item</span>) -&gt; "ToDoListItem":</span>
      <span class="hljs-keyword">return</span> ToDoListItem(
          id=item[<span class="hljs-string">"id"</span>],
          label=item[<span class="hljs-string">"label"</span>],
          checked=item[<span class="hljs-string">"checked"</span>],
      )

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoList</span>(<span class="hljs-params">BaseModel</span>):</span>
  id: str
  name: str
  items: list[ToDoListItem]

<span class="hljs-meta">  @staticmethod</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">from_doc</span>(<span class="hljs-params">doc</span>) -&gt; "ToDoList":</span>
      <span class="hljs-keyword">return</span> ToDoList(
          id=str(doc[<span class="hljs-string">"_id"</span>]),
          name=doc[<span class="hljs-string">"name"</span>],
          items=[ToDoListItem.from_doc(item) <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> doc[<span class="hljs-string">"items"</span>]],
      )

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoDAL</span>:</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, todo_collection: AsyncIOMotorCollection</span>):</span>
      self._todo_collection = todo_collection

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">list_todo_lists</span>(<span class="hljs-params">self, session=None</span>):</span>
      <span class="hljs-keyword">async</span> <span class="hljs-keyword">for</span> doc <span class="hljs-keyword">in</span> self._todo_collection.find(
          {},
          projection={
              <span class="hljs-string">"name"</span>: <span class="hljs-number">1</span>,
              <span class="hljs-string">"item_count"</span>: {<span class="hljs-string">"$size"</span>: <span class="hljs-string">"$items"</span>},
          },
          sort={<span class="hljs-string">"name"</span>: <span class="hljs-number">1</span>},
          session=session,
      ):
          <span class="hljs-keyword">yield</span> ListSummary.from_doc(doc)

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_todo_list</span>(<span class="hljs-params">self, name: str, session=None</span>) -&gt; str:</span>
      response = <span class="hljs-keyword">await</span> self._todo_collection.insert_one(
          {<span class="hljs-string">"name"</span>: name, <span class="hljs-string">"items"</span>: []},
          session=session,
      )
      <span class="hljs-keyword">return</span> str(response.inserted_id)

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_todo_list</span>(<span class="hljs-params">self, id: str | ObjectId, session=None</span>) -&gt; ToDoList:</span>
      doc = <span class="hljs-keyword">await</span> self._todo_collection.find_one(
          {<span class="hljs-string">"_id"</span>: ObjectId(id)},
          session=session,
      )
      <span class="hljs-keyword">return</span> ToDoList.from_doc(doc)

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_todo_list</span>(<span class="hljs-params">self, id: str | ObjectId, session=None</span>) -&gt; bool:</span>
      response = <span class="hljs-keyword">await</span> self._todo_collection.delete_one(
          {<span class="hljs-string">"_id"</span>: ObjectId(id)},
          session=session,
      )
      <span class="hljs-keyword">return</span> response.deleted_count == <span class="hljs-number">1</span>

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_item</span>(<span class="hljs-params">
      self,
      id: str | ObjectId,
      label: str,
      session=None,
  </span>) -&gt; ToDoList | <span class="hljs-keyword">None</span>:</span>
      result = <span class="hljs-keyword">await</span> self._todo_collection.find_one_and_update(
          {<span class="hljs-string">"_id"</span>: ObjectId(id)},
          {
              <span class="hljs-string">"$push"</span>: {
                  <span class="hljs-string">"items"</span>: {
                      <span class="hljs-string">"id"</span>: uuid4().hex,
                      <span class="hljs-string">"label"</span>: label,
                      <span class="hljs-string">"checked"</span>: <span class="hljs-literal">False</span>,
                  }
              }
          },
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      <span class="hljs-keyword">if</span> result:
          <span class="hljs-keyword">return</span> ToDoList.from_doc(result)

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">set_checked_state</span>(<span class="hljs-params">
      self,
      doc_id: str | ObjectId,
      item_id: str,
      checked_state: bool,
      session=None,
  </span>) -&gt; ToDoList | <span class="hljs-keyword">None</span>:</span>
      result = <span class="hljs-keyword">await</span> self._todo_collection.find_one_and_update(
          {<span class="hljs-string">"_id"</span>: ObjectId(doc_id), <span class="hljs-string">"items.id"</span>: item_id},
          {<span class="hljs-string">"$set"</span>: {<span class="hljs-string">"items.$.checked"</span>: checked_state}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      <span class="hljs-keyword">if</span> result:
          <span class="hljs-keyword">return</span> ToDoList.from_doc(result)

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_item</span>(<span class="hljs-params">
      self,
      doc_id: str | ObjectId,
      item_id: str,
      session=None,
  </span>) -&gt; ToDoList | <span class="hljs-keyword">None</span>:</span>
      result = <span class="hljs-keyword">await</span> self._todo_collection.find_one_and_update(
          {<span class="hljs-string">"_id"</span>: ObjectId(doc_id)},
          {<span class="hljs-string">"$pull"</span>: {<span class="hljs-string">"items"</span>: {<span class="hljs-string">"id"</span>: item_id}}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      <span class="hljs-keyword">if</span> result:
          <span class="hljs-keyword">return</span> ToDoList.from_doc(result)
</code></pre>
<p>This concludes Part 1 of the tutorial, where we set up the project structure and implemented the Data Access Layer for our FARM stack todo application. In the next part, we'll implement the FastAPI server and create the API endpoints.</p>
<h2 id="heading-implementing-the-fastapi-server">Implementing the FastAPI Server</h2>
<p>Step 6: Implement the FastAPI server</p>
<p>Open src/<a target="_blank" href="http://server.py">server.py</a> and add the following content:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> contextlib <span class="hljs-keyword">import</span> asynccontextmanager
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> sys

<span class="hljs-keyword">from</span> bson <span class="hljs-keyword">import</span> ObjectId
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, status
<span class="hljs-keyword">from</span> motor.motor_asyncio <span class="hljs-keyword">import</span> AsyncIOMotorClient
<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel
<span class="hljs-keyword">import</span> uvicorn

<span class="hljs-keyword">from</span> dal <span class="hljs-keyword">import</span> ToDoDAL, ListSummary, ToDoList

COLLECTION_NAME = <span class="hljs-string">"todo_lists"</span>
MONGODB_URI = os.environ[<span class="hljs-string">"MONGODB_URI"</span>]
DEBUG = os.environ.get(<span class="hljs-string">"DEBUG"</span>, <span class="hljs-string">""</span>).strip().lower() <span class="hljs-keyword">in</span> {<span class="hljs-string">"1"</span>, <span class="hljs-string">"true"</span>, <span class="hljs-string">"on"</span>, <span class="hljs-string">"yes"</span>}


<span class="hljs-meta">@asynccontextmanager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lifespan</span>(<span class="hljs-params">app: FastAPI</span>):</span>
    <span class="hljs-comment"># Startup:</span>
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    <span class="hljs-comment"># Ensure the database is available:</span>
    pong = <span class="hljs-keyword">await</span> database.command(<span class="hljs-string">"ping"</span>)
    <span class="hljs-keyword">if</span> int(pong[<span class="hljs-string">"ok"</span>]) != <span class="hljs-number">1</span>:
        <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">"Cluster connection is not okay!"</span>)

    todo_lists = database.get_collection(COLLECTION_NAME)
    app.todo_dal = ToDoDAL(todo_lists)

    <span class="hljs-comment"># Yield back to FastAPI Application:</span>
    <span class="hljs-keyword">yield</span>

    <span class="hljs-comment"># Shutdown:</span>
    client.close()


app = FastAPI(lifespan=lifespan, debug=DEBUG)


<span class="hljs-meta">@app.get("/api/lists")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_all_lists</span>() -&gt; list[ListSummary]:</span>
    <span class="hljs-keyword">return</span> [i <span class="hljs-keyword">async</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> app.todo_dal.list_todo_lists()]


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewList</span>(<span class="hljs-params">BaseModel</span>):</span>
    name: str


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewListResponse</span>(<span class="hljs-params">BaseModel</span>):</span>
    id: str
    name: str


<span class="hljs-meta">@app.post("/api/lists", status_code=status.HTTP_201_CREATED)</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_todo_list</span>(<span class="hljs-params">new_list: NewList</span>) -&gt; NewListResponse:</span>
    <span class="hljs-keyword">return</span> NewListResponse(
        id=<span class="hljs-keyword">await</span> app.todo_dal.create_todo_list(new_list.name),
        name=new_list.name,
    )


<span class="hljs-meta">@app.get("/api/lists/{list_id}")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_list</span>(<span class="hljs-params">list_id: str</span>) -&gt; ToDoList:</span>
    <span class="hljs-string">"""Get a single to-do list"""</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> app.todo_dal.get_todo_list(list_id)


<span class="hljs-meta">@app.delete("/api/lists/{list_id}")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_list</span>(<span class="hljs-params">list_id: str</span>) -&gt; bool:</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> app.todo_dal.delete_todo_list(list_id)


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewItem</span>(<span class="hljs-params">BaseModel</span>):</span>
    label: str


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewItemResponse</span>(<span class="hljs-params">BaseModel</span>):</span>
    id: str
    label: str


<span class="hljs-meta">@app.post(</span>
    <span class="hljs-string">"/api/lists/{list_id}/items/"</span>,
    status_code=status.HTTP_201_CREATED,
)
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_item</span>(<span class="hljs-params">list_id: str, new_item: NewItem</span>) -&gt; ToDoList:</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> app.todo_dal.create_item(list_id, new_item.label)


<span class="hljs-meta">@app.delete("/api/lists/{list_id}/items/{item_id}")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_item</span>(<span class="hljs-params">list_id: str, item_id: str</span>) -&gt; ToDoList:</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> app.todo_dal.delete_item(list_id, item_id)


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoItemUpdate</span>(<span class="hljs-params">BaseModel</span>):</span>
    item_id: str
    checked_state: bool


<span class="hljs-meta">@app.patch("/api/lists/{list_id}/checked_state")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">set_checked_state</span>(<span class="hljs-params">list_id: str, update: ToDoItemUpdate</span>) -&gt; ToDoList:</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> app.todo_dal.set_checked_state(
        list_id, update.item_id, update.checked_state
    )


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DummyResponse</span>(<span class="hljs-params">BaseModel</span>):</span>
    id: str
    when: datetime


<span class="hljs-meta">@app.get("/api/dummy")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_dummy</span>() -&gt; DummyResponse:</span>
    <span class="hljs-keyword">return</span> DummyResponse(
        id=str(ObjectId()),
        when=datetime.now(),
    )


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>(<span class="hljs-params">argv=sys.argv[<span class="hljs-number">1</span>:]</span>):</span>
    <span class="hljs-keyword">try</span>:
        uvicorn.run(<span class="hljs-string">"server:app"</span>, host=<span class="hljs-string">"0.0.0.0"</span>, port=<span class="hljs-number">3001</span>, reload=DEBUG)
    <span class="hljs-keyword">except</span> KeyboardInterrupt:
        <span class="hljs-keyword">pass</span>


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<p>This implementation sets up the FastAPI server with CORS middleware, connects to MongoDB, and defines the API endpoints for our todo application.</p>
<p>Step 7: Set up environment variables</p>
<p>Create a .env file in the root directory with the following content. Make sure to add the database name ("todo") at the end of ".mongodb.net/".</p>
<pre><code class="lang-csharp">MONGODB_URI=<span class="hljs-string">'mongodb+srv://beau:codecamp@cluster0.ji7hu.mongodb.net/todo?retryWrites=true&amp;w=majority&amp;appName=Cluster0'</span>
</code></pre>
<p>Step 8: Create a docker-compose file</p>
<p>In the root directory of your project (farm-stack-todo), create a file named compose.yml with the following content:</p>
<pre><code class="lang-csharp">name: todo-app
services:
  nginx:
    image: nginx:<span class="hljs-number">1.17</span>
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/<span class="hljs-keyword">default</span>.conf
    ports:
      - <span class="hljs-number">8000</span>:<span class="hljs-number">80</span>
    depends_on:
      - backend
      - frontend
  frontend:
    image: <span class="hljs-string">"node:22"</span>
    user: <span class="hljs-string">"node"</span>
    working_dir: /home/node/app
    environment:
      - NODE_ENV=development
      - WDS_SOCKET_PORT=<span class="hljs-number">0</span>
    volumes:
      - ./frontend/:/home/node/app
    expose:
      - <span class="hljs-string">"3000"</span>
    ports:
      - <span class="hljs-string">"3000:3000"</span>
    command: <span class="hljs-string">"npm start"</span>
  backend:
    image: todo-app/backend
    build: ./backend
    volumes:
      - ./backend/:/usr/src/app
    expose:
      - <span class="hljs-string">"3001"</span>
    ports:
      - <span class="hljs-string">"8001:3001"</span>
    command: <span class="hljs-string">"python src/server.py"</span>
    environment:
      - DEBUG=<span class="hljs-literal">true</span>
    env_file:
      - path: ./.env
        required: <span class="hljs-literal">true</span>
</code></pre>
<p>Step 9: Set up Nginx configuration</p>
<p>Create a directory named nginx in the root of your project:</p>
<pre><code class="lang-csharp">mkdir nginx
</code></pre>
<p>Create a file named nginx.conf inside the nginx directory with the following content:</p>
<pre><code class="lang-python">server {
    listen <span class="hljs-number">80</span>;
    server_name farm_intro;

    location / {
        proxy_pass http://frontend:<span class="hljs-number">3000</span>;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection <span class="hljs-string">"upgrade"</span>;
    }

    location /api {
        proxy_pass http://backend:<span class="hljs-number">3001</span>/api;
    }
}
</code></pre>
<p>This concludes Part 2 of the tutorial, where we implemented the FastAPI server, set up environment variables, created a docker-compose file, and configured Nginx. In the next part, we'll focus on setting up the React frontend for our FARM stack todo application.</p>
<h1 id="heading-setting-up-the-react-frontend">Setting up the React Frontend</h1>
<p>Step 10: Create the React application</p>
<p>Navigate to the frontend directory:</p>
<pre><code class="lang-python">cd ../frontend
</code></pre>
<p>Create a new React application using Create React App:</p>
<pre><code class="lang-python">npx create-react-app .
</code></pre>
<p>Install additional dependencies:</p>
<pre><code class="lang-python">   npm install axios react-icons
</code></pre>
<p>Step 11: Set up the main App component</p>
<p>Replace the content of src/App.js with the following:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./App.css"</span>;
<span class="hljs-keyword">import</span> ListToDoLists <span class="hljs-keyword">from</span> <span class="hljs-string">"./ListTodoLists"</span>;
<span class="hljs-keyword">import</span> ToDoList <span class="hljs-keyword">from</span> <span class="hljs-string">"./ToDoList"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [listSummaries, setListSummaries] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [selectedItem, setSelectedItem] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    reloadData().catch(<span class="hljs-built_in">console</span>.error);
  }, []);

  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">reloadData</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.get(<span class="hljs-string">"/api/lists"</span>);
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.data;
    setListSummaries(data);
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleNewToDoList</span>(<span class="hljs-params">newName</span>) </span>{
    <span class="hljs-keyword">const</span> updateData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> newListData = {
        <span class="hljs-attr">name</span>: newName,
      };

      <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">`/api/lists`</span>, newListData);
      reloadData().catch(<span class="hljs-built_in">console</span>.error);
    };
    updateData();
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleDeleteToDoList</span>(<span class="hljs-params">id</span>) </span>{
    <span class="hljs-keyword">const</span> updateData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">await</span> axios.delete(<span class="hljs-string">`/api/lists/<span class="hljs-subst">${id}</span>`</span>);
      reloadData().catch(<span class="hljs-built_in">console</span>.error);
    };
    updateData();
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleSelectList</span>(<span class="hljs-params">id</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Selecting item"</span>, id);
    setSelectedItem(id);
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">backToList</span>(<span class="hljs-params"></span>) </span>{
    setSelectedItem(<span class="hljs-literal">null</span>);
    reloadData().catch(<span class="hljs-built_in">console</span>.error);
  }

  <span class="hljs-keyword">if</span> (selectedItem === <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">ListToDoLists</span>
          <span class="hljs-attr">listSummaries</span>=<span class="hljs-string">{listSummaries}</span>
          <span class="hljs-attr">handleSelectList</span>=<span class="hljs-string">{handleSelectList}</span>
          <span class="hljs-attr">handleNewToDoList</span>=<span class="hljs-string">{handleNewToDoList}</span>
          <span class="hljs-attr">handleDeleteToDoList</span>=<span class="hljs-string">{handleDeleteToDoList}</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">ToDoList</span> <span class="hljs-attr">listId</span>=<span class="hljs-string">{selectedItem}</span> <span class="hljs-attr">handleBackButton</span>=<span class="hljs-string">{backToList}</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>Step 12: Create the ListTodoLists component</p>
<p>Create a new file src/ListTodoLists.js with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">"./ListTodoLists.css"</span>;
<span class="hljs-keyword">import</span> { useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { BiSolidTrash } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ListToDoLists</span>(<span class="hljs-params">{
  listSummaries,
  handleSelectList,
  handleNewToDoList,
  handleDeleteToDoList,
}</span>) </span>{
  <span class="hljs-keyword">const</span> labelRef = useRef();

  <span class="hljs-keyword">if</span> (listSummaries === <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ListToDoLists loading"</span>&gt;</span>Loading to-do lists ...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (listSummaries.length === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ListToDoLists"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"box"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
          New To-Do List:<span class="hljs-symbol">&amp;nbsp;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">{labelRef}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        &gt;
          New
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>There are no to-do lists!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
  }
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ListToDoLists"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>All To-Do Lists<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"box"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
          New To-Do List:<span class="hljs-symbol">&amp;nbsp;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">{labelRef}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        &gt;
          New
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      {listSummaries.map((summary) =&gt; {
        return (
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
            <span class="hljs-attr">key</span>=<span class="hljs-string">{summary.id}</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"summary"</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> handleSelectList(summary.id)}
          &gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"name"</span>&gt;</span>{summary.name} <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"count"</span>&gt;</span>({summary.item_count} items)<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"trash"</span>
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{(evt)</span> =&gt;</span> {
                evt.stopPropagation();
                handleDeleteToDoList(summary.id);
              }}
            &gt;
              <span class="hljs-tag">&lt;<span class="hljs-name">BiSolidTrash</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        );
      })}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ListToDoLists;
</code></pre>
<p>Create a new file src/ListTodoLists.css with the following content:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.ListToDoLists</span> <span class="hljs-selector-class">.summary</span> {
    <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid lightgray;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">cursor</span>: pointer;
    <span class="hljs-attribute">display</span>: flex;
}

<span class="hljs-selector-class">.ListToDoLists</span> <span class="hljs-selector-class">.count</span> {
    <span class="hljs-attribute">padding-left</span>: <span class="hljs-number">1ex</span>;
    <span class="hljs-attribute">color</span>: blueviolet;
    <span class="hljs-attribute">font-size</span>: <span class="hljs-number">92%</span>;
}
</code></pre>
<p>Step 13: Create the ToDoList component</p>
<p>Create a new file src/ToDoList.js with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">"./ToDoList.css"</span>;
<span class="hljs-keyword">import</span> { useEffect, useState, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;
<span class="hljs-keyword">import</span> { BiSolidTrash } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/bi"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ToDoList</span>(<span class="hljs-params">{ listId, handleBackButton }</span>) </span>{
  <span class="hljs-keyword">let</span> labelRef = useRef();
  <span class="hljs-keyword">const</span> [listData, setListData] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.get(<span class="hljs-string">`/api/lists/<span class="hljs-subst">${listId}</span>`</span>);
      <span class="hljs-keyword">const</span> newData = <span class="hljs-keyword">await</span> response.data;
      setListData(newData);
    };
    fetchData();
  }, [listId]);

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleCreateItem</span>(<span class="hljs-params">label</span>) </span>{
    <span class="hljs-keyword">const</span> updateData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">`/api/lists/<span class="hljs-subst">${listData.id}</span>/items/`</span>, {
        <span class="hljs-attr">label</span>: label,
      });
      setListData(<span class="hljs-keyword">await</span> response.data);
    };
    updateData();
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleDeleteItem</span>(<span class="hljs-params">id</span>) </span>{
    <span class="hljs-keyword">const</span> updateData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.delete(
        <span class="hljs-string">`/api/lists/<span class="hljs-subst">${listData.id}</span>/items/<span class="hljs-subst">${id}</span>`</span>
      );
      setListData(<span class="hljs-keyword">await</span> response.data);
    };
    updateData();
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleCheckToggle</span>(<span class="hljs-params">itemId, newState</span>) </span>{
    <span class="hljs-keyword">const</span> updateData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.patch(
        <span class="hljs-string">`/api/lists/<span class="hljs-subst">${listData.id}</span>/checked_state`</span>,
        {
          <span class="hljs-attr">item_id</span>: itemId,
          <span class="hljs-attr">checked_state</span>: newState,
        }
      );
      setListData(<span class="hljs-keyword">await</span> response.data);
    };
    updateData();
  }

  <span class="hljs-keyword">if</span> (listData === <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ToDoList loading"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"back"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleBackButton}</span>&gt;</span>
          Back
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        Loading to-do list ...
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
  }
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ToDoList"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"back"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleBackButton}</span>&gt;</span>
        Back
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>List: {listData.name}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"box"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
          New Item:<span class="hljs-symbol">&amp;nbsp;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">{labelRef}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span>
            handleCreateItem(document.getElementById(labelRef).value)
          }
        &gt;
          New
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      {listData.items.length &gt; 0 ? (
        listData.items.map((item) =&gt; {
          return (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{item.id}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">{item.checked</span> ? "<span class="hljs-attr">item</span> <span class="hljs-attr">checked</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">item</span>"}
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> handleCheckToggle(item.id, !item.checked)}
            &gt;
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{item.checked ? "✅" : "⬜️"} <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"label"</span>&gt;</span>{item.label} <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span>
                <span class="hljs-attr">className</span>=<span class="hljs-string">"trash"</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{(evt)</span> =&gt;</span> {
                  evt.stopPropagation();
                  handleDeleteItem(item.id);
                }}
              &gt;
                <span class="hljs-tag">&lt;<span class="hljs-name">BiSolidTrash</span> /&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          );
        })
      ) : (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"box"</span>&gt;</span>There are currently no items.<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      )}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ToDoList;
</code></pre>
<p>Create a new file src/ToDoList.css with the following content:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.ToDoList</span> <span class="hljs-selector-class">.back</span> {
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">float</span>: left;
}

<span class="hljs-selector-class">.ToDoList</span> <span class="hljs-selector-class">.item</span> {
    <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid lightgray;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">cursor</span>: pointer;
    <span class="hljs-attribute">display</span>: flex;
}

<span class="hljs-selector-class">.ToDoList</span> <span class="hljs-selector-class">.label</span> {
    <span class="hljs-attribute">margin-left</span>: <span class="hljs-number">1ex</span>;
}

<span class="hljs-selector-class">.ToDoList</span> <span class="hljs-selector-class">.checked</span> <span class="hljs-selector-class">.label</span> {
    <span class="hljs-attribute">text-decoration</span>: line-through;
    <span class="hljs-attribute">color</span>: lightgray;
}
</code></pre>
<p>Step 14: Update the main CSS file</p>
<p>Replace the content of src/index.css with the following:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">html</span>, <span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">font-family</span>: -apple-system, BlinkMacSystemFont, <span class="hljs-string">'Segoe UI'</span>, <span class="hljs-string">'Roboto'</span>, <span class="hljs-string">'Oxygen'</span>,
    <span class="hljs-string">'Ubuntu'</span>, <span class="hljs-string">'Cantarell'</span>, <span class="hljs-string">'Fira Sans'</span>, <span class="hljs-string">'Droid Sans'</span>, <span class="hljs-string">'Helvetica Neue'</span>,
    sans-serif;
  <span class="hljs-attribute">-webkit-font-smoothing</span>: antialiased;
  <span class="hljs-attribute">-moz-osx-font-smoothing</span>: grayscale;
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">12pt</span>;
}

<span class="hljs-selector-tag">input</span>, <span class="hljs-selector-tag">button</span> {
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1em</span>;
}

<span class="hljs-selector-tag">code</span> {
  <span class="hljs-attribute">font-family</span>: source-code-pro, Menlo, Monaco, Consolas, <span class="hljs-string">'Courier New'</span>,
    monospace;
}

<span class="hljs-selector-class">.box</span> {
    <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid lightgray;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">1em</span>;
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">1em</span>;
}

<span class="hljs-selector-class">.flex</span> {
  <span class="hljs-attribute">flex</span>: <span class="hljs-number">1</span>;
}
</code></pre>
<p>This concludes Part 3 of the tutorial, where we set up the React frontend for our FARM stack todo application. We've created the main App component, the ListTodoLists component for displaying all todo lists, and the ToDoList component for individual todo lists. In the next part, we'll focus on running and testing the application.</p>
<h1 id="heading-running-and-testing-the-application">Running and Testing the Application</h1>
<p>Step 18: Run the application using Docker Compose</p>
<ol>
<li><p>Make sure you have Docker and Docker Compose installed on your system</p>
</li>
<li><p>Open a terminal in the root directory of your project (farm-stack-todo)</p>
</li>
<li><p>Build and start the containers:</p>
</li>
</ol>
<pre><code class="lang-python">docker-compose up --build
</code></pre>
<ol start="4">
<li>Once the containers are up and running, open your web browser and go to <a target="_blank" href="http://localhost:8000/">http://localhost:8000</a></li>
</ol>
<p>Step 19: Stopping the application</p>
<ol>
<li><p>If you're running the application without Docker:</p>
<ul>
<li><p>Stop the React development server by pressing Ctrl+C in its terminal</p>
</li>
<li><p>Stop the FastAPI server by pressing Ctrl+C in its terminal</p>
</li>
<li><p>Stop the MongoDB server by pressing Ctrl+C in its terminal</p>
</li>
</ul>
</li>
<li><p>If you're running the application with Docker Compose:</p>
<ul>
<li><p>Press Ctrl+C in the terminal where you ran docker-compose up</p>
</li>
<li><p>Run the following command to stop and remove the containers:</p>
</li>
</ul>
</li>
</ol>
<pre><code class="lang-python">     docker-compose down
</code></pre>
<p>```</p>
<p>Congratulations! You have successfully built and tested a FARM stack todo application. This application demonstrates the integration of FastAPI, React, and MongoDB in a full-stack web application.</p>
<p>Here are some potential next steps to enhance your application:</p>
<ol>
<li><p>Add user authentication and authorization</p>
</li>
<li><p>Implement data validation and error handling</p>
</li>
<li><p>Add more features like due dates, priorities, or categories for todo items</p>
</li>
<li><p>Improve the UI/UX with a more polished design</p>
</li>
<li><p>Write unit and integration tests for both frontend and backend</p>
</li>
<li><p>Set up continuous integration and deployment (CI/CD) for your application</p>
</li>
</ol>
<p>Remember to keep your dependencies updated and follow best practices for security and performance as you continue to develop your application.</p>
<h1 id="heading-conclusion-and-next-steps">Conclusion and Next Steps</h1>
<p>Congratulations on completing this comprehensive FARM stack tutorial! By building this todo application, you've gained hands-on experience with some of the most powerful and popular technologies in modern web development. You've learned how to create a robust backend API with FastAPI, build a dynamic and responsive frontend with React, persist data with MongoDB, and containerize your entire application using Docker. This project has demonstrated how these technologies work together seamlessly to create a full-featured, scalable web application.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ FastAPI Handbook – How to Develop, Test, and Deploy APIs ]]>
                </title>
                <description>
                    <![CDATA[ Welcome to the world of FastAPI, a sleek and high-performance web framework for constructing Python APIs. Don't worry if you're new to API programming – we'll start at the beginning. An API (Application Programming Interface) connects several softwar... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/fastapi-quickstart/</link>
                <guid isPermaLink="false">66d45d9aa44b8bb91150f653</guid>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atharva Shah ]]>
                </dc:creator>
                <pubDate>Tue, 25 Jul 2023 20:54:10 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/FastAPI-Handbook-Cover.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Welcome to the world of FastAPI, a sleek and high-performance web framework for constructing Python APIs. Don't worry if you're new to API programming – we'll start at the beginning.</p>
<p>An <strong>API</strong> (Application Programming Interface) connects several software programs allowing them to converse and exchange information. APIs are essential in modern software development as they are an application's backend architecture.</p>
<p>After reading this quick start guide, you will be able to develop a course administration API using <a target="_blank" href="https://fastapi.tiangolo.com/"><strong>FastAPI</strong></a> and <a target="_blank" href="https://www.mongodb.com/"><strong>MongoDB</strong></a>. The best part is that you will not only be writing APIs but also testing and containerizing the app.</p>
<p>In this walkthrough project, we'll create a Python backend system using FastAPI, a fast web framework, and a MongoDB database for course information storage and retrieval.</p>
<p>The system will allow users to access course details, view chapters, rate individual chapters, and aggregate ratings.</p>
<p>The project is designed for Python developers with basic programming knowledge and some NoSQL knowledge. Familiarity with MongoDB, Docker, and PyTest is not required since I will be highlighting everything you need to know for the scope of this project.</p>
<h2 id="heading-what-well-build">What We'll Build</h2>
<p>Here's what we are going to be building:</p>
<p><strong>FastAPI Backend:</strong> It will serve as the interface for handling API requests and responses. FastAPI is chosen for its ease of use, performance, and intuitive design.</p>
<p><strong>MongoDB Database:</strong> A NoSQL database to store course information. MongoDB's flexible schema allows us to store data in JSON-like documents, making it suitable for this project.</p>
<p><strong>Course Information:</strong> Users will be able to view various course details, such as course name, description, instructor, etc.</p>
<p><strong>Chapter Details:</strong> The system will provide information about the chapters in a course, including chapter names, descriptions, and any other relevant data.</p>
<p><strong>Chapter Rating:</strong> Users will have the ability to rate individual chapters. We will implement functionality to record and retrieve chapter ratings.</p>
<p><strong>Course Aggregated Rating:</strong> The system will calculate and display the aggregated rating for each course based on the ratings of its chapters.</p>
<p>This walkthrough shows how to set up a development environment, build a FastAPI backend, integrate MongoDB, define API endpoints, add chapter rating functionality, and compute aggregate course ratings. It covers fundamental project concepts as well as Python, MongoDB, and NoSQL databases.</p>
<p>By the end, this useful backend system will manage chapter details, course information, and user ratings, serving as the basis for a complex and rewarding project.</p>
<p>The goal is to create a system that processes course-related queries. The course information must then be retrieved from MongoDB depending on the request. Lastly, this answer data must be returned in a standard format (JSON).</p>
<p>We'll begin with a script that reads the course information from courses.json. This data will be stored in the MongoDB instance. Once the data has been loaded, our API code may connect to this database to allow for simple data retrieval.</p>
<p>The interesting aspect is creating several endpoints with FastAPI. Our API will be able to:</p>
<ul>
<li><p>Fetch a list of all courses</p>
</li>
<li><p>Show a comprehensive course overview</p>
</li>
<li><p>List detailed information about certain chapters</p>
</li>
<li><p>Record user scores for each chapter.</p>
</li>
</ul>
<p>Additionally, for each course, we will aggregate all reviews, providing visitors with relevant information regarding course popularity and quality.</p>
<p>This tutorial focuses on building a scalable, efficient, and user-friendly API. Once we've tested everything, we'll containerize the application using Docker. This will greatly simplify deployment, maintenance, and installation.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<p>Here are the sections of this tutorial:</p>
<ul>
<li><p><a class="post-section-overview" href="#heading-api-methods">API Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-client-and-server">Client and Server</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-the-mongodb-database">How to Set Up the MongoDB Database</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-parse-and-insert-course-data-into-mongodb">How to Parse and Insert Course Data into MongoDB</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-design-the-fastapi-endpoints">How to Design the FastAPI Endpoints</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-automated-api-endpoint-testing-with-pytest">Automated API Endpoint Testing with PyTest</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-containerize-the-application-with-docker">How to Containerize the Application with Docker</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-api-methods">API Methods</h2>
<p>HTTP (Hypertext Transfer Protocol) methods specify the action to be taken on a resource. The following are the most often used API development methods:</p>
<p><strong>GET</strong>: Requests information from a server. When a client submits a GET request, it is requesting data from the server.</p>
<p><strong>POST</strong>: Sends data to the server for processing. When a client submits a POST request, it is often delivering data to the server to create or update a resource.</p>
<p><strong>PUT</strong>: Updates server data. When a client submits a PUT request, the resource indicated in the request is updated.</p>
<p><strong>DELETE</strong>: A client sending a DELETE request is asking for the removal of the specified resource.</p>
<h2 id="heading-client-and-server">Client and Server</h2>
<p>The <strong>client</strong> is often a front-end application that sends requests to the server, such as a web browser or a mobile app. The <strong>server</strong>, on the other hand, is the back-end application in charge of processing client requests and responding appropriately.</p>
<p>A request is a communication delivered by the client to the server that specifies the intended action and any required data. The HTTP method, URL (Uniform Resource Locator), headers, and, in the case of POST or PUT requests, the data payload are all part of a request.</p>
<p>After the server gets the <strong>request</strong>, it processes it and returns a <strong>response</strong>. The response is the message given back to the client by the server that contains the requested data or the outcome of the activity.</p>
<p>A response generally comprises an HTTP status code indicating the success or failure of the request, as well as any data sent back to the client by the server.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-131.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Diagram showing how APIs work</em></p>
<h2 id="heading-how-to-set-up-the-mongodb-database">How to Set Up the MongoDB Database</h2>
<p>MongoDB is a type of NoSQL database. It is non-relational and saves information as collections and documents.</p>
<p>Install MongoDB for your operating system from the <a target="_blank" href="https://www.mongodb.com/try/download/community">official website.</a></p>
<p>Now run the <code>mongosh</code> command for your terminal to verify if the installation was successful.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-125.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Running the mongosh command should yield this output</em></p>
<p>Connect to the MongoDB server with <strong>MongoDB Compass</strong>. I recommend that you set up MongoDB by specifying settings such as port number, storage engine, authentication, and so forth.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-124.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Create a new MongoDB connection</em></p>
<p>Now that the connection is established, the next step is to create a database or a "document". Call this database "courses". It will be empty for you currently. In just a minute we'll insert the documents using a Python script.</p>
<h2 id="heading-how-to-parse-and-insert-course-data-into-mongodb">How to Parse and Insert Course Data into MongoDB</h2>
<p>You could insert records one by one, but it is best to use a JSON file to simplify that process. Download this file <a target="_blank" href="https://github.com/HighnessAtharva/fastapi-kimo/blob/master/courses.json"><strong>courses.json</strong></a> from GitHub. All course information is present in it (as a list of courses).</p>
<p>Specifically, each course has the following structure:</p>
<ul>
<li><p><strong>name:</strong> The title of the course.</p>
</li>
<li><p><strong>date:</strong> Creation date as a UNIX timestamp.</p>
</li>
<li><p><strong>description:</strong> The description of the course.</p>
</li>
<li><p><strong>domain:</strong> List of the course domain(s).</p>
</li>
<li><p><strong>chapters:</strong> List of the course chapters. Each chapter has a title name and content text.</p>
</li>
</ul>
<p>You will need a few Python packages for this project.</p>
<ul>
<li><p><code>BSON</code> - Binary serialization format that is used in MongoDB for efficient data storage and retrieval. It comes bundled with PyMongo.</p>
</li>
<li><p><code>FastAPI</code> - Web framework for creating Python APIs that offer high performance, automatic validation, interactive documentation, and support for async operations.</p>
</li>
<li><p><code>PyMongo</code> - Official MongoDB driver for Python. It serves as a high-level API for integrating MongoDB within Python.</p>
</li>
<li><p><code>Uvicorn</code> - Primary ASGI server that improves application performance. It is responsible for server startup.</p>
</li>
<li><p><code>Starlette</code> - ASGI framework that powers FastAPI and allows rapid prototyping development.</p>
</li>
<li><p><code>Pydantic</code> - Integrated data validation and parsing library. We need it to create interactive API documentation while automatically validating incoming request data and enforcing data type rules.</p>
</li>
</ul>
<p>Get them installed via the pip commands like so:</p>
<pre><code class="lang-javascript">pip install fastapi pymongo uvicorn starlette pydantic
</code></pre>
<p>Now, let's write a Python script to insert all this course data into the database so that we can start building API routes. Spin up your IDE, create a file called <code>script.py</code>, and make sure it is in the same directory as the <code>courses.json</code> file.</p>
<pre><code class="lang-py"><span class="hljs-string">""" 
Script to parse course information from courses.json, create the appropriate databases and
collection(s) on a local instance of MongoDB, create the appropriate indices (for efficient retrieval)
and finally add the course data on the collection(s).
"""</span>

<span class="hljs-keyword">import</span> pymongo
<span class="hljs-keyword">import</span> json

<span class="hljs-comment"># Connect to MongoDB</span>
client = pymongo.MongoClient(<span class="hljs-string">"mongodb://localhost:27017/"</span>)
db = client[<span class="hljs-string">"courses"</span>]
collection = db[<span class="hljs-string">"courses"</span>]

<span class="hljs-comment"># Read courses from courses.json</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"courses.json"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
    courses = json.load(f)

<span class="hljs-comment"># Create index for efficient retrieval</span>
collection.create_index(<span class="hljs-string">"name"</span>)

<span class="hljs-comment"># add rating field to each course</span>
<span class="hljs-keyword">for</span> course <span class="hljs-keyword">in</span> courses:
    course[<span class="hljs-string">'rating'</span>] = {<span class="hljs-string">'total'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>}

<span class="hljs-comment"># add rating field to each chapter</span>
<span class="hljs-keyword">for</span> course <span class="hljs-keyword">in</span> courses:
    <span class="hljs-keyword">for</span> chapter <span class="hljs-keyword">in</span> course[<span class="hljs-string">'chapters'</span>]:
        chapter[<span class="hljs-string">'rating'</span>] = {<span class="hljs-string">'total'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>}

<span class="hljs-comment"># Add courses to collection</span>
<span class="hljs-keyword">for</span> course <span class="hljs-keyword">in</span> courses:
    collection.insert_one(course)

<span class="hljs-comment"># Close MongoDB connection</span>
client.close()
</code></pre>
<p>This script populates a MongoDB database with the course information from the JSON file.</p>
<p>It begins by connecting to the local MongoDB instance. It reads course data from a file called <code>courses.json</code> and creates a new field for course ratings. It then develops an index to speed up data retrieval. Lastly, the course data is added to the MongoDB collection.</p>
<p>It's a straightforward script for managing course data in a database. On running the script, all records from the <code>courses.json</code> should have been inserted into the courses DB. Switch to MongoDB Compass to verify it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-116.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>You should be able to see the JSON items in your courses database after running the python script</em></p>
<h2 id="heading-how-to-design-the-fastapi-endpoints">How to Design the FastAPI Endpoints</h2>
<p>These API endpoints provide an efficient way to manage course information, retrieve course details, and allow user interactions for rating chapters.</p>
<p>I recommend designing the API endpoints first along with the HTTP request type before writing the code. This acts as a good reference and provides clarity during the coding process.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Endpoint</td><td>Request Type</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>/courses</td><td>GET</td><td>Get a list of all available courses with sorting options.  </td></tr>
</tbody>
</table>
</div><p>Options: Sort by title (ascending), date (descending), or total course rating (descending).  </p>
<p>Optional filtering based on domain is supported. |
| /courses/{course_id} | GET | Get the overview of a specific course identified by course_id. |
| /courses/{course_id}/{chapter_id} | GET | Get information about a specific chapter within a course. |
| /courses/{course_id}/{chapter_id} | POST | Rate a specific chapter within a course.  </p>
<p>Options: Positive rating (1), negative rating (-1).  </p>
<p>The ratings are aggregated for each course. |</p>
<p>Okay, time to dive into the API code. Create a brand new Python file and call it <code>main.py</code>:</p>
<pre><code class="lang-py"><span class="hljs-keyword">import</span> contextlib
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, HTTPException, Query
<span class="hljs-keyword">from</span> pymongo <span class="hljs-keyword">import</span> MongoClient
<span class="hljs-keyword">from</span> bson <span class="hljs-keyword">import</span> ObjectId
<span class="hljs-keyword">from</span> fastapi.encoders <span class="hljs-keyword">import</span> jsonable_encoder

app = FastAPI()
client = MongoClient(<span class="hljs-string">'mongodb://localhost:27017/'</span>)
db = client[<span class="hljs-string">'courses'</span>]
</code></pre>
<p>The code imports essential modules and creates an active instance of the FastAPI class named app. It also establishes a connection to the local MongoDB database using the PyMongo library and the <code>db</code> variable now stores the connection reference to the courses document.</p>
<p>Let's go over each of these endpoints in more detail now.</p>
<h3 id="heading-the-get-all-courses-endpoint-courses-get">The Get All Courses Endpoint (<code>/courses</code> – GET)</h3>
<p>This endpoint allows you to retrieve a list of all available courses. You can sort the courses based on different criteria, such as alphabetical order (based on the course title in ascending order), date (in descending order), or total course rating (in descending order). Also, we'll allow users to filter the courses based on their domain.</p>
<pre><code class="lang-py"><span class="hljs-meta">@app.get('/courses')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_courses</span>(<span class="hljs-params">sort_by: str = <span class="hljs-string">'date'</span>, domain: str = None</span>):</span>
    <span class="hljs-comment"># set the rating.total and rating.count to all the courses based on the sum of the chapters rating</span>
    <span class="hljs-keyword">for</span> course <span class="hljs-keyword">in</span> db.courses.find():
        total = <span class="hljs-number">0</span>
        count = <span class="hljs-number">0</span>
        <span class="hljs-keyword">for</span> chapter <span class="hljs-keyword">in</span> course[<span class="hljs-string">'chapters'</span>]:
            <span class="hljs-keyword">with</span> contextlib.suppress(KeyError):
                total += chapter[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'total'</span>]
                count += chapter[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'count'</span>]
        db.courses.update_one({<span class="hljs-string">'_id'</span>: course[<span class="hljs-string">'_id'</span>]}, {<span class="hljs-string">'$set'</span>: {<span class="hljs-string">'rating'</span>: {<span class="hljs-string">'total'</span>: total, <span class="hljs-string">'count'</span>: count}}})


    <span class="hljs-comment"># sort_by == 'date' [DESCENDING]</span>
    <span class="hljs-keyword">if</span> sort_by == <span class="hljs-string">'date'</span>:
        sort_field = <span class="hljs-string">'date'</span>
        sort_order = <span class="hljs-number">-1</span>

    <span class="hljs-comment"># sort_by == 'rating' [DESCENDING]</span>
    <span class="hljs-keyword">elif</span> sort_by == <span class="hljs-string">'rating'</span>:
        sort_field = <span class="hljs-string">'rating.total'</span>
        sort_order = <span class="hljs-number">-1</span>

    <span class="hljs-comment"># sort_by == 'alphabetical' [ASCENDING]</span>
    <span class="hljs-keyword">else</span>:  
        sort_field = <span class="hljs-string">'name'</span>
        sort_order = <span class="hljs-number">1</span>

    query = {}
    <span class="hljs-keyword">if</span> domain:
        query[<span class="hljs-string">'domain'</span>] = domain


    courses = db.courses.find(query, {<span class="hljs-string">'name'</span>: <span class="hljs-number">1</span>, <span class="hljs-string">'date'</span>: <span class="hljs-number">1</span>, <span class="hljs-string">'description'</span>: <span class="hljs-number">1</span>, <span class="hljs-string">'domain'</span>:<span class="hljs-number">1</span>,<span class="hljs-string">'rating'</span>:<span class="hljs-number">1</span>,<span class="hljs-string">'_id'</span>: <span class="hljs-number">0</span>}).sort(sort_field, sort_order)
    <span class="hljs-keyword">return</span> list(courses)
</code></pre>
<p>This code defines an endpoint in the FastAPI application to retrieve a list of all available courses. The endpoint can be accessed using an HTTP GET request to the '/courses' URL.</p>
<p>The <code>@app.get()</code> decorator is attached to the <code>get_course</code> function and it takes care of this.</p>
<p>When a request is made to this endpoint, the code first calculates the total course rating by summing up the ratings of all the chapters in each course. It then updates the <code>rating</code> field of each course in the MongoDB database with the computed total and count of ratings.</p>
<p>Next, the code determines the sorting mode based on the <code>sort_by</code> query parameter. If <code>sort_by</code> is set to <code>date</code>, the courses will be sorted by their creation date in descending order. If it is set to <code>rating</code>, the courses will be sorted by their total rating in descending order. Otherwise, the courses will be sorted alphabetically by their names in ascending order.</p>
<p>If the optional <code>domain</code> query parameter is provided, the code will filter the courses based on the specified domain.</p>
<p>Finally, the code queries the MongoDB database to retrieve the relevant course information, including the course name, creation date, description, domain, and rating. The courses are sorted according to the selected sorting mode and returned as a list.</p>
<p>That was the code explanation, but what about the actual API response? Run the command below in your terminal from the current working directory:</p>
<pre><code class="lang-javascript">uvicorn main:app --reload
</code></pre>
<p>Uvicorn is an ASGI webserver. You can interact with API endpoints right on your local machine without any external server. On running the above command you should see a success message stating that the server has started.</p>
<p>Fire up your browser and enter <a target="_blank" href="http://127.0.0.1:8000/courses"><code>http://127.0.0.1:8000/courses</code></a> in the URL bar. The output that you will see will be the JSON response directly from the server.</p>
<p>Verify that the first object contains the following:</p>
<pre><code class="lang-json">{
<span class="hljs-attr">"name"</span>: <span class="hljs-string">"Introduction to Programming"</span>,
<span class="hljs-attr">"date"</span>: <span class="hljs-number">1659906000</span>,
<span class="hljs-attr">"description"</span>: <span class="hljs-string">"An introduction to programming using a language called Python. Learn how to read and write code as well as how to test and \"debug\" it. Designed for students with or without prior programming experience who'd like to learn Python specifically. Learn about functions, arguments, and return values (oh my!); variables and types; conditionals and Boolean expressions; and loops. Learn how to handle exceptions, find and fix bugs, and write unit tests; use third-party libraries; validate and extract data with regular expressions; model real-world entities with classes, objects, methods, and properties; and read and write files. Hands-on opportunities for lots of practice. Exercises inspired by real-world programming problems. No software required except for a web browser, or you can write code on your own PC or Mac."</span>,
<span class="hljs-attr">"domain"</span>: [
    <span class="hljs-string">"programming"</span>
    ],
<span class="hljs-attr">"rating"</span>: {
    <span class="hljs-attr">"total"</span>: <span class="hljs-number">6</span>,
    <span class="hljs-attr">"count"</span>: <span class="hljs-number">12</span>
    }
}
</code></pre>
<p>Guess what? It is a list of all the courses that we stored in our database. Your front-end application may now iterate over all these items and present them in a fancy way to the user. That is the power of APIs.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-117.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>The Rating for the entire course will be updated as per the aggregated sum of chapters as mentioned in the assignment document.</em></p>
<p>At this point, if you wish to see the documentation for your API do so by navigating to the <a target="_blank" href="http://127.0.0.1:8000/docs"><code>http://127.0.0.1:8000/docs</code></a> endpoint. This navigable API comes prepackages with FastAPI. How cool is that?</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-126.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>FastAPI docs for all your API endpoints</em></p>
<p>Don't like the plain old look of the docs? Fret not, there is also a <code>/redoc</code> endpoint with a slightly fancier interface. Just navigate to <code>[http://127.0.0.1:8000/](http://127.0.0.1:8000/docs)redoc</code> and you will be greeted with this screen.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-127.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>FastAPI alternate redoc interface with search and download options</em></p>
<h3 id="heading-the-get-course-overview-endpoint-coursescourseid-get">The Get Course Overview Endpoint (<code>/courses/{course_id}</code> – GET)</h3>
<p>You'll use this endpoint to get an overview of a specific course. Simply provide the course_id in the URL, and the API will return detailed information about that particular course.</p>
<pre><code class="lang-py"><span class="hljs-meta">@app.get('/courses/{course_id}')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_course</span>(<span class="hljs-params">course_id: str</span>):</span>
    course = db.courses.find_one({<span class="hljs-string">'_id'</span>: ObjectId(course_id)}, {<span class="hljs-string">'_id'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'chapters'</span>: <span class="hljs-number">0</span>})
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> course:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">404</span>, detail=<span class="hljs-string">'Course not found'</span>)
    <span class="hljs-keyword">try</span>:
        course[<span class="hljs-string">'rating'</span>] = course[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'total'</span>]
    <span class="hljs-keyword">except</span> KeyError:
        course[<span class="hljs-string">'rating'</span>] = <span class="hljs-string">'Not rated yet'</span> 

    <span class="hljs-keyword">return</span> course
</code></pre>
<p>This code snippet searches the MongoDB database for the course with the specified <code>course_ id</code> and extracts the course information while leaving out the <code>chapters</code> field.</p>
<p>If it cannot find the course, it throws an <code>HTTPException</code> with the status code 404. If it finds it, it tries to access the <code>rating</code> field and replaces it with its 'total' value to display the total rating. If not, the <code>rating</code> box is set to <code>Not rated yet</code>.</p>
<p>Finally, without the <code>chapters</code> field, it returns the JSON response of the course information, including the total rating.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-118.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Single Course Overview Endpoint Response</em></p>
<h3 id="heading-get-specific-chapter-information-endpoint-coursescourseidchapterid-get">Get Specific Chapter Information Endpoint (<code>/courses/{course_id}/{chapter_id}</code> – GET)</h3>
<p>Hitting this endpoint returns specific information about a chapter within a course. By specifying both the <code>course_id</code> and the <code>chapter_id</code> in the URL, you can access the details of that particular chapter.</p>
<pre><code class="lang-py"><span class="hljs-meta">@app.get('/courses/{course_id}/{chapter_id}')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_chapter</span>(<span class="hljs-params">course_id: str, chapter_id: str</span>):</span>    
    course = db.courses.find_one({<span class="hljs-string">'_id'</span>: ObjectId(course_id)}, {<span class="hljs-string">'_id'</span>: <span class="hljs-number">0</span>, })
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> course:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">404</span>, detail=<span class="hljs-string">'Course not found'</span>)
    chapters = course.get(<span class="hljs-string">'chapters'</span>, [])
    <span class="hljs-keyword">try</span>:
        chapter = chapters[int(chapter_id)]
    <span class="hljs-keyword">except</span> (ValueError, IndexError) <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">404</span>, detail=<span class="hljs-string">'Chapter not found'</span>) <span class="hljs-keyword">from</span> e
    <span class="hljs-keyword">return</span> chapter
</code></pre>
<p>As you might expect, <code>course_id</code> is the course identity, and <code>chapter id</code> is the chapter identifier inside that course.</p>
<p>When a request is made to this endpoint, the code first searches the MongoDB database for the course with the specified <code>course id</code>, ignoring the <code>_id</code> column in the response.</p>
<p>If the course with the supplied <code>course_id</code> cannot be found in the database, the code throws an HTTPException with the status code 404, indicating that the course could not be located.</p>
<p>The code then uses the GET function to retrieve the list of chapters for the course, setting the default value to an empty list if the 'chapters' field does not exist.</p>
<p>Using the <code>chapter_id</code> provided in the request, the code then attempts to retrieve the exact chapter within the list of chapters. If the <code>chapter id</code> is not a valid integer or is out of range for the list of chapters, the code throws an HTTPException with the status code 404. This indicates that it could not locate the chapter.</p>
<p>If it locates the chapter, the response contains information on the individual chapter within the course.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-119.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Chapter Detail Endpoint</em></p>
<h3 id="heading-rate-chapter-endpoint-coursescourseidchapterid-post">Rate Chapter Endpoint (<code>/courses/{course_id}/{chapter_id}</code> – POST)</h3>
<p>This endpoint allows users to rate individual chapters within a course. You can provide a rating of 1 for a positive review or -1 for a negative review. The API aggregates all the ratings for each course, providing valuable feedback for future improvements.</p>
<p>Up until now, we've mostly seen GET requests. But now let's see how you can send data to the server, validate it, and insert it in the application database.</p>
<pre><code class="lang-py"><span class="hljs-meta">@app.post('/courses/{course_id}/{chapter_id}')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">rate_chapter</span>(<span class="hljs-params">course_id: str, chapter_id: str, rating: int = Query(<span class="hljs-params">..., gt=<span class="hljs-number">-2</span>, lt=<span class="hljs-number">2</span></span>)</span>):</span>
    course = db.courses.find_one({<span class="hljs-string">'_id'</span>: ObjectId(course_id)}, {<span class="hljs-string">'_id'</span>: <span class="hljs-number">0</span>, })
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> course:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">404</span>, detail=<span class="hljs-string">'Course not found'</span>)
    chapters = course.get(<span class="hljs-string">'chapters'</span>, [])
    <span class="hljs-keyword">try</span>:
        chapter = chapters[int(chapter_id)]
    <span class="hljs-keyword">except</span> (ValueError, IndexError) <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">404</span>, detail=<span class="hljs-string">'Chapter not found'</span>) <span class="hljs-keyword">from</span> e
    <span class="hljs-keyword">try</span>:
        chapter[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'total'</span>] += rating
        chapter[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'count'</span>] += <span class="hljs-number">1</span>
    <span class="hljs-keyword">except</span> KeyError:
        chapter[<span class="hljs-string">'rating'</span>] = {<span class="hljs-string">'total'</span>: rating, <span class="hljs-string">'count'</span>: <span class="hljs-number">1</span>}
    db.courses.update_one({<span class="hljs-string">'_id'</span>: ObjectId(course_id)}, {<span class="hljs-string">'$set'</span>: {<span class="hljs-string">'chapters'</span>: chapters}})
    <span class="hljs-keyword">return</span> chapter
</code></pre>
<p>We have put in place an endpoint for users to rate each chapter within a course using an HTTP POST request to the <code>/courses/course_id/chapter_id</code> URL. Users can provide a rating value of 1 for a positive rating or -1 for a negative rating. The code queries the MongoDB database to find the course with the specified <code>course_id</code>, excluding the <code>_id</code> field.</p>
<p>If it doesn't find the course, it raises an HTTP exception with a status code of 404. The code retrieves the list of chapters, setting the default value to an empty list.</p>
<p>If the <code>chapter_id</code> is not a valid integer or is out of range, it raises an <code>HTTPException</code> with a status code of 404. If the chapter is found, the code updates its rating by incrementing the <code>total</code> rating value with the provided rating and incrementing the <code>count</code> value.</p>
<p>If the chapter does not have an existing <code>rating</code> field, it creates one and initializes it with the provided rating and a count of 1. The updated rating is then updated in the database, and the updated chapter is returned as the response, providing feedback to the user about their rating for that chapter.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-120.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>POST Request to add a rating to a chapter</em></p>
<p>To make a POST request, open the docs and click on the request highlighted in the above image. Then, click on "Try it out", fill in the post data, and press the Execute button right below. This sends the POST data to the server which is then validated.</p>
<p>If all the submitted data is as expected, the server accepts and shows the 200 status code meaning that the operation was successful. The submitted data is now in the MongoDB document.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-121.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Post Request Success</em></p>
<p>That's a wrap on the API development part.</p>
<h2 id="heading-automated-api-endpoint-testing-with-pytest">Automated API Endpoint Testing with PyTest</h2>
<p>As the complexity of modern web applications increases, so does the number of API endpoints and their interactions.</p>
<p>In a dynamic e-commerce web app, there could be hundreds of endpoints, each supporting multiple HTTP request methods. And these endpoints might be intricately interconnected.</p>
<p>Ensuring the proper functioning of all these endpoints after each development iteration becomes a formidable task for developers and QA teams. Here is where automated testing comes to the rescue.</p>
<p>Create a file <code>test_app.py</code> in the same directory as <code>courses.json</code> and <code>main.py</code>:</p>
<pre><code class="lang-py"><span class="hljs-keyword">from</span> fastapi.testclient <span class="hljs-keyword">import</span> TestClient
<span class="hljs-keyword">from</span> pymongo <span class="hljs-keyword">import</span> MongoClient
<span class="hljs-keyword">from</span> bson <span class="hljs-keyword">import</span> ObjectId
<span class="hljs-keyword">import</span> pytest
<span class="hljs-keyword">from</span> main <span class="hljs-keyword">import</span> app

client = TestClient(app)
mongo_client = MongoClient(<span class="hljs-string">'mongodb://localhost:27017/'</span>)
db = mongo_client[<span class="hljs-string">'courses'</span>]
</code></pre>
<p>That sets up an automated testing environment.</p>
<p><strong>FastAPI Test Client</strong> simulates HTTP requests to the web app. With this, you can pretend to be a user, sending requests to your app and getting responses back, just like a real user would.</p>
<p>We're using <strong>MongoDB Connection</strong> for course data storage, with MongoClient enabling interaction and data updates during tests.</p>
<p><strong>Test Database</strong> is a separate database for testing. It will not affect the actual course documents.</p>
<p>With this configuration, you can now create test functions that send requests to your FastAPI app using the TestClient. You will interact with your MongoDB database during these tests, but don't worry—this is just the test database, so nothing important will be harmed.</p>
<h3 id="heading-how-to-test-the-get-courses-list-endpoint">How to Test the "Get Courses List" Endpoint</h3>
<p>These test functions use <code>TestClient</code> to interact with the "/courses" endpoint of the FastAPI application. They check if the endpoint behaves as expected when different parameters, such as sorting and filtering by domain, are provided.</p>
<p>The tests verify the status codes, data presence, sorting order, and domain filtering in the API responses, ensuring the functionality of the course endpoint is correct and reliable.</p>
<pre><code class="lang-py"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_no_params</span>():</span>
    response = client.get(<span class="hljs-string">"/courses"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_sort_by_alphabetical</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?sort_by=alphabetical"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> sorted(courses, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'name'</span>]) == courses


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_sort_by_date</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?sort_by=date"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> sorted(courses, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'date'</span>], reverse=<span class="hljs-literal">True</span>) == courses

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_sort_by_rating</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?sort_by=rating"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> sorted(courses, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'rating'</span>][<span class="hljs-string">'total'</span>], reverse=<span class="hljs-literal">True</span>) == courses

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_filter_by_domain</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?domain=mathematics"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> all([c[<span class="hljs-string">'domain'</span>][<span class="hljs-number">0</span>] == <span class="hljs-string">'mathematics'</span> <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> courses])

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_filter_by_domain_and_sort_by_alphabetical</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?domain=mathematics&amp;sort_by=alphabetical"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> all([c[<span class="hljs-string">'domain'</span>][<span class="hljs-number">0</span>] == <span class="hljs-string">'mathematics'</span> <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> courses])
    <span class="hljs-keyword">assert</span> sorted(courses, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'name'</span>]) == courses

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_courses_filter_by_domain_and_sort_by_date</span>():</span>
    response = client.get(<span class="hljs-string">"/courses?domain=mathematics&amp;sort_by=date"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    courses = response.json()
    <span class="hljs-keyword">assert</span> len(courses) &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> all([c[<span class="hljs-string">'domain'</span>][<span class="hljs-number">0</span>] == <span class="hljs-string">'mathematics'</span> <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> courses])
    <span class="hljs-keyword">assert</span> sorted(courses, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'date'</span>], reverse=<span class="hljs-literal">True</span>) == courses
</code></pre>
<p>Pay attention to the assert statements. The expected results are checked against actual results and it returns a <code>True</code> or <code>False</code> Boolean based on the this comparison. The objective is to get all the tests to pass by equalizing these values.</p>
<h3 id="heading-how-to-test-the-get-single-course-info-endpoint">How to Test the "Get Single Course Info" Endpoint</h3>
<p>The tests use TestClient to send queries to FastAPI's "/courses/course id" endpoint, retrieving course data from the MongoDB database using the <code>db.courses.find_one</code> function. Comparing API response data to database data can help you determine if the endpoint handles existing and non-existent course IDs.</p>
<pre><code class="lang-py"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_course_by_id_exists</span>():</span>
    response = client.get(<span class="hljs-string">"/courses/6431137ab5da949e5978a281"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    course = response.json()
    <span class="hljs-comment"># get the course from the database</span>
    course_db = db.courses.find_one({<span class="hljs-string">'_id'</span>: ObjectId(<span class="hljs-string">'6431137ab5da949e5978a281'</span>)})
    <span class="hljs-comment"># get the name of the course from the database</span>
    name_db = course_db[<span class="hljs-string">'name'</span>]
    <span class="hljs-comment"># get the name of the course from the response</span>
    name_response = course[<span class="hljs-string">'name'</span>]
    <span class="hljs-comment"># compare the two</span>
    <span class="hljs-keyword">assert</span> name_db == name_response


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_course_by_id_not_exists</span>():</span>
    response = client.get(<span class="hljs-string">"/courses/6431137ab5da949e5978a280"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">404</span>
    <span class="hljs-keyword">assert</span> response.json() == {<span class="hljs-string">'detail'</span>: <span class="hljs-string">'Course not found'</span>}
</code></pre>
<h3 id="heading-how-to-test-the-get-course-chapter-info-endpoint">How to Test the "Get Course Chapter Info" Endpoint</h3>
<p>The tests anticipate the FastAPI application's "/courses/course id/chapter number" endpoint to provide chapter information for a certain course ID and number when they use the TestClient to make the request.</p>
<p>We use assertions to determine if the answer includes the anticipated data or gives a "Not Found" response for a non-existent chapter. It validates that the correct API chapter was retrieved and handles existing and non-existent chapters.</p>
<pre><code class="lang-py"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_chapter_info</span>():</span>
    response = client.get(<span class="hljs-string">"/courses/6431137ab5da949e5978a281/1"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    chapter = response.json()
    <span class="hljs-keyword">assert</span> chapter[<span class="hljs-string">'name'</span>] == <span class="hljs-string">'Big Picture of Calculus'</span>
    <span class="hljs-keyword">assert</span> chapter[<span class="hljs-string">'text'</span>] == <span class="hljs-string">'Highlights of Calculus'</span>


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_get_chapter_info_not_exists</span>():</span>
    response = client.get(<span class="hljs-string">"/courses/6431137ab5da949e5978a281/990"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">404</span>
    <span class="hljs-keyword">assert</span> response.json() == {<span class="hljs-string">'detail'</span>: <span class="hljs-string">'Chapter not found'</span>}
</code></pre>
<h3 id="heading-how-to-test-the-post-course-rating-endpoint">How to Test the "Post Course Rating" Endpoint</h3>
<p>To test the rating capability, the test function specifies the course ID, chapter ID, and rating variables. It uses the TestClient's post method to submit a POST request to the "/courses/course id/chapter id" API, providing the course ID and chapter number in the URL and passing the rating variable as a query parameter.</p>
<p>FastAPI mimics a user's activity to rate a certain chapter of a course. The response is successful with a 200 status code. JSON content is validated for "name" and "rating" keys, as well as "total" and "count" keys. The total rating and rating count are greater than 0, indicating users have rated the chapter.</p>
<pre><code class="lang-py"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_rate_chapter</span>():</span>
    course_id = <span class="hljs-string">"6431137ab5da949e5978a281"</span>
    chapter_id = <span class="hljs-string">"1"</span>
    rating = <span class="hljs-number">1</span>

    response = client.post(<span class="hljs-string">f"/courses/<span class="hljs-subst">{course_id}</span>/<span class="hljs-subst">{chapter_id}</span>?rating=<span class="hljs-subst">{rating}</span>"</span>)

    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>

    <span class="hljs-comment"># Check if the response body has the expected structure</span>
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"name"</span> <span class="hljs-keyword">in</span> response.json()
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"rating"</span> <span class="hljs-keyword">in</span> response.json()
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"total"</span> <span class="hljs-keyword">in</span> response.json()[<span class="hljs-string">"rating"</span>]
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"count"</span> <span class="hljs-keyword">in</span> response.json()[<span class="hljs-string">"rating"</span>]

    <span class="hljs-keyword">assert</span> response.json()[<span class="hljs-string">"rating"</span>][<span class="hljs-string">"total"</span>] &gt; <span class="hljs-number">0</span>
    <span class="hljs-keyword">assert</span> response.json()[<span class="hljs-string">"rating"</span>][<span class="hljs-string">"count"</span>] &gt; <span class="hljs-number">0</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_rate_chapter_not_exists</span>():</span>
    response = client.post(<span class="hljs-string">"/courses/6431137ab5da949e5978a281/990/rate"</span>, json={<span class="hljs-string">"rating"</span>: <span class="hljs-number">1</span>})
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">404</span>
    <span class="hljs-keyword">assert</span> response.json() == {<span class="hljs-string">'detail'</span>: <span class="hljs-string">'Not Found'</span>}
</code></pre>
<p>This verification makes sure that the rating addition endpoint works as intended, with the API returning the correct success code and expected information about the chapter, including its name and updated rating details.</p>
<p>By running the <code>pytest</code> command, all the test functions in the <code>test_app.py</code> file will be executed, and you'll get feedback on whether the endpoints are functioning as expected or if any errors or regressions have occurred. This allows developers and QA teams to catch issues early in the development cycle and maintain the application's reliability and stability.</p>
<p>As you can see in the image below, all the tests are passing. Good job! As you keep on adding more features and endpoints to the app, keep adding the associated tests in order to validate correctness. This is called <a target="_blank" href="https://www.freecodecamp.org/news/an-introduction-to-test-driven-development-c4de6dce5c/">Test Driven Development (TDD)</a>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-122.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Running API Tests with Pytest</em></p>
<p>Running the Pytest command shows the output as illustrated in the image above. It says that 13 tests pasts. This means that all our endpoints are functional and return the expected responses.</p>
<p>By detecting regressions, integrating components, resolving errors, doing load and performance tests, and testing for security, endpoint testing verifies that an application's essential operations are right. All potential weaknesses and vulnerabilities are noted and tagged for inspection.</p>
<p>Pytest helps you make sure that API endpoints work well together, and also helps you deal with failures and edge cases. It can manage numerous concurrent large requests in practical situations.</p>
<h2 id="heading-how-to-containerize-the-application-with-docker">How to Containerize the Application with Docker</h2>
<p>You can put your application and all of its dependencies together into a single unit called a container. This is called <strong>containerization</strong>. It separates the application from the underlying system, which maintains consistency across different operating systems.</p>
<p><strong>Docker</strong> is a modern containerization technology that makes it easier to create, distribute, and execute containers. It enables developers to consistently and reproducibly build, ship, and execute apps without building from source.</p>
<p>Get Docker installed from here: <a target="_blank" href="https://www.docker.com/get-started">https://www.docker.com/get-started</a>.</p>
<p>Dockerizing Python programs helps you make sure that they run consistently across multiple computers, eliminating compatibility difficulties. It containerizes the software, its dependencies, and customizations, making it portable.</p>
<p>In the same directory as other files, make a new file called <code>Dockerfile</code>. Note that it does not require any extension.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use an official Python runtime as a parent image</span>
<span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.9</span>-slim-buster

<span class="hljs-comment"># Set the working directory to /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Copy the current directory contents into the container at /code</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> ./requirements.txt /app/requirements.txt</span>

<span class="hljs-comment"># Install any needed packages specified in requirements.txt</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install --no-cache-dir --upgrade -r /app/requirements.txt</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . /app</span>

<span class="hljs-comment"># Run app.py when the container launches</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"uvicorn"</span>, <span class="hljs-string">"main:app"</span>, <span class="hljs-string">"--host"</span>, <span class="hljs-string">"0.0.0.0"</span>, <span class="hljs-string">"--port"</span>, <span class="hljs-string">"80"</span>]</span>
</code></pre>
<p>Starting with the official Python 3.9 thin image, the Dockerfile defines the image's blueprint.</p>
<p>It changes the working directory to /app, which is where the application code will be stored. This projects requirements are listed in the <code>requirements.txt</code> file, which was put into the container.</p>
<p>The RUN command uses pip to install Python requirements. COPY moves the app's code from the host to the container's /app directory. CMD provides the command that will be executed when the container starts.</p>
<p>In this case, it runs "uvicorn main:app" (the main.py FastAPI app) with host set to 0.0.0.0 and port 80.</p>
<h3 id="heading-how-to-run-the-docker-container">How to Run the Docker Container</h3>
<p>Build the Docker image in the same directory as the Dockerfile using: <code>**docker build -t my_python_app .**</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-123.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Containerizing the FastAPI app with Docker</em></p>
<p>Run the container in detached mode using the command <code>**docker run -d -p 80:80 my_python_app**</code>.</p>
<p>Once you do this, you can view the status of the containers and the image from Docker Desktop.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-128.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Docker Desktop shows that our container image is now in a running state on port 80</em></p>
<h3 id="heading-how-to-terminate-the-docker-container">How to Terminate the Docker Container</h3>
<p>Find the container ID or name with <code>**docker ps**</code>. Stop the container using its ID or name: <code>**docker stop &lt;container_id_or_name&gt;**</code></p>
<p>This walkthrough has only addressed development, testing, and containerization. Just note that post deployment container security, if neglected, introduces risks like vulnerabilities, misconfigurations, and attacks. You should ideally take advantage of a <a target="_blank" href="https://www.accuknox.com/blog/cnapp-buyers-guide">CNAPP</a> (Cloud Native Application Protection Platform) to scan images, stick to best practises, and monitor running containers for protection.</p>
<p>The takeaway is that Docker containerization allows bundling of Python scripts with dependencies, making them consistent and portable. The Dockerfile describes how the image should be created.</p>
<p>Running the container after it has been constructed is as simple as issuing a single command. It's just as simple to put a stop to it. Docker makes it simple to manage Python application distribution.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This tutorial was a quick start guide to help you leverage the power of FastAPI. We built a course administration API that efficiently handles queries related to courses.</p>
<p>We did this by importing course data from a JSON file into MongoDB and then creating multiple endpoints for users to access course lists, overviews, chapter information, and user scores. We also added a review aggregation feature to demonstrate using HTTP POST and HTTP GET methods so that you can grab data as well as post data to the server.</p>
<p>PyTest helped us handle automated testing, ensuring dependability and stability. We then containerized the application Docker, which simplifies deployment and maintenance.</p>
<p>My <a target="_blank" href="https://github.com/HighnessAtharva/fastapi-kimo/">Github Repository</a> contains the complete code covered in this quick start walkthrough. Subscribe to my <a target="_blank" href="https://atharvashah.netlify.app/">technical blog</a> for technical cheat sheets and resources.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Add JWT Authentication in FastAPI – A Practical Guide ]]>
                </title>
                <description>
                    <![CDATA[ By Abdullah Adeel FastAPI is a modern, fast, battle tested and light-weight web development framework written in Python. Other popular options in the space are Django, Flask and Bottle. And since it's new, FastAPI comes with both advantages and disad... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-add-jwt-authentication-in-fastapi/</link>
                <guid isPermaLink="false">66d45d59246e57ac83a2c6f7</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JWT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jun 2022 23:28:25 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/06/fcc-fastapi-jwt-auth.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Abdullah Adeel</p>
<p><a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a> is a modern, fast, battle tested and light-weight web development framework written in Python. Other popular options in the space are <a target="_blank" href="https://www.djangoproject.com/">Django</a>, <a target="_blank" href="https://flask.palletsprojects.com/en/2.1.x/">Flask</a> and <a target="_blank" href="https://bottlepy.org/docs/dev/">Bottle</a>.</p>
<p>And since it's new, FastAPI comes with both advantages and disadvantages.</p>
<p>On the positive side, FastAPI implements all the modern standards, taking full advantage of the features supported by the latest Python versions. It has async support and type hinting. And it's also fast (hence the name FastAPI), unopinionated, robust, and easy to use.</p>
<p>On the negative side, FastAPI lacks some complex features like out of the box user management and admin panel that come baked in with Django. The community support for FastAPI is good but not as great as other frameworks that have been out there for years and have hundreds if not thousands of open-source projects for different use cases.</p>
<p>That was a very brief introduction to FastAPI. In this article, you'll learn how to implement JWT (JSON Web Token) authentication in FastAPI with a practical example.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>In this example, I am going to use <a target="_blank" href="https://replit.com"><strong>replit</strong></a> (a great web-based IDE). Alternatively, you can simply setup your FastAPI project locally by <a target="_blank" href="https://fastapi.tiangolo.com/tutorial/">following the docs</a> or use this <a target="_blank" href="https://replit.com/@abdadeel/FastAPIstarter">replit starter template</a> by forking it. This template has all the required dependencies already installed.</p>
<p>If you have the project setup on your local environment, here are the dependencies that you need to install for JWT authentication (assuming that you have a FastAPI project running):</p>
<pre><code class="lang-shell">pip install "python-jose[cryptography]" "passlib[bcrypt]" python-multipart
</code></pre>
<p><strong>NOTE:</strong> In order to store users, I am going to use replit's built-in database. But you can apply similar operations if you are using any standard database like PostgreSQL, MongoDB, and so on. </p>
<p>If you want to see the complete implementation, I have <a target="_blank" href="https://www.youtube.com/watch?v=G8MsHbCzyZ4&amp;">this full video tutorial</a> that includes everything a production ready FastAPI application might have.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://replit.com/@abdadeel/FastAPIwithJWTauth">https://replit.com/@abdadeel/FastAPIwithJWTauth</a></div>
<h2 id="heading-authentication-with-fastapi">Authentication with FastAPI</h2>
<p>Authentication in general can have a lot of moving parts, from handling password hashing and assigning tokens to validating tokens on each request. </p>
<p>FastAPI leverages <a target="_blank" href="https://en.wikipedia.org/wiki/Dependency_injection#:~:text=In%20software%20engineering%2C%20dependency%20injection,leading%20to%20loosely%20coupled%20programs.">dependency injection</a> (a software engineering design pattern) to handle authentication schemes. Here is the list of some general steps in the process:</p>
<ul>
<li>Password hashing</li>
<li>Creating and assigning JWT tokens</li>
<li>User creation</li>
<li>Validating tokens on each request to ensure authentication</li>
</ul>
<h2 id="heading-password-hashing">Password Hashing</h2>
<p>When creating a user with a username and password, you need to hash passwords before storing them in the database. Let's see how to easily hash passwords.</p>
<p>Create a file named <code>utils.py</code> in the <code>app</code> directory and add the following function to hash user passwords.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> passlib.context <span class="hljs-keyword">import</span> CryptContext

password_context = CryptContext(schemes=[<span class="hljs-string">"bcrypt"</span>], deprecated=<span class="hljs-string">"auto"</span>)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_hashed_password</span>(<span class="hljs-params">password: str</span>) -&gt; str:</span>
    <span class="hljs-keyword">return</span> password_context.hash(password)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">verify_password</span>(<span class="hljs-params">password: str, hashed_pass: str</span>) -&gt; bool:</span>
    <span class="hljs-keyword">return</span> password_context.verify(password, hashed_pass)
</code></pre>
<p>We're using <code>passlib</code> to create the configuration context for password hashing. Here we are configuring it to use <code>bcrypt</code> . </p>
<p>The <code>get_hashed_password</code> function takes a plain password and returns the hash for it that can be safely stored in the database. The <code>verify_password</code> function takes the plain and hashed passwords and return a boolean representing whether the passwords match or not.</p>
<h2 id="heading-how-to-generate-jwt-tokens">How to Generate JWT Tokens</h2>
<p>In this section, we will write two helper functions to generate access and refresh tokens with a particular payload. Later we can use these functions to generate tokens for a particular user by passing the user-related payload.</p>
<p>Inside the <code>app/utils.py</code> file that you created earlier, add the following import statements:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime, timedelta
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Union, Any
<span class="hljs-keyword">from</span> jose <span class="hljs-keyword">import</span> jwt
</code></pre>
<p>Add the following constants that will be passed when creating JWTs:</p>
<pre><code class="lang-python">ACCESS_TOKEN_EXPIRE_MINUTES = <span class="hljs-number">30</span>  <span class="hljs-comment"># 30 minutes</span>
REFRESH_TOKEN_EXPIRE_MINUTES = <span class="hljs-number">60</span> * <span class="hljs-number">24</span> * <span class="hljs-number">7</span> <span class="hljs-comment"># 7 days</span>
ALGORITHM = <span class="hljs-string">"HS256"</span>
JWT_SECRET_KEY = os.environ[<span class="hljs-string">'JWT_SECRET_KEY'</span>]   <span class="hljs-comment"># should be kept secret</span>
JWT_REFRESH_SECRET_KEY = os.environ[<span class="hljs-string">'JWT_REFRESH_SECRET_KEY'</span>]    <span class="hljs-comment"># should be kept secret</span>
</code></pre>
<p><code>JWT_SECRET_KEY</code> and <code>JWT_REFRESH_SECRET_KEY</code> can be any strings, but make sure to keep them secret and set them as environment variables. </p>
<p>If you are following along on replit.com, you can set these environment variables from the <code>Secrets</code> tab on the left menu bar.</p>
<p>Add the following functions at the end of the <code>app/utils.py</code> file:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_access_token</span>(<span class="hljs-params">subject: Union[str, Any], expires_delta: int = None</span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> expires_delta <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        expires_delta = datetime.utcnow() + expires_delta
    <span class="hljs-keyword">else</span>:
        expires_delta = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    to_encode = {<span class="hljs-string">"exp"</span>: expires_delta, <span class="hljs-string">"sub"</span>: str(subject)}
    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, ALGORITHM)
    <span class="hljs-keyword">return</span> encoded_jwt

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_refresh_token</span>(<span class="hljs-params">subject: Union[str, Any], expires_delta: int = None</span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> expires_delta <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        expires_delta = datetime.utcnow() + expires_delta
    <span class="hljs-keyword">else</span>:
        expires_delta = datetime.utcnow() + timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)

    to_encode = {<span class="hljs-string">"exp"</span>: expires_delta, <span class="hljs-string">"sub"</span>: str(subject)}
    encoded_jwt = jwt.encode(to_encode, JWT_REFRESH_SECRET_KEY, ALGORITHM)
    <span class="hljs-keyword">return</span> encoded_jwt
</code></pre>
<p>The only difference between these two functions is that the expiration time for refresh tokens is longer than for access tokens.</p>
<p>The functions simply take the payload to include inside the JWT, which can be anything. Usually you would want to store information like USER_ID here, but this can be anything from strings to objects/dictionaries. The functions return tokens as strings.</p>
<p>In the end your <code>app/utils.py</code> file should look something like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> passlib.context <span class="hljs-keyword">import</span> CryptContext
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime, timedelta
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Union, Any
<span class="hljs-keyword">from</span> jose <span class="hljs-keyword">import</span> jwt

ACCESS_TOKEN_EXPIRE_MINUTES = <span class="hljs-number">30</span>  <span class="hljs-comment"># 30 minutes</span>
REFRESH_TOKEN_EXPIRE_MINUTES = <span class="hljs-number">60</span> * <span class="hljs-number">24</span> * <span class="hljs-number">7</span> <span class="hljs-comment"># 7 days</span>
ALGORITHM = <span class="hljs-string">"HS256"</span>
JWT_SECRET_KEY = os.environ[<span class="hljs-string">'JWT_SECRET_KEY'</span>]     <span class="hljs-comment"># should be kept secret</span>
JWT_REFRESH_SECRET_KEY = os.environ[<span class="hljs-string">'JWT_REFRESH_SECRET_KEY'</span>]      <span class="hljs-comment"># should be kept secret</span>

password_context = CryptContext(schemes=[<span class="hljs-string">"bcrypt"</span>], deprecated=<span class="hljs-string">"auto"</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_hashed_password</span>(<span class="hljs-params">password: str</span>) -&gt; str:</span>
    <span class="hljs-keyword">return</span> password_context.hash(password)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">verify_password</span>(<span class="hljs-params">password: str, hashed_pass: str</span>) -&gt; bool:</span>
    <span class="hljs-keyword">return</span> password_context.verify(password, hashed_pass)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_access_token</span>(<span class="hljs-params">subject: Union[str, Any], expires_delta: int = None</span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> expires_delta <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        expires_delta = datetime.utcnow() + expires_delta
    <span class="hljs-keyword">else</span>:
        expires_delta = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    to_encode = {<span class="hljs-string">"exp"</span>: expires_delta, <span class="hljs-string">"sub"</span>: str(subject)}
    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, ALGORITHM)
    <span class="hljs-keyword">return</span> encoded_jwt

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_refresh_token</span>(<span class="hljs-params">subject: Union[str, Any], expires_delta: int = None</span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> expires_delta <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        expires_delta = datetime.utcnow() + expires_delta
    <span class="hljs-keyword">else</span>:
        expires_delta = datetime.utcnow() + timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)

    to_encode = {<span class="hljs-string">"exp"</span>: expires_delta, <span class="hljs-string">"sub"</span>: str(subject)}
    encoded_jwt = jwt.encode(to_encode, JWT_REFRESH_SECRET_KEY, ALGORITHM)
    <span class="hljs-keyword">return</span> encoded_jwt
</code></pre>
<h2 id="heading-how-to-handle-user-signups">How to Handle User Signups</h2>
<p>Inside the <code>app/app.py</code> file, create another endpoint for handling user signups. The endpoint should take the username/email and password as data. It then checks to make sure another account with the email/username does not exist. Then it creates the user and saves it to the database.</p>
<p>In <code>app/app.py</code>, add the following handler function:</p>
<pre><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, status, HTTPException
<span class="hljs-keyword">from</span> fastapi.responses <span class="hljs-keyword">import</span> RedirectResponse
<span class="hljs-keyword">from</span> app.schemas <span class="hljs-keyword">import</span> UserOut, UserAuth
<span class="hljs-keyword">from</span> replit <span class="hljs-keyword">import</span> db
<span class="hljs-keyword">from</span> app.utils <span class="hljs-keyword">import</span> get_hashed_password
<span class="hljs-keyword">from</span> uuid <span class="hljs-keyword">import</span> uuid4

@app.post(<span class="hljs-string">'/signup'</span>, summary=<span class="hljs-string">"Create new user"</span>, response_model=UserOut)
<span class="hljs-keyword">async</span> def create_user(data: UserAuth):
    # querying database to check <span class="hljs-keyword">if</span> user already exist
    user = db.get(data.email, None)
    <span class="hljs-keyword">if</span> user is not None:
            raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=<span class="hljs-string">"User with this email already exist"</span>
        )
    user = {
        <span class="hljs-string">'email'</span>: data.email,
        <span class="hljs-string">'password'</span>: get_hashed_password(data.password),
        <span class="hljs-string">'id'</span>: str(uuid4())
    }
    db[data.email] = user    # saving user to database
    <span class="hljs-keyword">return</span> user
</code></pre><h2 id="heading-how-to-handle-logins">How to Handle Logins</h2>
<p>FastAPI has a standard way of handling logins to comply with OpenAPI standards. This automatically adds authentication in the swagger docs without any extra configurations.</p>
<p>Add the following handler function for user logins and assign each user access and refresh tokens. Don't forget to include imports.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, status, HTTPException, Depends
<span class="hljs-keyword">from</span> fastapi.security <span class="hljs-keyword">import</span> OAuth2PasswordRequestForm
<span class="hljs-keyword">from</span> fastapi.responses <span class="hljs-keyword">import</span> RedirectResponse
<span class="hljs-keyword">from</span> app.schemas <span class="hljs-keyword">import</span> UserOut, UserAuth, TokenSchema
<span class="hljs-keyword">from</span> replit <span class="hljs-keyword">import</span> db
<span class="hljs-keyword">from</span> app.utils <span class="hljs-keyword">import</span> (
    get_hashed_password,
    create_access_token,
    create_refresh_token,
    verify_password
)
<span class="hljs-keyword">from</span> uuid <span class="hljs-keyword">import</span> uuid4

<span class="hljs-meta">@app.post('/login', summary="Create access and refresh tokens for user", response_model=TokenSchema)</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">login</span>(<span class="hljs-params">form_data: OAuth2PasswordRequestForm = Depends(<span class="hljs-params"></span>)</span>):</span>
    user = db.get(form_data.username, <span class="hljs-literal">None</span>)
    <span class="hljs-keyword">if</span> user <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">raise</span> HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=<span class="hljs-string">"Incorrect email or password"</span>
        )

    hashed_pass = user[<span class="hljs-string">'password'</span>]
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> verify_password(form_data.password, hashed_pass):
        <span class="hljs-keyword">raise</span> HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=<span class="hljs-string">"Incorrect email or password"</span>
        )

    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"access_token"</span>: create_access_token(user[<span class="hljs-string">'email'</span>]),
        <span class="hljs-string">"refresh_token"</span>: create_refresh_token(user[<span class="hljs-string">'email'</span>]),
    }
</code></pre>
<p>This endpoint is a bit different from the other post endpoints where you defined the schema for filtering incoming data. </p>
<p>For login endpoints, we use <code>OAuth2PasswordRequestForm</code> as a dependency. This will make sure to extract data from the request and pass is as a <code>form_data</code> argument to the the <code>login</code> handler function. <code>python-multipart</code> is used to extract form data. So make sure that you have installed it.</p>
<p>The endpoint will reflect in the swagger docs with inputs for username and password. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-49.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>On successful response, you will get tokens as shown here:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-50.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-add-protected-routes">How to Add Protected Routes</h2>
<p>Now since we have added support for login and signup, we can add protected endpoints. In FastAPI, protected endpoints are handled using dependency injection and FastAPI can infer this from the OpenAPI schema and reflect it in the swagger docs. </p>
<p>Let's see the power of dependency injection. At this point, there is no way we can authenticate from the docs. This is because currently we don't have any protected endpoint, so the OpenAPI schema does not have enough information about the login strategy we are using.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-51.png" alt="Image" width="600" height="400" loading="lazy">
<em>No button in swagger docs to login.</em></p>
<p>Let's create our custom dependency. It's nothing but a function that is run before the actual handler function to get arguments passed to the hander function. Let's see with a practical example.</p>
<p>Create another file <code>app/deps.py</code>  and add include the following function in it:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Union, Any
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends, HTTPException, status
<span class="hljs-keyword">from</span> fastapi.security <span class="hljs-keyword">import</span> OAuth2PasswordBearer
<span class="hljs-keyword">from</span> .utils <span class="hljs-keyword">import</span> (
    ALGORITHM,
    JWT_SECRET_KEY
)

<span class="hljs-keyword">from</span> jose <span class="hljs-keyword">import</span> jwt
<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> ValidationError
<span class="hljs-keyword">from</span> app.schemas <span class="hljs-keyword">import</span> TokenPayload, SystemUser
<span class="hljs-keyword">from</span> replit <span class="hljs-keyword">import</span> db

reuseable_oauth = OAuth2PasswordBearer(
    tokenUrl=<span class="hljs-string">"/login"</span>,
    scheme_name=<span class="hljs-string">"JWT"</span>
)


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_current_user</span>(<span class="hljs-params">token: str = Depends(<span class="hljs-params">reuseable_oauth</span>)</span>) -&gt; SystemUser:</span>
    <span class="hljs-keyword">try</span>:
        payload = jwt.decode(
            token, JWT_SECRET_KEY, algorithms=[ALGORITHM]
        )
        token_data = TokenPayload(**payload)

        <span class="hljs-keyword">if</span> datetime.fromtimestamp(token_data.exp) &lt; datetime.now():
            <span class="hljs-keyword">raise</span> HTTPException(
                status_code = status.HTTP_401_UNAUTHORIZED,
                detail=<span class="hljs-string">"Token expired"</span>,
                headers={<span class="hljs-string">"WWW-Authenticate"</span>: <span class="hljs-string">"Bearer"</span>},
            )
    <span class="hljs-keyword">except</span>(jwt.JWTError, ValidationError):
        <span class="hljs-keyword">raise</span> HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=<span class="hljs-string">"Could not validate credentials"</span>,
            headers={<span class="hljs-string">"WWW-Authenticate"</span>: <span class="hljs-string">"Bearer"</span>},
        )

    user: Union[dict[str, Any], <span class="hljs-literal">None</span>] = db.get(token_data.sub, <span class="hljs-literal">None</span>)


    <span class="hljs-keyword">if</span> user <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">raise</span> HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=<span class="hljs-string">"Could not find user"</span>,
        )

    <span class="hljs-keyword">return</span> SystemUser(**user)
</code></pre>
<p>Here we are defining the <code>get_current_user</code> function as a dependency which in turn takes an instance of <code>OAuth2PasswordBearer</code> as a dependency.</p>
<pre><code class="lang-python">reuseable_oauth = OAuth2PasswordBearer(
    tokenUrl=<span class="hljs-string">"/login"</span>,
    scheme_name=<span class="hljs-string">"JWT"</span>
)
</code></pre>
<p><code>OAuth2PasswordBearer</code> takes two required parameters. <code>tokenUrl</code> is the URL in your application that handles user login and return tokens. <code>scheme_name</code> set to <code>JWT</code> will allow the frontend swagger docs to call <code>tokenUrl</code> from the frontend and save tokens in memory. Then each subsequent request to the protected endpoints will have the token sent as <code>Authorization</code> headers so <code>OAuth2PasswordBearer</code> can parse it.</p>
<p>Now let's add a protected endpoint that returns user account information as the response. For this, a user has to be logged in and the endpoint will respond with information for the currently logged-in user.</p>
<p>In <code>app/app.py</code> create another handler function. Make sure to include imports as well.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> app.deps <span class="hljs-keyword">import</span> get_current_user

<span class="hljs-meta">@app.get('/me', summary='Get details of currently logged in user', response_model=UserOut)</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_me</span>(<span class="hljs-params">user: User = Depends(<span class="hljs-params">get_current_user</span>)</span>):</span>
    <span class="hljs-keyword">return</span> user
</code></pre>
<p>As soon as you add this endpoint, you will be able to see the <code>Authorize</code> button in the swagger docs and a 🔒 icon in front of the protected endpoint <code>/me</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-56.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>This is power of dependency injection and FastAPI's ability to generate an automatic OpenAPI schema. </p>
<p>Clicking the <code>Authorize</code> button will open the authorization form with the required fields for login. On a successful response, tokens will be saved and sent to subsequent request in the headers.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-57.png" alt="Image" width="600" height="400" loading="lazy">
<em>Swagger integrated login form</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/06/image-58.png" alt="Image" width="600" height="400" loading="lazy">
<em>successfully logged in</em></p>
<p>At this point, you can access all the protected endpoints. To make an endpoint protected, you just need to add the <code>get_current_user</code> function as a dependency. That's all you need to do!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you followed along, you should have a working FastAPI application with JWT authentication. If not, you can always run <a target="_blank" href="https://replit.com/@abdadeel/FastAPI-with-JWT-authentication">this repl</a> and play around with it or visit <a target="_blank" href="https://fastapi-with-jwt-authentication.abdadeel.repl.co/docs">this deployed version</a>. You can find the GitHub code for this project <a target="_blank" href="https://github.com/mabdullahadeel/fcc-fastapi-jwt">here</a>.</p>
<p>If you found this article helpful, give me a follow at <a target="_blank" href="https://twitter.com/abdadeel_">twitter</a> <a target="_blank" href="https://twitter.com/abdadeel_">@abdadeel_</a>. And don't forget that you can always watch <a target="_blank" href="https://www.youtube.com/watch?v=G8MsHbCzyZ4&amp;ab_channel=ABDLogs">this video</a> for detail explanation with a practical example.</p>
<p>Thanks ;)</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create Microservices with FastAPI ]]>
                </title>
                <description>
                    <![CDATA[ FastAPI is a Web framework for developing RESTful APIs in Python. It is a great choice when you want to build an app based on microservices. We just published a course on the freeCodeCamp.org YouTube channel that will teach you how to develop microse... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-microservices-with-fastapi/</link>
                <guid isPermaLink="false">66b2031427569435a9255aba</guid>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 24 Mar 2022 16:43:34 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/03/microservices.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>FastAPI is a Web framework for developing RESTful APIs in Python. It is a great choice when you want to build an app based on microservices.</p>
<p>We just published a course on the freeCodeCamp.org YouTube channel that will teach you how to develop microservices app using FastAPI.</p>
<p>In this course, you will create a simple microservices app using Python FastAPI with React on the frontend. You will learn how to use RedisJSON as a Database and dispatch events with Redis Streams. RedisJSON is a NoSQL database just like MongoDB and Redis Streams is an Event Bus just like RabbitMQ or Apache Kafka.</p>
<p>Antonio Papa from Scalable Scripts developed this course. He has a bunch if experience working with a variety of frontend and backend frameworks.</p>
<p>Here are the sections in this course:</p>
<ul>
<li>App Demo</li>
<li>Inventory Microservice Setup</li>
<li>Redis Cloud</li>
<li>Connect to Redis Cloud</li>
<li>Products CRUD</li>
<li>Payment Microservice Setup</li>
<li>Internal Http Requests</li>
<li>Background Tasks</li>
<li>Redis Streams</li>
<li>Frontend</li>
</ul>
<p>Watch the full course below or on <a target="_blank" href="https://youtu.be/Cy9fAvsXGZA">the freeCodeCamp.org YouTube channel</a> (1.5 hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Cy9fAvsXGZA" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
