DEV Community

Mwenda Harun Mbaabu
Mwenda Harun Mbaabu

Posted on

Deploying Machine Learning Models with FastAPI

A practical journey from training a machine learning model in a notebook to exposing it as an API and connecting it to a real user interface.

Building a machine learning model is an important achievement. But training a model is not the end of the machine learning lifecycle.

For many people learning Data Science and Machine Learning, the journey often looks like this:

  • Collect a dataset
  • Clean the data
  • Perform exploratory data analysis
  • Engineer features
  • Train several models
  • Evaluate their performance
  • Select the best model
  • Generate predictions
  • Save the notebook

At that point, the model works. But only inside your environment. A real-world machine learning system needs to go further.

The model needs to become accessible to:

  • Web applications
  • Mobile applications
  • Internal business systems
  • Other developers
  • Data platforms
  • Customer-facing applications

This is where Machine Learning Model Deployment becomes important.

In our upcoming FastAPI Fundamentals and Deploying Machine Learning Models with FastAPI Workshop, we will explore how to take a trained machine learning model and expose it through a production-style REST API using Python and FastAPI.

The goal is not simply to train better models, the goal is to build machine learning systems that people and applications can actually use. Move machine learning beyond the notebook and make it usable in real applications.


What We Will Build.

By the end of the workshop, we will deploy a Customer Churn Prediction Model and connect it to a ready-made web interface.

The user interface will already be developed and made available to all participants.

This allows us to concentrate on the most important part of the workshop: building the machine learning backend and exposing the model through API endpoints.

Our workflow will therefore focus on:

  • Understanding APIs
  • Understanding FastAPI
  • Building API endpoints
  • Loading a trained machine learning model
  • Validating incoming customer data
  • Sending the data to the model
  • Generating churn probabilities
  • Returning predictions as JSON
  • Connecting the FastAPI API to the frontend UI

When everything is connected, a user will be able to enter customer information into the interface and receive a result such as:

prediction_response = {
    "prediction": "Likely to Churn",
    "churn_probability": 0.81
}
Enter fullscreen mode Exit fullscreen mode

Instead of seeing:

prediction = 1
Enter fullscreen mode Exit fullscreen mode

The end user could see something meaningful:

result = {
    "prediction": "Likely to Churn",
    "risk_level": "High",
    "churn_probability": "81%",
    "recommendation": "Consider retention outreach or a discount offer."
}
Enter fullscreen mode Exit fullscreen mode

That is the transition from a machine learning experiment to a machine learning application.


The Problem: Your Model Works, But Only on Your Computer.

Imagine that you have spent several hours building a machine learning model.

You have successfully:

  1. Collected your dataset
  2. Cleaned and transformed the data
  3. Performed exploratory data analysis
  4. Selected important features
  5. Split the data into training and testing sets
  6. Trained several machine learning algorithms
  7. Evaluated their performance
  8. Selected the best-performing model

Eventually, you run:

prediction = model.predict(new_data)

print(prediction)
Enter fullscreen mode Exit fullscreen mode

And the model returns:

prediction = [1]
Enter fullscreen mode Exit fullscreen mode

Great.

Your machine learning model works. But there is still a problem.

Who can actually use it?

At this point, probably only someone who has:

  • Your Jupyter Notebook
  • Python installed
  • Your dependencies installed
  • Access to the trained model
  • Knowledge of your preprocessing steps
  • Knowledge of your Python code

That is not practical for a real application. Consider a telecom company using your churn model.

A customer service officer should not need to open Jupyter Notebook and run:

model.predict(customer_data)
Enter fullscreen mode Exit fullscreen mode

A web application should not need to understand how Random Forest works. A mobile application should not need access to your notebook. A CRM should not need your entire machine learning project.

Instead, these applications should simply send data to your model and receive a prediction.

This is where APIs become extremely useful.


What Is an API?

API stands for:

  • Application Programming Interface

An API provides a structured way for software applications to communicate with each other.

Think of an API as an intermediary. One application sends a request. Another application processes that request. A response is returned.

For our machine learning system, the communication might look like this:

Frontend

API

Machine Learning Model

Prediction

API

