# I Built My First Machine Learning API — Here's Everything I Learned

> Source: <https://dev.to/ericmwaimiri/i-built-my-first-machine-learning-api-heres-everything-i-learned-1f7>
> Published: 2026-08-13 22:49:39+00:00

Predicts whether a telecom customer will churn using the Telco Customer Churn dataset. The project covers data cleaning and EDA, comparison of three classifiers, and a FastAPI service that serves the final model.

```
.
├── API/
│   └── main.py                                 # FastAPI app serving the model
├── Data/
│   └── WA_Fn-UseC_-Telco-Customer-Churn.csv    # raw dataset (7,043 customers)
├── Models/
│   ├── ml_pipeline.joblib                      # fitted preprocessing + classifier pipeline
│   └── target_labels.joblib                    # LabelEncoder for the churn target
├── Notebooks/
│   └── customer_churn_prediction.ipynb         # data cleaning, EDA, model training
├── cpp/                                        # (Python virtual environment)
├── .env                                        # environment variables (not committed)
├── .gitignore
├── requirements.txt
└── README.md
```

`main.py`

loads `../models/ml_pipeline.joblib`

and `../models/target_labels.joblib`

,
so it must be run from inside `API/`

for those relative paths to resolve.

The raw data is the [Telco Customer Churn dataset](https://www.kaggle.com/datasets/blastchar/telco-customer-churn)
7,043 rows and 21 columns covering customer demographics, account…

If you've ever trained a model in a Jupyter notebook and then wondered "okay... now what?" — this post is for you. I recently built a **Customer Churn Prediction API**, and I want to walk you through it the way I wish someone had walked me through my first one: no jargon dump, just what each piece does and why it's there.

Think of it like a Safaricom customer care team trying to figure out **which subscribers are about to switch to Airtel** — before they actually leave. That's churn prediction. And instead of leaving that insight trapped inside a notebook, I turned it into an API anyone (or any app) can call.

I started, like most people do, in a Jupyter notebook. I had a dataset of telecom customers — their contract type, monthly charges, whether they had streaming services, and so on — with a label saying whether they churned (left) or not.

I cleaned the data, explored it, trained a few models, and picked a winner. Cool. But here's the thing: **a trained model sitting in a notebook is useless to anyone else.** A frontend developer can't "import your notebook" into an app. A business analyst can't click a button in your `.ipynb`

file to get a prediction.

That's the gap an API fills.

Before any API talk, the notebook had to do its job. Here's the flow, in plain English:

`gender`

, `tenure`

, `Contract`

, `MonthlyCharges`

, and the target column, `Churn`

(Yes/No).`TotalCharges`

becomes `totalcharges`

), and a numeric column that was secretly stored as text gets converted properly. A handful of broken rows get dropped.`tenure`

and `MonthlyCharges`

) get scaled, and categorical columns (like `Contract`

or `PaymentMethod`

) get one-hot encoded, because models only understand numbers.`joblib`

, I saved the entire fitted pipeline (preprocessing + model, bundled together) plus the label encoder, so predictions later come back as "Yes"/"No" instead of `1`

/`0`

.That last step is the bridge to everything that follows. Once your pipeline is saved to disk, it stops being "notebook code" and becomes a **reusable artifact** — a file any Python program can load and use.

An API is just a way for two programs to talk to each other over the web, using a set of agreed-upon rules. You send it something, it sends something back.

For this project, the deal is simple:

"Send me a customer's details, and I'll tell you whether they're likely to churn."

That's it. No magic. Just a structured request in, a structured response out.

I used [FastAPI](https://fastapi.tiangolo.com/), a Python framework built specifically for creating APIs quickly, with automatic validation and interactive documentation baked in.

Here's the skeleton of what's happening in `main.py`

:

``` python
import joblib
from fastapi import FastAPI
from pydantic import BaseModel
import pandas as pd

# Load the model and target labels
labels = joblib.load('../models/target_labels.joblib')
model = joblib.load('../models/ml_pipeline.joblib')

app = FastAPI(
    title="Customer Churn Prediction API",
    description="An API for predicting customer churn using a pre-trained machine learning model."
)
```

Two things happen right at the top, before the API even starts handling requests:

`ml_pipeline.joblib`

) is loaded into memory. This is the exact same preprocessing + model combo we saved from the notebook.`target_labels.joblib`

) is loaded too, so we can turn a `0`

