First FastAPI route — POST /predict
Load model.pkl once at startup. Expose POST /predict that takes JSON input and returns a predicted score.
🧮 Code files for this stage
from fastapi import FastAPI
from pydantic import BaseModel, Field
import joblib
import pandas as pd
import uvicorn
app = FastAPI(title='Student Performance Predictor')
# ── Load model ONCE at startup ──────────────────────────
# Don't load per request — would be 100x slower.
model = joblib.load('app/ml/model.pkl')
# ── Pydantic models (request + response) ───────────────
class StudentInput(BaseModel):
study_hours: float = Field(..., ge=0, le=24)
previous_score: float = Field(..., ge=0, le=100)
attendance: float = Field(..., ge=0, le=100)
assignments_completed: int = Field(..., ge=0, le=20)
sleep_hours: float = Field(..., ge=0, le=24)
extracurricular_hours: float = Field(..., ge=0, le=10)
parental_support: int = Field(..., ge=1, le=5)
class PredictResponse(BaseModel):
predicted_score: float
performance_band: str # 'Poor' / 'OK' / 'Good' / 'Excellent'
# ── Route: POST /predict ────────────────────────────────
@app.post('/predict', response_model=PredictResponse)
async def predict(student: StudentInput):
# Pydantic already validated the input. Build a 1-row DataFrame
# with the EXACT same column order as training.
X_new = pd.DataFrame([{
'study_hours': student.study_hours,
'previous_score': student.previous_score,
'attendance': student.attendance,
'assignments_completed': student.assignments_completed,
'sleep_hours': student.sleep_hours,
'extracurricular_hours': student.extracurricular_hours,
'parental_support': student.parental_support,
}])
score = float(model.predict(X_new)[0])
# Bucket into a performance band for the UI
if score < 50: band = 'Poor'
elif score < 70: band = 'OK'
elif score < 85: band = 'Good'
else: band = 'Excellent'
return PredictResponse(predicted_score=round(score, 1),
performance_band=band)
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
# Start the server
python app/main.py &
# Test with curl
curl -X POST http://localhost:8000/predict \
-H 'Content-Type: application/json' \
-d '{
"study_hours": 6,
"previous_score": 72,
"attendance": 85,
"assignments_completed": 8,
"sleep_hours": 7,
"extracurricular_hours": 2,
"parental_support": 3
}'
# Response:
# {"predicted_score":76.4,"performance_band":"Good"}
# Try invalid input — FastAPI returns 422 automatically:
curl -X POST http://localhost:8000/predict \
-H 'Content-Type: application/json' \
-d '{"study_hours": 999}'
# -> 422 Unprocessable Entity (study_hours must be <= 24)
How it works
Three big ideas in this route: (1) LOAD MODEL ONCE — `model = joblib.load(...)` runs at module import time, not inside the route. If you loaded it per request, every prediction would take 500ms instead of 5ms. (2) Pydantic input — StudentInput validates input. If a user sends study_hours=999, FastAPI returns 422 automatically — your route code never runs. You never write validation code; Pydantic does it. (3) response_model=PredictResponse — FastAPI serializes the return value AND validates the output shape. If your route returns a NaN, FastAPI catches it instead of sending 'NaN' as JSON (which is invalid JSON and breaks the client).
Yes — and here's why. Pydantic response models do THREE jobs at once: 1. VALIDATION of the output -> if your model returns a NaN, FastAPI catches it 2. DOCUMENTATION -> /docs shows the exact shape of the response 3. SERIALIZATION -> Python float -> JSON number automatically Without a response model, FastAPI would still return JSON — but you'd lose validation and docs. For a toy project, you can skip it. For anything real, always declare a response_model. It's 2 lines of code and pays for itself the first time a bug returns the wrong shape.
- Loading the model inside the route — every request reloads the .pkl, 100x slower.
- Not using response_model — you lose validation, lose /docs, and risk returning NaN as JSON.
- Building the input DataFrame with columns in a different ORDER than training — sklearn will silently give wrong predictions. Always match training column order exactly.