Frontend

Suppose our Customer Churn Prediction system exposes this endpoint:

endpoint = "/predict"
Enter fullscreen mode Exit fullscreen mode

The frontend sends customer information to the endpoint.

For example:

customer_data = {
    "tenure": 12,
    "monthly_charges": 89.50,
    "contract": "Month-to-month",
    "internet_service": "Fiber optic",
    "payment_method": "Electronic check",
    "paperless_billing": "Yes",
    "senior_citizen": "No",
    "dependents": "No"
}
Enter fullscreen mode Exit fullscreen mode

The API receives the information. It then passes the customer data to the machine learning model.

The model calculates a prediction. The API returns:

response = {
    "prediction": "Likely to Churn",
    "churn_probability": 0.81
}
Enter fullscreen mode Exit fullscreen mode

The frontend can now display:

The application using our API does not need to understand how our model was trained.

It does not need to know whether we used:

  • Logistic Regression
  • Random Forest
  • XGBoost
  • Gradient Boosting
  • Neural Networks

It only needs to understand the API contract:

api_contract = {
    "endpoint": "/predict",
    "method": "POST",
    "input": "Customer information",
    "output": "Churn prediction"
}
Enter fullscreen mode Exit fullscreen mode

This separation is extremely powerful.


Why FastAPI?

For this workshop, we will use FastAPI. FastAPI is a modern Python framework designed for building APIs.

It is particularly attractive for machine learning applications because most machine learning development already happens inside the Python ecosystem.

A Data Scientist or Machine Learning Engineer may already be working with:

  • Pandas
  • NumPy
  • Scikit-learn
  • XGBoost
  • PyTorch
  • TensorFlow
  • Joblib

Adding FastAPI means we can build the serving layer without abandoning Python.

A very simple FastAPI application can look like this:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def home():
    return {
        "message": "Machine Learning API is running."
    }
Enter fullscreen mode Exit fullscreen mode

We can make our application slightly more descriptive:

from fastapi import FastAPI


app = FastAPI(
    title="Customer Churn Prediction API",
    description="Machine Learning API for predicting customer churn.",
    version="1.0.0"
)


@app.get("/")
def home():
    return {
        "status": "healthy",
        "message": "Customer Churn Prediction API is running."
    }
Enter fullscreen mode Exit fullscreen mode

Already, we have created a working API.


The Journey From Notebook to Production.

Our workshop will follow this architecture:

Raw Data

Data Cleaning

Feature Engineering

Model Training

Model Evaluation

Model Pipeline

Save Model

FastAPI Application

Prediction Endpoint

Frontend

User

In Python, we could represent the lifecycle as:

machine_learning_lifecycle = [
    "Data Collection",
    "Data Cleaning",
    "Exploratory Data Analysis",
    "Feature Engineering",
    "Model Training",
    "Model Evaluation",
    "Model Persistence",
    "API Development",
    "Model Serving",
    "Frontend Integration"
]
Enter fullscreen mode Exit fullscreen mode

The important idea is that machine learning does not exist in isolation.

Eventually, it must interact with other software systems.


Our Business Problem: Customer Churn.

For the workshop, our main project will be Customer Churn Prediction. Customer churn occurs when a customer stops using a company's service.

Examples include customers:

  • Cancelling a telecommunications subscription
  • Closing a bank account
  • Cancelling an insurance policy
  • Leaving a streaming platform
  • Cancelling a SaaS subscription
  • Moving to another internet provider

For many companies, customer retention is extremely important.

Our machine learning problem can therefore be stated as:

Given information about a customer and their subscription behaviour, can we estimate their likelihood of leaving the company?

Our model might consider features such as:

  • Customer tenure
  • Monthly charges
  • Contract type
  • Internet service
  • Payment method
  • Paperless billing
  • Senior citizen status
  • Dependents
  • Services subscribed to

Our target might be:

target = "churn"
Enter fullscreen mode Exit fullscreen mode

Where:

churn_labels = {
    0: "Not Likely to Churn",
    1: "Likely to Churn"
}
Enter fullscreen mode Exit fullscreen mode

This makes Customer Churn a binary classification problem.


