AI Web App: Build a Beginner-Friendly Prediction App Step-by-Step (Python + FastAPI)
12 min read
A practical workflow: build the model, connect it to a web server, and test predictions in the browser.
Last updated: February 2026 ✅
Building an AI web app sounds hard… until you break it into small, clear steps.
Most beginners get stuck in one of these places:
- You trained a model once, but don’t know how to use it in a real app
- You don’t know which tools to install (and what’s actually needed)
- You can run Python scripts, but “web app” feels like a different world
- You don’t know how to structure files so the project stays clean
This tutorial is designed to fix all of that.
You’ll build a complete beginner web app that:
- trains a small model in Python
- saves it to a file
- loads it inside a web server
- provides a simple page where users can submit text
- returns a prediction result
No advanced math. No confusing theory. Just practical steps.
✅ Key Takeaways
🧱 Real app structure
You’ll organize code into training, server, templates, and static files like a real project.
🌐 Web app + API
FastAPI serves both a web page and JSON endpoints so you can grow later.
Learn what to deploy, what to ignore, and how to avoid common production mistakes.
🧭 Quick Navigation
👉 Click to open navigation
- 🤖 What Is an AI Web App?
- 🧩 What We’re Building in This Tutorial
- 🧰 Programs to Use (Beginner Setup)
- 🗂️ Recommended Folder Structure
- 🧠 Step 1: Train and Save a Model
- 🌐 Step 2: Build the Web Server (FastAPI)
- 🖥️ Step 3: Create the Web Page UI
- 📦 Step 4: Add a JSON API Endpoint
- 🚀 Step 5: Run Locally and Test
- 🛠️ Step 6: Improve the App (Validation + UX)
- ☁️ Step 7: Deployment Options for Beginners
- 📊 Strong Table: Problems → Causes → Fixes
- ⚠️ Common Mistakes
- 🏋️ Exercises / Mini Projects (With Solutions)
- ✅ Checklist
- 🧠 Mini Quiz
- 📚 Recommended Reading
- ❓ FAQ
🤖 What Is an AI Web App?
An AI web app is a normal web app that includes a prediction step.
A user provides input (text, numbers, selections).
Your server sends that input into a model.
Then the app returns a result.
The model can do many tasks, for example:
- classify text (spam vs not spam)
- estimate a number (price estimate)
- score something (risk score)
- recommend items (similarity)
In this tutorial, we’ll use a beginner-safe task:
✅ Text sentiment classification (positive / negative).
It’s simple, easy to understand, and easy to test without special datasets.
🧩 What We’re Building in This Tutorial
We’ll build a small app called Sentiment Checker.
Users will:
- open a webpage
- type a short message
- click “Predict”
- see a result
We’ll also provide a JSON endpoint so later you can connect:
- React / Vue frontend
- mobile apps
- other services
App architecture (beginner view)
| Layer | What it does | Tool |
|---|---|---|
| Model training | learns from labeled examples | scikit-learn |
| Model file | saved model you can reuse | joblib |
| Web server | receives input + runs prediction | FastAPI |
| Web UI | simple form page | Jinja templates |
| JSON API | returns results for apps | FastAPI route |
This is a strong “first real app” pattern.
🧰 Programs to Use (Beginner Setup)

You need a setup that is easy and stable.
Recommended tools
- VS Code (code editor)
- Python 3.10+ (runtime)
- Virtual environment (keeps dependencies clean)
- FastAPI (web server)
- Uvicorn (runs the server)
- scikit-learn (model training)
- joblib (save/load model)
Why we use a virtual environment
It prevents “dependency mess” across projects.
If you skip this, beginners commonly run into:
- wrong package versions
- broken installs
- random import errors
We’ll do it the clean way.
🗂️ Recommended Folder Structure
Create a folder for your project:
ai-web-app
Inside it, use this structure:
| Path | Purpose |
|---|---|
app/main.py | FastAPI server |
app/ml/model.joblib | saved model |
app/ml/train.py | training script |
app/templates/index.html | web page UI |
app/static/ | optional CSS/images |
requirements.txt | dependencies |
This structure keeps the model and the web app together.
🧠 Step 1: Train and Save a Model

