Build an End-to-End ML Pipeline
Train, evaluate, and serve a classification model via a FastAPI REST API.
Introduction
30 Second Summary
Every time you open your email and spam gets filtered out, or a medical app flags something unusual in a scan, a trained machine learning model is working behind the scenes. But a model sitting in a notebook is useless on its own. It needs a way for other applications to send it data and get predictions back.
In this project, you will build a complete ML pipeline that trains classification models on a real medical dataset, compares their performance, saves the best one, and serves predictions through a REST API.
What You'll Build
A full end-to-end ML pipeline that goes from raw data to a locally-running prediction API, accepting tumor measurements and returning malignant or benign classifications with confidence scores.
By the end of this project, you'll have:
- A training script that preprocesses data, trains two classifiers, compares them with proper metrics, and saves the best performer.
- A FastAPI prediction API that loads the trained model at startup and returns real-time predictions with confidence scores.
- Interactive Swagger UI documentation for testing your prediction endpoint without writing any client code.
- Secret Mission: Add batch prediction and model metadata endpoints for production readiness.
Are there any prerequisites?
You should be comfortable with Python fundamentals like functions, classes, and importing packages. Basic familiarity with ML concepts (what a model is, what training means) is helpful but not required.
Before We Start
Before We Start...
Set Up Your Environment
Every ML project starts with a clean, isolated workspace. A virtual environment keeps your project's dependencies separate from the rest of your system, so nothing clashes when you install ML libraries alongside web frameworks.
In this step, you'll create a project folder, spin up a virtual environment, and install the three core packages for your pipeline: a machine learning library, an API framework, and a web server.
In this step, get ready to:
- Create a project directory and virtual environment.
- Install ML and API dependencies.
- Set up the project file structure.
Create a project directory and virtual environment
You need a dedicated folder on your Desktop for this project, plus a virtual environment to isolate your Python packages.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ml-pipeline
cd ml-pipeline
- Create a virtual environment by running this command:
python -m venv venv
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install here won't affect any other project on your machine.
The venv folder it creates contains its own python binary and pip, ready for this project's dependencies.
- Activate the virtual environment so your terminal uses the isolated Python:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
- Verify your Python version by running:
python --version
You should see Python 3.14.4 (or your installed 3.14.x version) printed in the terminal. Your prompt should also show (venv) at the beginning, confirming the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activation command for your operating system. On macOS, use source venv/bin/activate (not just the path alone). On Windows PowerShell, you may need to allow script execution first by running Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned then retrying the activate command.
help me activate my Python virtual environment
Install dependencies
Your pipeline needs three packages: scikit-learn for training ML models, FastAPI for building the prediction API, and uvicorn for running the web server.
- Install all required packages by running this command:
pip install scikit-learn==1.9.0 "fastapi[standard]==0.139.0" uvicorn==0.49.0
What are these packages?
scikit-learn is the most widely used Python library for classical machine learning. It provides ready-to-use algorithms for classification, regression, and more.
fastapi[standard] installs FastAPI plus its recommended extras (including a development server). uvicorn is the ASGI server that actually runs your FastAPI app.
- Verify all packages installed correctly by running:
python -c "import sklearn; import fastapi; import uvicorn; print('All packages installed successfully')"
You should see All packages installed successfully printed with no errors.
Seeing an import error?
If you see ModuleNotFoundError, check that (venv) appears in your terminal prompt. If it doesn't, your virtual environment isn't active and pip installed the packages elsewhere. Activate the environment again, then re-run the pip install command.
help me fix my pip install in my virtual environment
Create the project file structure
Your project needs two Python files and a directory to store the trained model artifact.
- Create the two Python files and the model storage directory by running these commands:
macOS
touch train.py app.py
mkdir models
Windows
New-Item train.py, app.py -ItemType File
mkdir models
What are these files for?
train.py is where you'll build the model training pipeline. It will load data, train classifiers, compare them, and save the best one.
app.py is where you'll build the FastAPI prediction server. It loads the saved model and exposes an endpoint for real-time predictions.
The models/ directory stores the trained model file so the API can load it at startup.
- Confirm the files and folder exist by running:
ls
You should see app.py, models, train.py, and venv in the output.
Missing files in the output?
Make sure you ran the file creation commands from inside the ml-pipeline folder. Run pwd (macOS) or Get-Location (Windows) to check your current directory. The path should end with ml-pipeline.
help me find where my project files were created
Your environment is ready to go. Next up, you'll write the training pipeline that loads real medical data, trains two competing classifiers, and saves the winner.
Build the Training Pipeline
Your environment is ready, and now comes the core of any ML project: training a model that can actually make predictions.
In this step, you will build a training script that loads real medical data, trains two different classifiers, compares their performance, and saves the winner. By the end, you will have a persisted model artifact ready to be served by an API.
In this step, get ready to:
- Load and explore the Breast Cancer Wisconsin dataset.
- Train and evaluate a Random Forest and Gradient Boosting classifier.
- Save the best-performing model to disk for later use.
Load and explore the dataset
Before training a model, you need data to learn from. The Breast Cancer Wisconsin dataset is a classic binary classification dataset that contains 569 tumor samples with 30 numeric features each.
- Open train.py in your code editor.
- Paste the following code to import your dependencies, load the dataset, print exploration info, and split it into training and test sets:
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from joblib import dump
import json
import os
def main():
# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target
feature_names = list(data.feature_names)
target_names = list(data.target_names)
print(f"Dataset: {data.DESCR.splitlines()[0]}")
print(f"Samples: {X.shape[0]}, Features: {X.shape[1]}")
print(f"Classes: {target_names}")
print(f"Class distribution: malignant={sum(y == 0)}, benign={sum(y == 1)}")
print("-" * 50)
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
What does this code do?
load_breast_cancer() loads a built-in scikit-learn dataset with real tumor measurements. Each sample has 30 features (radius, texture, perimeter, etc.) and a label indicating malignant or benign.
train_test_split() splits your data into 80% training and 20% testing. The random_state=42 ensures you get the same split every time you run the script, making results reproducible.
- Save train.py.
- You won't see a change yet. The next chunk adds the training logic that makes this function complete.
Train and compare two models
A good ML engineer does not just pick one model and hope for the best. You will train two different classifiers and let the metrics decide which one wins.
Why Random Forest vs Gradient Boosting?
Both are tree-based ensemble models, but they use fundamentally different strategies. Random Forest trains many trees in parallel and averages their votes (bagging). Gradient Boosting trains trees sequentially, where each new tree corrects the mistakes of the previous ones (boosting).
Comparing both teaches you to reason about model selection based on metrics rather than gut feeling.
- In train.py, add the following code directly below the train_test_split call (still inside the main() function):
# Train models
models = {
"Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
"Gradient Boosting": GradientBoostingClassifier(
n_estimators=100, learning_rate=0.1, random_state=42
),
}
results = {}
for name, model in models.items():
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
results[name] = {
"model": model,
"accuracy": accuracy_score(y_test, y_pred),
"precision": precision_score(y_test, y_pred),
"recall": recall_score(y_test, y_pred),
"f1": f1_score(y_test, y_pred),
}
# Print comparison
print(f"\n{'Model':<25} {'Accuracy':<12} {'Precision':<12} {'Recall':<12} {'F1':<12}")
print("=" * 73)
for name, metrics in results.items():
print(
f"{name:<25} {metrics['accuracy']:<12.4f} {metrics['precision']:<12.4f} "
f"{metrics['recall']:<12.4f} {metrics['f1']:<12.4f}"
)
What does this code do?
- The models dictionary holds both classifiers so you can loop through and train them identically.
- model.fit() trains each model on the training set. model.predict() generates predictions on the held-out test set.
- Four metrics are calculated: accuracy (overall correctness), precision (of predicted positives, how many are correct), recall (of actual positives, how many were found), and F1 (harmonic mean of precision and recall).
- The comparison table prints all four metrics side-by-side so you can visually compare the models.
- Save train.py.
- You won't see output yet. The next chunk adds model selection and the script's entry point.
Save the best model
Training a model is only useful if you can reuse it later without retraining. Model persistence saves your trained model to a file so your API can load it instantly at startup.
- In train.py, add the following code below the comparison printing loop (still inside main()), plus the entry point at the bottom of the file:
# Select best model by F1 score
best_name = max(results, key=lambda k: results[k]["f1"])
best_model = results[best_name]["model"]
best_metrics = results[best_name]
print(f"\nBest model: {best_name} (F1: {best_metrics['f1']:.4f})")
# Save model and artifacts
os.makedirs("models", exist_ok=True)
dump(best_model, "models/model.joblib")
dump(feature_names, "models/feature_names.joblib")
# Save metadata
metadata = {
"model_type": best_name,
"accuracy": best_metrics["accuracy"],
"precision": best_metrics["precision"],
"recall": best_metrics["recall"],
"f1_score": best_metrics["f1"],
"n_features": len(feature_names),
"target_names": target_names,
}
with open("models/metadata.json", "w") as f:
json.dump(metadata, f, indent=2)
print(f"Model saved to models/model.joblib")
print(f"Metadata saved to models/metadata.json")
if __name__ == "__main__":
main()
What does this code do?
- max(results, key=lambda k: results[k]["f1"]) picks the model with the highest F1 score. F1 balances precision and recall, making it a better single metric than accuracy alone for medical data.
- dump() from joblib serializes the trained model and feature names to .joblib files. scikit-learn recommends joblib over pickle for efficiency with large NumPy arrays.
- The metadata dictionary captures training metrics in a JSON file so your API can report model info without retraining.
- if __name__ == "__main__" ensures main() only runs when you execute the script directly, not when importing from it.
- Save train.py.
- Run the training script by executing this command in your terminal:
python train.py
- You should see a dataset summary, a comparison table with accuracy, precision, recall, and F1 for both models, and confirmation that the model was saved.
What should I see?
Your terminal output shows 569 samples with 30 features. The comparison table lists both Random Forest and Gradient Boosting with their metrics. The script then announces the best model and confirms it saved to models/model.joblib.
- Confirm the model files were created by running:
ls models/
- You should see model.joblib, feature_names.joblib, and metadata.json in the output.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
- Check for typos in your import statements. The imports use sklearn (not scikit-learn) and joblib (not job_lib).
- If you see an IndentationError, make sure all the code inside main() is indented with 4 spaces.
Help me debug my train.py script
✔️ Awesome, I've got everything!
Great. Double check you have saved train.py and that you see three files in your models/ directory.
ⓧ I'd like to double check the full code
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from joblib import dump
import json
import os
def main():
# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target
feature_names = list(data.feature_names)
target_names = list(data.target_names)
print(f"Dataset: {data.DESCR.splitlines()[0]}")
print(f"Samples: {X.shape[0]}, Features: {X.shape[1]}")
print(f"Classes: {target_names}")
print(f"Class distribution: malignant={sum(y == 0)}, benign={sum(y == 1)}")
print("-" * 50)
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train models
models = {
"Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
"Gradient Boosting": GradientBoostingClassifier(
n_estimators=100, learning_rate=0.1, random_state=42
),
}
results = {}
for name, model in models.items():
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
results[name] = {
"model": model,
"accuracy": accuracy_score(y_test, y_pred),
"precision": precision_score(y_test, y_pred),
"recall": recall_score(y_test, y_pred),
"f1": f1_score(y_test, y_pred),
}
# Print comparison
print(f"\n{'Model':<25} {'Accuracy':<12} {'Precision':<12} {'Recall':<12} {'F1':<12}")
print("=" * 73)
for name, metrics in results.items():
print(
f"{name:<25} {metrics['accuracy']:<12.4f} {metrics['precision']:<12.4f} "
f"{metrics['recall']:<12.4f} {metrics['f1']:<12.4f}"
)
# Select best model by F1 score
best_name = max(results, key=lambda k: results[k]["f1"])
best_model = results[best_name]["model"]
best_metrics = results[best_name]
print(f"\nBest model: {best_name} (F1: {best_metrics['f1']:.4f})")
# Save model and artifacts
os.makedirs("models", exist_ok=True)
dump(best_model, "models/model.joblib")
dump(feature_names, "models/feature_names.joblib")
# Save metadata
metadata = {
"model_type": best_name,
"accuracy": best_metrics["accuracy"],
"precision": best_metrics["precision"],
"recall": best_metrics["recall"],
"f1_score": best_metrics["f1"],
"n_features": len(feature_names),
"target_names": target_names,
}
with open("models/metadata.json", "w") as f:
json.dump(metadata, f, indent=2)
print(f"Model saved to models/model.joblib")
print(f"Metadata saved to models/metadata.json")
if __name__ == "__main__":
main()
Your model is trained, evaluated, and saved. Next up, you will build a FastAPI application that loads this model and serves predictions through a REST endpoint.
Build the Prediction API
Your training pipeline saved the best model to disk. Now you need a way for other applications to use that model without running the training script every time.
In this step, you will build a REST API using FastAPI that loads your trained model once at startup and serves predictions on demand through HTTP endpoints.
In this step, get ready to:
- Define Pydantic request and response schemas for type-safe input validation.
- Create the FastAPI application with a lifespan context manager to load the model at startup.
- Implement prediction and health-check endpoints.
Define Pydantic request and response schemas
Before writing any endpoint logic, you need to define the shape of data coming into and going out of your API. Pydantic models let FastAPI automatically validate incoming requests and generate documentation.
- Open app.py in your code editor and add the following imports, module-level dictionary, and schema classes:
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from joblib import load
# Dictionary to hold the loaded model and feature names
ml_models = {}
class PredictionRequest(BaseModel):
features: list[float]
class PredictionResponse(BaseModel):
prediction: str
confidence: float
probabilities: dict[str, float]
What does this code do?
The imports bring in everything you need: asynccontextmanager for the app lifecycle, FastAPI and HTTPException for the web framework, BaseModel for data validation, and load from joblib to deserialize your saved model.
The ml_models dictionary acts as a module-level container. Your model will be loaded into it once at startup and accessed from every endpoint afterward.
The two Pydantic classes define the API contract. PredictionRequest expects a list of 30 float values (one per feature). PredictionResponse guarantees every response includes the predicted class label, a confidence score, and the full probability distribution.
- Save app.py.
You will not see a visible change yet. The next chunk uses these schemas to build the running application.
Create the FastAPI application with lifespan
Loading a model from disk takes time. You want this to happen once when the server starts, not on every request. FastAPI's lifespan pattern handles this cleanly.
- In app.py, add the following code between the ml_models = {} line and the PredictionRequest class:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load the ML model and feature names at startup
ml_models["model"] = load("models/model.joblib")
ml_models["feature_names"] = load("models/feature_names.joblib")
yield
# Clean up
ml_models.clear()
app = FastAPI(
title="ML Prediction API",
description="Breast Cancer Classification API serving predictions from a trained scikit-learn model.",
version="1.0.0",
lifespan=lifespan,
)
What does this code do?
The lifespan function is an async context manager. Everything before the yield runs once when the server starts. Everything after runs when the server shuts down.
Before yield, you load the trained model and the feature names list from the models/ directory into the ml_models dictionary. After yield, ml_models.clear() frees memory on shutdown.
The app = FastAPI(...) line creates the application instance with metadata that populates the auto-generated documentation. Passing lifespan=lifespan connects the startup/shutdown logic.
Why the lifespan pattern instead of on_event?
Older FastAPI tutorials use @app.on_event("startup") decorators. Those are deprecated. The lifespan async context manager is the current recommended approach because it keeps startup and shutdown logic in one place and works correctly with modern ASGI servers.
- Save app.py.
You will not see a visible change yet. The next chunk adds endpoints that use the loaded model.
Not sure where to place this code?
The lifespan function and app instance go directly after ml_models = {} and before the two class definitions (PredictionRequest and PredictionResponse). Check the full-file tab at the end of this step if you are unsure.
help me figure out where to place the lifespan code in app.py
Implement the prediction endpoint
With the model loaded at startup and schemas defined, you can now expose endpoints that accept requests and return predictions.
- In app.py, add the health-check endpoint below the PredictionResponse class:
@app.get("/health")
async def health():
model_loaded = "model" in ml_models
return {"status": "healthy", "model_loaded": model_loaded}
What does this endpoint do?
The GET /health endpoint is a lightweight check that confirms the server is running and the model loaded successfully. Production systems use health endpoints for monitoring and load balancer checks.
- Save app.py.
You will not see a change yet. The prediction endpoint you add next is where the real inference happens.
- Add the prediction endpoint below the health function:
@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
model = ml_models["model"]
feature_names = ml_models["feature_names"]
# Validate that the request has the correct number of features
if len(request.features) != len(feature_names):
raise HTTPException(
status_code=422,
detail=f"Expected {len(feature_names)} features, got {len(request.features)}",
)
# Run inference
probabilities = model.predict_proba([request.features])[0]
prediction_idx = probabilities.argmax()
# Map prediction index to class label
target_names = ["malignant", "benign"]
prediction_label = target_names[prediction_idx]
confidence = float(probabilities[prediction_idx])
return PredictionResponse(
prediction=prediction_label,
confidence=confidence,
probabilities={
"malignant": float(probabilities[0]),
"benign": float(probabilities[1]),
},
)
What does this endpoint do?
The POST /predict endpoint accepts a JSON body with 30 float features. It first validates the feature count matches what the model expects.
Then it calls predict_proba to get probability estimates for each class. The class with the highest probability becomes the prediction label. The confidence score is the probability of the winning class.
The response_model=PredictionResponse parameter tells FastAPI to validate and document the output shape automatically.
- Save app.py.
- Start the development server by running:
uvicorn app:app --reload
- Open your browser and navigate to http://127.0.0.1:8000/docs.
You should see the interactive Swagger UI with your /health and /predict endpoints listed. Your terminal should show a line containing Application startup complete confirming the model loaded successfully.
Server not starting?
- Make sure you are running the command from inside the ml-pipeline folder where app.py lives.
- Confirm the models/ directory contains model.joblib and feature_names.joblib. If not, run python train.py first.
- Check that your virtual environment is active. You should see (venv) in your terminal prompt.
help me debug why uvicorn is not starting my FastAPI app
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and the Swagger UI is showing both endpoints.
ⓧ I'd like to double check the full code
Here is the complete app.py file at this point:
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from joblib import load
ml_models = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load the ML model and feature names at startup
ml_models["model"] = load("models/model.joblib")
ml_models["feature_names"] = load("models/feature_names.joblib")
yield
# Clean up
ml_models.clear()
app = FastAPI(
title="ML Prediction API",
description="Breast Cancer Classification API serving predictions from a trained scikit-learn model.",
version="1.0.0",
lifespan=lifespan,
)
class PredictionRequest(BaseModel):
features: list[float]
class PredictionResponse(BaseModel):
prediction: str
confidence: float
probabilities: dict[str, float]
@app.get("/health")
async def health():
model_loaded = "model" in ml_models
return {"status": "healthy", "model_loaded": model_loaded}
@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
model = ml_models["model"]
feature_names = ml_models["feature_names"]
if len(request.features) != len(feature_names):
raise HTTPException(
status_code=422,
detail=f"Expected {len(feature_names)} features, got {len(request.features)}",
)
probabilities = model.predict_proba([request.features])[0]
prediction_idx = probabilities.argmax()
target_names = ["malignant", "benign"]
prediction_label = target_names[prediction_idx]
confidence = float(probabilities[prediction_idx])
return PredictionResponse(
prediction=prediction_label,
confidence=confidence,
probabilities={
"malignant": float(probabilities[0]),
"benign": float(probabilities[1]),
},
)
Your prediction API is live and documented. Next up, you will test it with real data using both the Swagger UI and command-line tools.
Test Your Live API
In the previous step, you built the full prediction API with Pydantic validation and a lifespan-managed model loader.
Now it is time to prove that everything works end-to-end. You will start the server, send real tumor measurements, and confirm that the API returns meaningful predictions with confidence scores.
In this step, get ready to:
- Test the prediction endpoint using the interactive Swagger UI documentation.
- Send requests via curl from the command line and verify the response format.
- Confirm the health endpoint reports the model is loaded.
Test predictions using the Swagger UI
FastAPI automatically generates interactive API documentation at the /docs path. This lets you test endpoints directly in your browser without writing any client code.
- Start the API server by running this command:
uvicorn app:app --reload
What does this command do?
This starts uvicorn (the ASGI server) pointing at your app object inside app.py. The --reload flag watches for file changes and auto-restarts the server during development.
- You should see output indicating the server is running at http://127.0.0.1:8000.
- Open your browser and navigate to http://127.0.0.1:8000/docs.
You should see the Swagger UI page with your API title, description, and two endpoints listed.
Don't see the docs page?
- Make sure your terminal is inside the ml-pipeline folder and your virtual environment is activated.
- Check the terminal output for errors. If you see a ModuleNotFoundError, your virtual environment might not be active.
- If the model files are missing, run python train.py first to regenerate them.
Help me debug why my uvicorn server won't start.
- Click on the POST /predict endpoint to expand it.
- Click the Try it out button on the right side.
- Replace the example request body with this sample feature vector (30 tumor measurements):
{
"features": [
17.99, 10.38, 122.8, 1001.0, 0.1184, 0.2776, 0.3001, 0.1471,
0.2419, 0.07871, 1.095, 0.9053, 8.589, 153.4, 0.006399, 0.04904,
0.05373, 0.01587, 0.03003, 0.006193, 25.38, 17.33, 184.6, 2019.0,
0.1622, 0.6656, 0.7119, 0.2654, 0.4601, 0.1189
]
}
Where do these numbers come from?
These are real measurements from the Breast Cancer Wisconsin dataset. Each value represents a physical characteristic of a cell nucleus (radius, texture, perimeter, area, smoothness, and more). This particular sample has 30 features as your model expects.
- Click the Execute button.
You should see a response with a status code of 200 and a JSON body containing a prediction label, confidence score, and probability distribution.
What should the response look like?
You will see a JSON response with three fields. prediction shows either malignant or benign. confidence is a float between 0 and 1 indicating how certain the model is. probabilities shows the likelihood for each class, and they sum to 1.
Test with curl from the command line
Swagger UI is great for interactive exploration. In production, you would call your API programmatically. Let's verify the same endpoint works from the command line using curl.
- Open a second terminal window (keep the uvicorn server running in the first one).
- Activate your virtual environment in the new terminal.
- Send a prediction request by running this command:
curl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{"features": [17.99, 10.38, 122.8, 1001.0, 0.1184, 0.2776, 0.3001, 0.1471, 0.2419, 0.07871, 1.095, 0.9053, 8.589, 153.4, 0.006399, 0.04904, 0.05373, 0.01587, 0.03003, 0.006193, 25.38, 17.33, 184.6, 2019.0, 0.1622, 0.6656, 0.7119, 0.2654, 0.4601, 0.1189]}'
What does this command do?
- -X POST sets the HTTP method to POST.
- -H "Content-Type: application/json" tells the server you are sending JSON data.
- -d provides the JSON request body with the 30 feature values.
- Verify that the response contains a prediction field with either malignant or benign, a confidence score between 0 and 1, and probabilities that sum to 1.
- Test the health endpoint by running:
curl http://127.0.0.1:8000/health
- You should see a JSON response with "status": "healthy" and "model_loaded": true.
Getting a connection refused error?
- Make sure the uvicorn server is still running in your first terminal window.
- Check that you are using port 8000 in the curl URL.
- If you see a 422 error, double-check that your JSON includes exactly 30 values in the features array.
Help me fix my curl request to the prediction API.
Secret mission
Add Batch Predictions and Model Metadata
Your API handles one prediction at a time, but production systems need to process hundreds of samples in a single request. In this secret mission, you will add a batch prediction endpoint and a model metadata endpoint that exposes training metrics and feature importances.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- ml-pipeline project directory (includes train.py, app.py, and the models/ directory with trained model artifacts).
- Python virtual environment (venv folder inside ml-pipeline).
- Running uvicorn development server.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the API.
- Your uvicorn server continues running in the terminal. You can send requests to it anytime.
- Your trained model artifacts remain in the models/ directory, ready for the API to load.
- Your virtual environment stays active with all packages installed.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your files and virtual environment stay on disk so you can resume later.
- Switch to the terminal where uvicorn is running and press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
When you come back, navigate to your ml-pipeline folder, reactivate the virtual environment, and start the server again.
Delete - I don't want to use this again
Remove all project files and the virtual environment completely.
- Switch to the terminal where uvicorn is running and press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
- Navigate to your Desktop and delete the entire project folder.
On macOS/Linux:
cd ~/Desktop
rm -rf ml-pipeline
On Windows (PowerShell):
cd ~\Desktop
Remove-Item -Recurse -Force ml-pipeline
This removes all Python files, the trained model, metadata, and the virtual environment.
Nice Work!
Nice Work!
You just built a complete end-to-end ML pipeline. From raw medical data to a live prediction API, you now have hands-on experience with the full ML Engineer lifecycle.
You've learned how to:
- Train and compare Random Forest and Gradient Boosting classifiers using proper evaluation metrics (accuracy, precision, recall, F1) and persist the best model with joblib.
- Build a production-style FastAPI REST API with Pydantic validation that loads a trained model at startup and serves real-time predictions.
- Test a live ML API using Swagger UI interactive documentation and curl from the command line.
- Secret Mission: Added batch prediction and model metadata endpoints for production readiness.
Ready to quiz yourself?