Step 1: Prepare the Data.

Before training the model, we separate our features and target.

For example:

target_column = "Churn"

X = df.drop(columns=[target_column])
y = df[target_column]
Enter fullscreen mode Exit fullscreen mode

We then divide the dataset into training and testing sets.

from sklearn.model_selection import train_test_split


X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y
)
Enter fullscreen mode Exit fullscreen mode

Why do we split the data?

Because we want to evaluate the model using observations it did not see during training.


Step 2: Build a Preprocessing Pipeline.

Real-world datasets rarely contain only perfectly formatted numerical values.

Our churn dataset may contain:

Numerical features

numerical_features = [
    "tenure",
    "monthly_charges"
]
Enter fullscreen mode Exit fullscreen mode

and categorical features:

categorical_features = [
    "contract",
    "internet_service",
    "payment_method",
    "paperless_billing",
    "senior_citizen",
    "dependents"
]
Enter fullscreen mode Exit fullscreen mode

We therefore need preprocessing.

Instead of manually repeating preprocessing every time we generate a prediction, we can build a reusable Scikit-learn pipeline.

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline


numerical_transformer = Pipeline(
    steps=[
        (
            "scaler",
            StandardScaler()
        )
    ]
)


categorical_transformer = Pipeline(
    steps=[
        (
            "encoder",
            OneHotEncoder(
                handle_unknown="ignore"
            )
        )
    ]
)


preprocessor = ColumnTransformer(
    transformers=[
        (
            "numerical",
            numerical_transformer,
            numerical_features
        ),
        (
            "categorical",
            categorical_transformer,
            categorical_features
        )
    ]
)
Enter fullscreen mode Exit fullscreen mode

This is an important production concept.

The same preprocessing transformations used during training should also be used when new requests arrive through our API.


Step 3: Train the Machine Learning Model.

We can combine our preprocessing and classification algorithm into one pipeline.

For example:

from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline


classifier = RandomForestClassifier(
    n_estimators=200,
    random_state=42,
    class_weight="balanced"
)


model_pipeline = Pipeline(
    steps=[
        (
            "preprocessor",
            preprocessor
        ),
        (
            "classifier",
            classifier
        )
    ]
)
Enter fullscreen mode Exit fullscreen mode

Now we train the complete pipeline.

model_pipeline.fit(
    X_train,
    y_train
)
Enter fullscreen mode Exit fullscreen mode

Notice what we are doing here.

We are not saving only the classifier.

We are creating:

Raw Customer Data

Preprocessing

Feature Transformation

Random Forest

Prediction

as one reusable pipeline.


Step 4: Evaluate the Model.

Training a model is not enough.

We need to understand how well it performs.

from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score
)


y_pred = model_pipeline.predict(
    X_test
)


evaluation = {
    "accuracy": accuracy_score(
        y_test,
        y_pred
    ),
    "precision": precision_score(
        y_test,
        y_pred
    ),
    "recall": recall_score(
        y_test,
        y_pred
    ),
    "f1_score": f1_score(
        y_test,
        y_pred
    )
}


print(evaluation)
Enter fullscreen mode Exit fullscreen mode

For churn prediction, we should not look at accuracy alone.

Imagine we care about identifying customers who are genuinely at risk of leaving.

In that situation, recall becomes particularly important.

It helps us answer:

Of all the customers who actually churned, how many did our model identify?

Machine learning metrics should therefore always be interpreted in the context of the business problem.


Step 5: Save the Complete Model Pipeline.

Once we are satisfied with our model, we save it.

We can use Joblib.

import joblib


MODEL_PATH = "model/customer_churn_pipeline.joblib"


joblib.dump(
    model_pipeline,
    MODEL_PATH
)
Enter fullscreen mode Exit fullscreen mode

Now we have a reusable machine learning artifact.

The important distinction is:

Jupyter Notebook

Used for:

  • Exploration
  • Experimentation
  • Model development
  • Training
  • Evaluation

Saved Model

Used for:

  • Prediction
  • Serving
  • Applications
  • Production systems

Our API does not need to retrain the model for every request.

It simply loads the trained model.


Step 6: Organise the Project.