We’ll train a small sentiment classifier from a tiny dataset.
Important beginner note:
This dataset is intentionally small because we’re learning the workflow, not trying to win accuracy contests.
1) Create the project and venv
In your terminal:
mkdir ai-web-app
cd ai-web-app
python -m venv .venv
Activate it:
Windows (PowerShell):
.venv\Scripts\Activate.ps1
Mac/Linux:
source .venv/bin/activate
2) Install dependencies
pip install fastapi uvicorn scikit-learn joblib jinja2 python-multipart
Why python-multipart?
It helps FastAPI read form submissions (HTML forms).
3) Create training file
Create folders:
mkdir -p app/ml app/templates app/static
Create app/ml/train.py and paste:
import joblib
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
# 1) Tiny training dataset (beginner-friendly)
texts = [
"I love this product",
"This is amazing and helpful",
"Fantastic experience, very happy",
"Worst thing I bought",
"I hate it, very disappointing",
"Terrible quality, not recommended",
"Great service and fast delivery",
"Not good, it broke quickly",
]
labels = [
1, 1, 1, # positive
0, 0, 0, # negative
1, 0
]
# 2) Pipeline = preprocessing + model together
pipeline = Pipeline([
("tfidf", TfidfVectorizer()),
("clf", LogisticRegression(max_iter=1000))
])
# 3) Train
pipeline.fit(texts, labels)
# 4) Save model
joblib.dump(pipeline, "app/ml/model.joblib")
print("✅ Model saved to app/ml/model.joblib")
What this code is doing (clear explanation)
- TfidfVectorizer converts text into numbers
It creates a numeric representation of words based on importance. - LogisticRegression learns to separate the two classes
- Pipeline ensures the same preprocessing happens in training and prediction
- joblib.dump writes a file you can load later
4) Run training
python app/ml/train.py
You should see:
✅ Model saved to app/ml/model.joblib
That file is what your web app will use.
🌐 Step 2: Build the Web Server (FastAPI)
Now we’ll build a server that:
- loads the saved model once
- accepts text input
- returns prediction
Create app/main.py:
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
import joblib
import os
app = FastAPI()
templates = Jinja2Templates(directory="app/templates")
MODEL_PATH = "app/ml/model.joblib"
# Load model once at startup
model = None
@app.on_event("startup")
def load_model():
global model
if not os.path.exists(MODEL_PATH):
model = None
return
model = joblib.load(MODEL_PATH)
def predict_sentiment(text: str) -> dict:
"""
Returns a dictionary with label + confidence-like score.
(We use predict_proba for a simple probability output.)
"""
if model is None:
return {"error": "Model not found. Run training first."}
clean = (text or "").strip()
if not clean:
return {"error": "Please enter some text."}
proba = model.predict_proba([clean])[0] # [neg, pos]
neg, pos = float(proba[0]), float(proba[1])
label = "positive" if pos >= 0.5 else "negative"
score = pos if label == "positive" else neg
return {"label": label, "score": round(score, 4)}
What’s happening here
startuploads the model once
This keeps predictions fast.predict_sentiment()is your single “business logic” function
Both the webpage and API endpoint will call it.
This separation is a very clean beginner habit.
🖥️ Step 3: Create the Web Page UI
Now let’s build a simple page with a text box and a predict button.
Create app/templates/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Sentiment Checker</title>
<style>
body{font-family:system-ui,Arial; background:#0b1220; color:#e8eefc; margin:0;}
.wrap{max-width:860px; margin:0 auto; padding:28px;}
.card{background:#111a2e; border:1px solid rgba(255,255,255,.08); border-radius:14px; padding:18px;}
textarea{width:100%; min-height:120px; border-radius:12px; padding:12px; border:1px solid rgba(255,255,255,.12); background:#0e1628; color:#e8eefc;}
button{margin-top:12px; padding:10px 14px; border-radius:12px; border:0; background:#19c2ff; color:#00101a; font-weight:700; cursor:pointer;}
.muted{opacity:.85; font-size:.95rem;}
.result{margin-top:14px; padding:12px; border-radius:12px; background:rgba(255,255,255,.06);}
.err{background:rgba(255,60,60,.14);}
</style>
</head>
<body>
<div class="wrap">
<h1>Sentiment Checker</h1>
<p class="muted">Type a short sentence and get a simple prediction.</p>
<div class="card">
<form method="post" action="/predict">
<label for="text" class="muted">Your text:</label><br><br>
<textarea id="text" name="text" placeholder="Example: I really like this!">{{ text or "" }}</textarea>
<br>
<button type="submit">Predict</button>
</form>
{% if result %}
<div class="result {% if result.error %}err{% endif %}">
{% if result.error %}
<strong>Error:</strong> {{ result.error }}
{% else %}
<strong>Label:</strong> {{ result.label }}<br>
<strong>Score:</strong> {{ result.score }}
{% endif %}
</div>
{% endif %}
</div>
</div>
</body>
</html>
Why this UI is built this way
- It’s simple (beginner-friendly)
- It works without JavaScript
- It’s easy to host later
- It keeps your tutorial readable and AdSense-safe
📦 Step 4: Add Web + JSON Endpoints
Now we’ll add routes to app/main.py:
Add these at the bottom:
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "result": None, "text": ""})
@app.post("/predict", response_class=HTMLResponse)
def predict_form(request: Request, text: str = Form(...)):
result = predict_sentiment(text)
return templates.TemplateResponse("index.html", {"request": request, "result": result, "text": text})
@app.post("/api/predict", response_class=JSONResponse)
def predict_api(payload: dict):
text = str(payload.get("text", ""))
result = predict_sentiment(text)
return result
What each endpoint does (simple table)
| Route | Method | Returns | Used by |
|---|---|---|---|
/ | GET | HTML page | browser |
/predict | POST | HTML page + result | form submit |
/api/predict | POST | JSON | apps / future frontend |
This is a strong “hybrid” pattern: one server serves both UI and API.
🚀 Step 5: Run Locally and Test

Run the server
From project root:
uvicorn app.main:app --reload --port 8000
Open:
http://localhost:8000/
Try texts like:
- “I love this”
- “This is terrible”
You should see label + score.
Test the JSON API with curl
curl -X POST http://localhost:8000/api/predict \
-H "Content-Type: application/json" \
-d "{\"text\":\"I really like it\"}"
You should receive JSON like:
{"label":"positive","score":0.73}
🛠️ Step 6: Improve the App (Validation + UX)
Beginners often stop at “it works.”
But small improvements make your app feel real.
Add a health endpoint
In app/main.py:
@app.get("/health")
def health():
return {"status": "ok", "modelLoaded": model is not None}
Now you can quickly confirm:
- server is running
- model file is loaded
Add better input rules (simple)
Inside predict_sentiment():
- trim whitespace
- reject super short inputs
Example improvement:
if len(clean) < 3:
return {"error": "Text is too short. Please type a longer message."}
Why this matters
It prevents users from getting confusing predictions from empty inputs.
☁️ Step 7: Deployment Options for Beginners
Once your app works locally, you can deploy it.
Beginner-friendly options (high-level):
| Platform | Best for | Difficulty | Notes |
|---|---|---|---|
| Render / Railway-like platforms | quick web apps | ⭐⭐ | easy deploy from repo |
| Docker + cloud | consistent environments | ⭐⭐⭐ | more setup, very practical |
| AWS Elastic Beanstalk | learning cloud deployment | ⭐⭐⭐ | solid next step after basics |
If you already built Deploying Your First App to AWS, this app can be deployed with the same idea:
- package code
- run server
- set environment config
Beginner rule:
✅ Deploy only after you can run it locally with one command.
📊 Strong Table: Problems → Causes → Fixes
| Problem | Likely cause | Fix |
|---|---|---|
| Page loads but prediction errors | model not trained | run python app/ml/train.py |
| “Model not found” message | wrong path | confirm app/ml/model.joblib exists |
| Form submit fails | missing python-multipart | pip install python-multipart |
| JSON endpoint returns error | missing text field | send {"text":"..."} |
| Predictions feel random | tiny dataset | expand training examples |
| Works once then slow | reloading model per request | load model at startup |
This table helps beginners debug without panic.
⚠️ Common Mistakes
| Mistake | Why it happens | Fix |
|---|---|---|
| Training code mixed inside server | “quick hack” | keep training in train.py |
| Loading model on every request | beginner pattern | load once on startup |
| No validation | trusting users | trim + check length |
| No pipeline | inconsistent preprocessing | use sklearn Pipeline |
| Not testing JSON endpoint | only testing UI | test with curl too |
| Saving secrets in code | convenience | use environment variables |
🏋️ Exercises / Mini Projects (With Solutions)
Exercise 1: Add a “confidence bar” to the UI
Goal: show a simple bar based on score.
Try it yourself
- In template, if result exists and no error, show a bar div
- Use width = score * 100
👉 Click here to see the solution
{% if result and not result.error %} Confidence bar (simple visual)
{% endif %} Explanation
- We convert score (0–1) into percent (0–100%)
- The inner div becomes a visual indicator
- This improves UX without adding complexity
Exercise 2: Add a “batch predict” API
Goal: accept multiple texts and return results for each.
Try it yourself
- Create
/api/predict-batch - Accept
{"texts":[... ]} - Loop and call
predict_sentiment()for each
👉 Click here to see the solution
@app.post("/api/predict-batch") def predict_batch(payload: dict): texts = payload.get("texts", []) if not isinstance(texts, list): return {"error": "texts must be a list"} results = [] for t in texts: results.append({"text": str(t), **predict_sentiment(str(t))}) return {"results": results} Explanation
- Batch endpoints are useful for real apps (bulk processing)
- Keeping
predict_sentiment()reusable makes this easy
Exercise 3: Expand the training dataset safely
Goal: improve stability by adding more examples.
Try it yourself
- Add 20–50 more short sentences
- Keep labels consistent
- Retrain and test again
👉 Click here to see the solution approach
What to do:
- Add more examples to
textsandlabelsintrain.py - Keep messages short and clear
- Run training again and reload the server
Why it helps:
- Small datasets produce unstable results
- More examples = more consistent word patterns
✅ Checklist
✅ Click to open the checklist
- Virtual environment created and activated
- Dependencies installed successfully
python app/ml/train.pycreatesmodel.joblib- FastAPI server runs with
uvicorn - Homepage loads at
/ - Form prediction works via
/predict - JSON API works via
/api/predict /healthshows model is loaded- Input validation prevents empty or too-short text
- Project files are organized (training separate from server)
🧠 Mini Quiz
❓ Why do we save the model to a file?
So the web server can load and use the trained model without retraining every time.
❓ Why use a Pipeline in scikit-learn?
It guarantees the same preprocessing is applied during training and prediction, reducing mistakes and inconsistencies.
❓ What’s the difference between the HTML route and JSON route?
The HTML route returns a webpage for humans. The JSON route returns data for apps, frontend frameworks, or automation.
❓ FAQ
Quick answers to common questions about building an AI web app.
❓ Do I need a large dataset to build my first AI web app?
No. Start small to learn the workflow. Then expand the dataset to improve stability and accuracy.
❓ Why FastAPI instead of something heavier?
FastAPI is simple, fast to run locally, and makes it easy to serve both a webpage and JSON endpoints.
❓ Should I retrain the model inside the web server?
No. Keep training separate. Train once, save the model, then load it in the server at startup.
❓ How can I make the UI nicer later?
Add better styling, add a confidence bar, and consider a frontend framework once your backend is stable.
❓ What’s the next step after this tutorial?
Add a real dataset, improve evaluation, and deploy the app so it’s accessible on the internet.
📚 Recommended Reading
- Machine Learning Projects for Beginners: Step-by-Step Python Tutorial (5 Mini Projects)
- REST API Tutorial: Build and Use Your First API Step-by-Step
- GraphQL Introduction: Learn Queries, Mutations, and Your First API Step-by-Step
- Deploying Your First App to AWS: A Beginner-Friendly Step-by-Step Tutorial
- Frontend Basics Hub: HTML, CSS & JavaScript (Beginner Roadmap)