/`1`

prediction back into a human-readable "No"/"Yes".This only happens **once**, when the server starts — not on every request. That's important for speed.

This is the part that impressed me most as a beginner. FastAPI uses something called **Pydantic** to define exactly what shape your input data must be:

```
class InputData(BaseModel):
    gender: str
    seniorcitizen: int
    partner: str
    dependents: str
    tenure: int
    phoneservice: str
    multiplelines: str
    internetservice: str
    onlinesecurity: str
    onlinebackup: str
    deviceprotection: str
    techsupport: str
    streamingtv: str
    streamingmovies: str
    contract: str
    paperlessbilling: str
    paymentmethod: str
    monthlycharges: float
    totalcharges: float
```

Every field here mirrors a column the model was trained on. If someone sends `tenure`

as `"twelve"`

instead of `12`

, FastAPI rejects the request automatically — before it ever reaches your model. You don't write a single line of manual validation code. That alone saved me from a bunch of silent bugs.

An API exposes "endpoints" — specific URLs that do specific things. This project has two.

**A health check**, so you (or a monitoring tool) can confirm the API is alive:

``` python
@app.get("/health", tags=["Health Check"])
def health_check():
    return {"status": "API is running"}
```

**The actual prediction endpoint**:

``` python
@app.post("/predict", tags=["Prediction"])
def predict_churn(payload: InputData):
    # Convert input data to DataFrame
    input_df = pd.DataFrame([payload.model_dump()])

    # Make prediction
    prediction = model.predict(input_df)

    # Decode the prediction
    decoded_prediction = labels.inverse_transform(prediction)

    return {"prediction": decoded_prediction[0]}
```

Walking through it:

`payload`

.`payload.model_dump()`

turns it into a plain dictionary, which gets wrapped into a one-row pandas DataFrame — because that's the format the trained pipeline expects.`model.predict(...)`

runs that row through the `0`

or `1`

), so `labels.inverse_transform(...)`

converts it back into `"No"`

or `"Yes"`

.That's the whole loop: **JSON in → DataFrame → pipeline → prediction → JSON out.**

Once you run:

```
uvicorn main:app --reload
```

FastAPI gives you a free interactive docs page at `http://127.0.0.1:8000/docs`

, where you can literally fill in a form and hit "Execute" to test `/predict`

without writing a single line of client code. As a beginner, this is where it finally clicked for me — seeing my model respond to a request in real time made it feel like a real product, not just a school notebook exercise.

Send it something like:

```
{
  "gender": "Female",
  "seniorcitizen": 0,
  "partner": "Yes",
  "dependents": "No",
  "tenure": 1,
  "phoneservice": "No",
  "multiplelines": "No phone service",
  "internetservice": "DSL",
  "onlinesecurity": "No",
  "onlinebackup": "Yes",
  "deviceprotection": "No",
  "techsupport": "No",
  "streamingtv": "No",
  "streamingmovies": "No",
  "contract": "Month-to-month",
  "paperlessbilling": "Yes",
  "paymentmethod": "Electronic check",
  "monthlycharges": 29.85,
  "totalcharges": 29.85
}
```

And you get back:

```
{"prediction": "No"}
```

Training a model answers the question "does this work?" Building an API answers a completely different question: **"can anyone else use this?"**

Those are two separate skills, and honestly, the second one felt more like real software engineering — dependency management, input validation, documentation, deployment thinking — than the modeling part did. If you've been putting off learning how to serve your models, don't. It's a smaller leap than it looks, and FastAPI in particular makes it a genuinely beginner-friendly one.

If you're working through something similar — maybe predicting M-Pesa transaction fraud, or matatu route demand — the pattern is identical: train, save the pipeline, load it in an API, validate input, predict, respond. Once you've done it once, you'll do it in your sleep.

*Have you built your first ML API yet? What tripped you up the most — validation, deployment, or something else entirely? Let me know in the comments.*