A professional project should have a clear structure.

For example:

project_structure = """
customer-churn-fastapi/
│
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── schemas.py
│   ├── model_loader.py
│   └── predictor.py
│
├── data/
│   └── customer_churn.csv
│
├── model/
│   └── customer_churn_pipeline.joblib
│
├── notebooks/
│   └── model_training.ipynb
│
├── tests/
│   └── test_api.py
│
├── requirements.txt
├── README.md
└── .gitignore
"""
Enter fullscreen mode Exit fullscreen mode

Each component has a responsibility.

The notebook trains the model.

The model directory stores the trained artifact.

The application directory contains the API.

The tests directory contains API tests.

This separation makes the project easier to understand and maintain.


Step 7: Load the Model Into FastAPI

We can create a small model loader.

from pathlib import Path

import joblib


MODEL_PATH = (
    Path(__file__).resolve().parent.parent
    / "model"
    / "customer_churn_pipeline.joblib"
)


def load_model():
    model = joblib.load(
        MODEL_PATH
    )

    return model
Enter fullscreen mode Exit fullscreen mode

Our API can now reuse the model without retraining it.


Step 8: Define the API Input Schema

When the frontend sends information, our API should know exactly what fields to expect.

We can define this using Pydantic.

from pydantic import BaseModel, Field


class CustomerInput(BaseModel):

    tenure: int = Field(
        ...,
        ge=0,
        description="Number of months the customer has stayed."
    )

    monthly_charges: float = Field(
        ...,
        ge=0,
        description="Customer monthly charges."
    )

    contract: str

    internet_service: str

    payment_method: str

    paperless_billing: str

    senior_citizen: str

    dependents: str
Enter fullscreen mode Exit fullscreen mode

This gives our API an explicit data contract.

If someone sends:

invalid_request = {
    "tenure": "twelve months",
    "monthly_charges": "expensive"
}
Enter fullscreen mode Exit fullscreen mode

FastAPI can reject the request before invalid data reaches the machine learning model.

Input validation is therefore not simply convenient.

It is part of building reliable systems.


Step 9: Create the Prediction Endpoint

Now we arrive at the central part of the workshop.

We connect FastAPI to our trained model.

import pandas as pd

from fastapi import FastAPI

from app.model_loader import load_model
from app.schemas import CustomerInput


app = FastAPI(
    title="Customer Churn Prediction API",
    description=(
        "REST API for serving predictions "
        "from a trained customer churn model."
    ),
    version="1.0.0"
)


model = load_model()


@app.get("/")
def home():

    return {
        "status": "healthy",
        "service": "Customer Churn Prediction API"
    }


@app.post("/predict")
def predict_churn(
    customer: CustomerInput
):

    input_data = pd.DataFrame(
        [
            customer.model_dump()
        ]
    )

    prediction = model.predict(
        input_data
    )[0]

    probabilities = model.predict_proba(
        input_data
    )[0]

    churn_probability = probabilities[1]

    prediction_label = (
        "Likely to Churn"
        if prediction == 1
        else "Not Likely to Churn"
    )

    return {
        "prediction": prediction_label,
        "churn_probability": round(
            float(churn_probability),
            4
        )
    }
Enter fullscreen mode Exit fullscreen mode

Now our machine learning model is accessible through:

prediction_endpoint = "POST /predict"
Enter fullscreen mode Exit fullscreen mode

This is a major transition.

Before:

Notebook → model.predict()

Now:

Application → API → Model → Prediction → Application

Our model has become a service.


Step 10: Run the FastAPI Application

We can start the API using Uvicorn.

command = "uvicorn app.main:app --reload"
Enter fullscreen mode Exit fullscreen mode

The development server will typically become available at:

api_url = "http://127.0.0.1:8000"
Enter fullscreen mode Exit fullscreen mode

FastAPI also automatically generates interactive API documentation.

swagger_docs = "http://127.0.0.1:8000/docs"
Enter fullscreen mode Exit fullscreen mode

This means that before connecting our UI, we can test the machine learning API directly from our browser.


Step 11: Send a Prediction Request.

Our frontend might eventually send information similar to:

