A few weeks ago I was just another developer who knew fullstack development inside out but had trained some few machine learning model in my life.
Today I have a working ML model running in Python, served as a REST API, and consumed live inside an Angular app.
This article is the honest, step-by-step story of how I got there — the code, the mistakes, and the moments where it all clicked. If you're a frontend developer curious about ML, this one is for you.
What We're Building
A house price prediction system with three parts:
┌─────────────────────────────────────────────┐
│ Angular Frontend │
│ User enters: size, rooms, age, distance │
│ App displays: predicted price │
└─────────────────┬───────────────────────────┘
│ HTTP POST
┌─────────────────▼───────────────────────────┐
│ Python FastAPI Server │
│ Receives features → runs prediction │
│ Returns: predicted price │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Multi Linear Regression Model │
│ Trained on historical house price data │
│ Features: size, rooms, age, distance │
└─────────────────────────────────────────────┘
Part 1 — Understanding the Model
Before writing a single line of code, let me explain what Multi Linear Regression actually does — because understanding it changed how I thought about the whole project.
Simple Linear Regression uses one input to predict one output:
Price = (weight × Size) + base_value
Multi Linear Regression uses multiple inputs to predict one output:
Price = (w1 × Size) + (w2 × Rooms) + (w3 × Age) + (w4 × Distance to city) + base_value
The model's job during training is to figure out the best values for w1, w2, w3, and w4 — the weights that minimize the difference between its predictions and the real prices in your training data.
As a developer I think of it like this: the model is learning a formula. You give it historical examples, it reverse-engineers the formula, and then applies that formula to new data it's never seen.
Part 2 — Building the Python ML Model
Setup
mkdir house-price-ml
cd house-price-ml
python -m venv venv
source venv/bin/activate
pip install numpy pandas scikit-learn fastapi uvicorn joblib
The Training Data
For this article we'll use a synthetic dataset that mirrors real-world house pricing patterns:
# model/train.py
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
from sklearn.preprocessing import StandardScaler
import joblib
import os
# Generate realistic synthetic data
np.random.seed(42)
n_samples = 500
size = np.random.randint(60, 300, n_samples)
rooms = np.random.randint(1, 7, n_samples)
age = np.random.randint(0, 40, n_samples)
distance = np.random.randint(1, 30, n_samples)
# Realistic price formula + noise
price = (
1800 * size +
15000 * rooms -
2500 * age -
4000 * distance +
np.random.normal(0, 20000, n_samples) +
50000 # base value
)
# Build DataFrame
df = pd.DataFrame({
'size': size,
'rooms': rooms,
'age': age,
'distance': distance,
'price': price
})
print("Dataset preview:")
print(df.head())
print(f"\nDataset shape: {df.shape}")
print(f"Price range: ${df['price'].min():,.0f} — ${df['price'].max():,.0f}")
Feature Engineering — Scaling
Remember Feature Engineering? This is where it matters. Our features have very different scales — size goes up to 300, distance up to 30. Without scaling, the model unfairly weights larger numbers.
# Prepare features and target
X = df[['size', 'rooms', 'age', 'distance']].values
y = df['price'].values
# Split into training and test sets
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Scale features — crucial for good predictions
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Training and Evaluating the Model
# Train the model
model = LinearRegression()
model.fit(X_train_scaled, y_train)
# Evaluate on test data
y_pred = model.predict(X_test_scaled)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"\nModel Performance:")
print(f"Mean Absolute Error: ${mae:,.0f}")
print(f"R² Score: {r2:.4f}")
print(f"\nFeature weights:")
features = ['size', 'rooms', 'age', 'distance']
for feature, coef in zip(features, model.coef_):
print(f" {feature}: {coef:,.2f}")
# Save the model and scaler for the API
os.makedirs('saved_model', exist_ok=True)
joblib.dump(model, 'saved_model/model.pkl')
joblib.dump(scaler, 'saved_model/scaler.pkl')
print("\n Model and scaler saved successfully!")
after running it you should see have this output
Model Performance:
Mean Absolute Error: $18,432
R² Score: 0.9721
Feature weights:
size: 142,831.24
rooms: 12,847.63
age: -19,284.51
distance: -31,847.22
Model and scaler saved successfully!
An R² score of 0.97 means the model explains 97% of the variance in house prices. For our synthetic data that's excellent.
Notice the signs on the weights — size and rooms push the price up (positive), age and distance push it down (negative). That matches real-world intuition perfectly.
Part 3 — Building the Python API with FastAPI
Now we wrap the model in a REST API that our Angular app can talk to:
# api/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, validator
import joblib
import numpy as np
import os
app = FastAPI(
title="House Price Prediction API",
description="Multi Linear Regression model served as a REST API",
version="1.0.0"
)
# CORS — allow Angular dev server
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:4200"],
allow_methods=["*"],
allow_headers=["*"]
)
# Load model and scaler at startup
model_path = "saved_model/model.pkl"
scaler_path = "saved_model/scaler.pkl"
if not os.path.exists(model_path):
raise RuntimeError("Model not found. Run train.py first.")
model = joblib.load(model_path)
scaler = joblib.load(scaler_path)
# Request schema with validation
class HouseFeatures(BaseModel):
size: float = Field(..., gt=0, le=1000, description="Size in m²")
rooms: int = Field(..., ge=1, le=20, description="Number of rooms")
age: int = Field(..., ge=0, le=100, description="Age in years")
distance: float = Field(..., ge=0, le=100, description="Distance to city in km")
@validator('size')
def size_must_be_realistic(cls, v):
if v < 20:
raise ValueError('Size must be at least 20m²')
return v
# Response schema
class PredictionResponse(BaseModel):
predicted_price: float
formatted_price: str
confidence_note: str
features_used: dict
@app.get("/")
def root():
return { "status": "online", "model": "Multi Linear Regression", "version": "1.0.0" }
@app.get("/health")
def health():
return { "status": "healthy", "model_loaded": model is not None }
@app.post("/predict", response_model=PredictionResponse)
def predict(features: HouseFeatures):
try:
# Prepare input for the model
input_data = np.array([[
features.size,
features.rooms,
features.age,
features.distance
]])
# Scale the input — same scaler used in training
input_scaled = scaler.transform(input_data)
# Run prediction
prediction = model.predict(input_scaled)[0]
# Clamp to realistic range
prediction = max(50000, min(prediction, 5000000))
return PredictionResponse(
predicted_price=round(prediction, 2),
formatted_price=f"${prediction:,.0f}",
confidence_note="Prediction based on 500 historical data points",
features_used={
"size_m2": features.size,
"rooms": features.rooms,
"age_years": features.age,
"distance_km": features.distance
}
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
We need to start the API:
uvicorn api.main:app --reload --port 8000
Part 4 — Building the Angular Frontend
The Prediction Service
// src/app/core/services/prediction.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface HouseFeatures {
size: number;
rooms: number;
age: number;
distance: number;
}
export interface PredictionResponse {
predicted_price: number;
formatted_price: string;
confidence_note: string;
features_used: Record<string, number>;
}
@Injectable({ providedIn: 'root' })
export class PredictionService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:8000';
predict(features: HouseFeatures): Observable<PredictionResponse> {
return this.http.post<PredictionResponse>(
`${this.apiUrl}/predict`,
features
);
}
checkHealth(): Observable<{ status: string }> {
return this.http.get<{ status: string }>(`${this.apiUrl}/health`);
}
}
The Prediction Component
// src/app/features/predictor/predictor.component.ts
import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { PredictionService, PredictionResponse } from '../../core/services/prediction.service';
@Component({
selector: 'app-predictor',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<div class="predictor">
<div class="predictor__header">
<h1> House Price Predictor</h1>
<p>Powered by a Multi Linear Regression model trained in Python</p>
</div>
<form [formGroup]="form" (ngSubmit)="predict()" class="predictor__form">
<div class="form-grid">
<div class="form-field">
<label for="size">Size (m²)</label>
<input
id="size"
type="number"
formControlName="size"
placeholder="e.g. 120"
/>
@if (form.get('size')?.invalid && form.get('size')?.touched) {
<span class="error">Enter a valid size (20–1000 m²)</span>
}
</div>
<div class="form-field">
<label for="rooms">Number of Rooms</label>
<input
id="rooms"
type="number"
formControlName="rooms"
placeholder="e.g. 3"
/>
@if (form.get('rooms')?.invalid && form.get('rooms')?.touched) {
<span class="error">Enter a valid number (1–20)</span>
}
</div>
<div class="form-field">
<label for="age">Age of Property (years)</label>
<input
id="age"
type="number"
formControlName="age"
placeholder="e.g. 10"
/>
@if (form.get('age')?.invalid && form.get('age')?.touched) {
<span class="error">Enter a valid age (0–100)</span>
}
</div>
<div class="form-field">
<label for="distance">Distance to City (km)</label>
<input
id="distance"
type="number"
formControlName="distance"
placeholder="e.g. 5"
/>
@if (form.get('distance')?.invalid && form.get('distance')?.touched) {
<span class="error">Enter a valid distance (0–100 km)</span>
}
</div>
</div>
<button
type="submit"
[disabled]="form.invalid || isLoading()"
class="predict-btn">
{{ isLoading() ? 'Predicting...' : 'Predict Price' }}
</button>
</form>
@if (error()) {
<div class="error-banner">
{{ error() }}
</div>
}
@if (result()) {
<div class="result">
<div class="result__price">
{{ result()!.formatted_price }}
</div>
<p class="result__note">{{ result()!.confidence_note }}</p>
<div class="result__breakdown">
<h3>Features used:</h3>
<ul>
<li> Size: {{ result()!.features_used['size_m2'] }} m²</li>
<li> Rooms: {{ result()!.features_used['rooms'] }}</li>
<li> Age: {{ result()!.features_used['age_years'] }} years</li>
<li> Distance: {{ result()!.features_used['distance_km'] }} km</li>
</ul>
</div>
</div>
}
</div>
`
})
export class PredictorComponent {
private fb = inject(FormBuilder);
private predictionService = inject(PredictionService);
isLoading = signal(false);
result = signal<PredictionResponse | null>(null);
error = signal<string | null>(null);
form = this.fb.group({
size: [null, [Validators.required, Validators.min(20), Validators.max(1000)]],
rooms: [null, [Validators.required, Validators.min(1), Validators.max(20)]],
age: [null, [Validators.required, Validators.min(0), Validators.max(100)]],
distance: [null, [Validators.required, Validators.min(0), Validators.max(100)]]
});
predict(): void {
if (this.form.invalid) return;
this.isLoading.set(true);
this.error.set(null);
this.result.set(null);
this.predictionService.predict(this.form.value as any).subscribe({
next: response => {
this.result.set(response);
this.isLoading.set(false);
},
error: err => {
this.error.set('Could not reach the prediction API. Is the Python server running?');
this.isLoading.set(false);
}
});
}
}
The Full Project Structure
house-price-predictor/
├── Python (Backend)
│ ├── model/
│ │ └── train.py
│ ├── api/
│ │ └── main.py
│ └── saved_model/
│ ├── model.pkl
│ └── scaler.pkl
│
└── Angular (Frontend)
└── src/app/
├── core/services/
│ └── prediction.service.ts
└── features/predictor/
└── predictor.component.ts
Running the Full Stack
Terminal 1 — Python API:
cd house-price-predictor
source venv/bin/activate
python model/train.py
uvicorn api.main:app --reload --port 8000
Terminal 2 — Angular:
ng serve
Open http://localhost:4200, enter house features, and watch your Angular app talk to your ML model in real time.
What I Learned
ML and frontend are closer than I thought
The hardest part wasn't the ML — it was connecting the two worlds. Once I understood that the model is just a function that takes numbers and returns a number, everything clicked.
Feature scaling is non-negotiable
This model without StandardScaler gave terrible predictions. Five minutes of preprocessing made all the difference. This connects directly back to Feature Engineering — garbage in, garbage out.
FastAPI is a frontend developer's dream
Auto-generated docs, type validation, clean JSON responses — FastAPI thinks the same way Angular does. Strong types everywhere.
The weights tell a story
Looking at the model's coefficients — seeing that age has a negative weight and size has a positive one — made the math feel real. The model learned something true about the world from data alone.
Conclusion
Six weeks ago I didn't know what a regression model was. Today I have one running in production serving predictions to an Angular frontend.
The gap between fullstack development and machine learning is smaller than it looks — especially if you already think in terms of functions, inputs, outputs, and APIs.
This is what learning in public looks like. Messy, exciting, and worth every confused moment.
Are you a frontend or fullstack developer exploring ML? What's your biggest question right now? let's figure it out together!
Top comments (0)