customer = {
    "tenure": 12,
    "monthly_charges": 89.50,
    "contract": "Month-to-month",
    "internet_service": "Fiber optic",
    "payment_method": "Electronic check",
    "paperless_billing": "Yes",
    "senior_citizen": "No",
    "dependents": "No"
}
Enter fullscreen mode Exit fullscreen mode

The request is sent to:

endpoint = "POST /predict"
Enter fullscreen mode Exit fullscreen mode

Our FastAPI backend sends the customer information through:

prediction = model.predict(
    input_data
)
Enter fullscreen mode Exit fullscreen mode

and:

probability = model.predict_proba(
    input_data
)
Enter fullscreen mode Exit fullscreen mode

The API returns:

response = {
    "prediction": "Likely to Churn",
    "churn_probability": 0.81
}
Enter fullscreen mode Exit fullscreen mode

The frontend then converts this machine-readable response into something useful for a human.

For example:

interface_result = {
    "status": "Likely to Churn",
    "churn_probability": "81%",
    "risk": "High",
    "recommendation": (
        "Consider retention outreach "
        "or a discount offer."
    )
}
Enter fullscreen mode Exit fullscreen mode

From Model Output to Business Decision.

This is an important distinction. A machine learning model might technically return:

raw_prediction = 1
Enter fullscreen mode Exit fullscreen mode

But 1 means very little to a business user.

We therefore transform the prediction into something understandable:

business_prediction = {
    "customer_status": "Likely to Churn",
    "probability": "81%",
    "risk_level": "High"
}
Enter fullscreen mode Exit fullscreen mode

We can go further and connect it to a business action:

business_action = {
    "risk_level": "High",
    "recommended_action": (
        "Prioritise customer for retention outreach."
    )
}
Enter fullscreen mode Exit fullscreen mode

This is ultimately why machine learning systems are built.

Not simply to produce numbers.

But to support decisions.


Connecting FastAPI to the User Interface

The final architecture will look like this:

USER

CUSTOMER CHURN UI

POST /predict

FASTAPI

PYDANTIC VALIDATION

PREPROCESSING PIPELINE

MACHINE LEARNING MODEL

CHURN PROBABILITY

JSON RESPONSE

CUSTOMER CHURN UI

USER

The frontend and machine learning model are therefore separated.

The frontend is responsible for presentation.

FastAPI is responsible for communication and serving.

The machine learning pipeline is responsible for prediction.


Separation of Responsibilities

A clean architecture might look conceptually like this:

system_components = {

    "frontend": {
        "responsibility": (
            "Collect user input and display results"
        )
    },

    "fastapi": {
        "responsibility": (
            "Receive requests, validate input "
            "and return responses"
        )
    },

    "machine_learning_pipeline": {
        "responsibility": (
            "Transform data and generate predictions"
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

This separation is one of the principles behind maintainable production systems.


Testing the API

A professional API should also be testable.

For example:

from fastapi.testclient import TestClient

from app.main import app


client = TestClient(
    app
)


def test_health_endpoint():

    response = client.get("/")

    assert response.status_code == 200


def test_prediction_endpoint():

    customer = {
        "tenure": 12,
        "monthly_charges": 89.50,
        "contract": "Month-to-month",
        "internet_service": "Fiber optic",
        "payment_method": "Electronic check",
        "paperless_billing": "Yes",
        "senior_citizen": "No",
        "dependents": "No"
    }

    response = client.post(
        "/predict",
        json=customer
    )

    assert response.status_code == 200

    result = response.json()

    assert "prediction" in result
    assert "churn_probability" in result
Enter fullscreen mode Exit fullscreen mode

This allows us to verify that our API continues to behave correctly as the application evolves.


What Makes This Different From Running a Notebook?

Inside a notebook:

prediction = model.predict(
    customer_data
)
Enter fullscreen mode Exit fullscreen mode

Only your Python environment can easily interact with the model.

After deployment:

application = "Any authorised client"

endpoint = "/predict"

response = "JSON"
Enter fullscreen mode Exit fullscreen mode

Now the model can potentially serve:

  • Web applications
  • Mobile applications
  • CRM systems
  • Internal company applications
  • Other APIs
  • Data platforms

That is why deployment is such an important machine learning skill.


Model Development vs Model Serving

It is useful to distinguish between two separate activities.

Model Development

model_development = [
    "Explore data",
    "Clean data",
    "Engineer features",
    "Experiment",
    "Train models",
    "Evaluate models",
    "Select model"
]
Enter fullscreen mode Exit fullscreen mode

Model Serving

model_serving = [
    "Load trained model",
    "Receive request",
    "Validate input",
    "Transform data",
    "Generate prediction",
    "Return response",
    "Serve application"
]
Enter fullscreen mode Exit fullscreen mode

FastAPI lives primarily in the serving layer.

This is the bridge between machine learning and software engineering.


The Bigger Picture

The eventual production architecture may become much larger.

For example:

production_architecture = {
    "client": "Web or mobile application",
    "api_gateway": "API access layer",
    "ml_api": "FastAPI prediction service",
    "model": "Customer churn pipeline",
    "database": "Prediction and customer records",
    "monitoring": "Performance and health monitoring",
    "logging": "Application and prediction logs"
}
Enter fullscreen mode Exit fullscreen mode

A more advanced system might eventually include:

  • Docker
  • PostgreSQL
  • Authentication
  • Cloud deployment
  • CI/CD
  • Monitoring
  • Model versioning
  • Logging
  • Drift detection
  • Retraining pipelines
  • Kubernetes
  • MLOps platforms

But those systems all build on the same fundamental concept we are learning:

How do we make a trained machine learning model accessible to another application?


What Participants Will Learn.

By the end of the workshop, participants should understand:

Python Foundations
  • Functions
  • Classes
  • Decorators
  • How these concepts appear inside FastAPI
API Fundamentals
  • What an API is
  • Why APIs are important
  • Requests and responses
  • GET requests
  • POST requests
  • JSON
  • API endpoints
FastAPI.
  • Creating a FastAPI application
  • Defining routes
  • Creating request models
  • Using Pydantic
  • Input validation
  • Returning responses
  • Interactive Swagger documentation
Machine Learning Deployment
  • Saving trained models
  • Loading trained models
  • Using Scikit-learn pipelines
  • Serving predictions
  • Returning prediction probabilities
  • Creating /predict
Integration
  • Sending UI data to FastAPI
  • Receiving prediction responses
  • Displaying model predictions
  • Understanding frontend/backend separation

What You Should Be Able to Explain After the Workshop

By the end, you should be able to explain this entire lifecycle:

Jupyter Notebook

Machine Learning Pipeline

Trained Model

Saved Model Artifact

FastAPI Application

Prediction Endpoint

Frontend Application

Real User

Or, in Python:

complete_ml_journey = [
    "Notebook",
    "Model Training",
    "Model Evaluation",
    "Model Persistence",
    "FastAPI",
    "REST API",
    "Prediction Endpoint",
    "Frontend Integration",
    "Real-World User"
]
Enter fullscreen mode Exit fullscreen mode

From Experiment to Product

There is a significant difference between saying:

I trained a machine learning model.

and saying:

I developed and evaluated a machine learning pipeline, persisted the trained model, built a FastAPI service around it, implemented request validation, exposed prediction endpoints, returned structured responses, tested the service, and connected the model to a user-facing application.

The first demonstrates knowledge of machine learning, while the second demonstrates an understanding of how machine learning becomes part of a software system.

That transition is important for anyone interested in:

  • Machine Learning Engineering
  • Data Science
  • MLOps
  • Backend Development
  • Applied AI
  • Production Machine Learning

Final Thought.

A machine learning model sitting inside a notebook is an experiment, a saved model is an artifact, while model exposed through an API becomes a service.

And a model connected to a usable interface begins to become a product.

machine_learning_evolution = {
    "stage_1": "Experiment",
    "stage_2": "Model",
    "stage_3": "API",
    "stage_4": "Application",
    "stage_5": "Business Value"
}
Enter fullscreen mode Exit fullscreen mode

That is the journey we will explore in this workshop.

From Notebook → To API → To Application → To Real-World Impact

Top comments (0)