Project B - Een webgebaseerd valuta-dashboard
01 - Inleiding: doel van Project B
In project A heb je al een eerste Flask-dashboard gebouwd met data uit een bestaande SQLite-database. In dit tweede project gaan we een stap verder: we halen nu zelf actuele data op uit een API en slaan die op in een eigen SQLite-database.
We bouwen een kleine webapp in Flask die de actuele Bitcoin-prijs laat zien. Daarbij gebruiken we een gratis API voor BTC-EUR, zodat de prijs direct in euro’s beschikbaar is. Als extra uitbreiding kun je later ook nog een eenvoudige USD/EUR API toevoegen.
- De nadruk ligt in dit project op drie nieuwe onderdelen: API-data ophalen, opslaan in SQLite en verversen in de browser met een klein beetje JavaScript.
-
Zaken die in project A al uitgebreid zijn behandeld, zoals de basis van Flask en de map
templates, herhalen we hier korter.
Het eindresultaat is een compacte Flask-app waarmee je leert hoe een Python-project kan samenwerken met externe webdata.
02 - Projectmap en virtual environment
- Net als in project A werken we weer in een virtual environment.
- De uitleg waarom je dat doet is daar al behandeld, dus hier volgen vooral de stappen.
-
Maak op je bureaublad een map aan met de naam
coins. - Open daarna een terminal of opdrachtprompt en ga naar die map.
cd Desktop/coins
-
Maak vervolgens een virtual environment aan met de naam
.venv.
python -m venv .venv
- Activeer daarna de virtual environment.
- Op Windows gebruik je:
.venv\Scripts\activate
- Op macOS of Linux gebruik je:
source .venv/bin/activate
- Controleer of de omgeving actief is.
-
Meestal zie je dan
(.venv)of een vergelijkbare naam vooraan in je prompt.
- Werk vanaf nu steeds binnen deze omgeving als je packages installeert of je project start.
03 - Benodigde packages installeren
- We installeren alleen de onderdelen die nodig zijn voor Flask en het ophalen van API-data.
- Installeer Flask en Requests met:
pip install flask requests
-
Flaskgebruiken we voor de webapp. -
requestsgebruiken we om data op te halen van een externe API. -
Voor SQLite hoef je niets extra’s te installeren, want
sqlite3zit standaard in Python.
-
Sla je dependencies daarna op in een
requirements.txt-bestand.
pip freeze > requirements.txt
- Dat is handig als je het project later opnieuw wilt opzetten.
- Je kunt dan alle packages opnieuw installeren met:
pip install -r requirements.txt
04 - Een minimale projectstructuur
- Voor project B gebruiken we weer dezelfde basisstructuur als in project A.
- Alleen voegen we nu ook een eigen databasebestand en een JavaScript-bestand toe.
-
Maak in de map
coinsde volgende structuur aan:
coins/
│
├── app.py
├── requirements.txt
├── data/
│ └── coins.db
├── templates/
│ └── index.html
└── static/
├── style.css
└── app.js
-
app.pybevat de Flask-app, de API-aanroep en het werken met SQLite. -
In
data/coins.dbslaan we de Bitcoin-prijzen op. -
In
templates/index.htmlkomt de HTML van het dashboard. -
In
static/style.csskomt de opmaak. -
In
static/app.jskomt een klein stukje JavaScript om gegevens te verversen zonder de hele pagina te herladen.
-
Maak eerst de mappen
data,templatesenstaticaan. - Maak daarna de bestanden aan die hierboven staan.
05 - De SQLite-database voorbereiden
- In project A gebruikten we een bestaande database.
- In dit project maken we de database zelf aan vanuit Python.
- Dat is een belangrijk verschil: de webapp moet nu niet alleen data lezen, maar ook nieuwe records opslaan.
-
Voeg in
app.pyeerst deze imports en basisconfiguratie toe:
import os
import sqlite3
from datetime import datetime
import requests
from flask import Flask, jsonify, render_template
app = Flask(__name__)
BASE_DIR = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_DIR, "data", "coins.db")
-
We gebruiken
os.path.join(...)zodat het pad naar de database netjes wordt opgebouwd. - Daardoor hoef je niet handmatig te werken met losse schuine strepen in paden.
- Voeg daarna een functie toe die de database en tabel aanmaakt als die nog niet bestaan:
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS bitcoin_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
price_eur REAL NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL
)
"""
)
conn.commit()
- Deze tabel krijgt vier belangrijke onderdelen.
-
idis een uniek volgnummer. -
price_eurbevat de prijs van Bitcoin in euro’s. -
sourcebewaart uit welke API de prijs kwam. -
created_atbevat het moment waarop de prijs is opgeslagen.
Waarom slaan we tijd en bron ook op?
- Als je alleen de prijs bewaart, weet je later niet meer wanneer die prijs is opgehaald.
- Door ook een tijdstip op te slaan, kun je later meerdere metingen tonen of een kleine grafiek bouwen.
-
Het veld
sourceis handig als je later meerdere API’s gaat gebruiken of wilt controleren waar de data vandaan komt.
06 - Bitcoin-data ophalen uit een gratis API
- Voor dit project gebruiken we een gratis endpoint dat de prijs van BTC-EUR teruggeeft.
- Daardoor hoeven we de prijs niet eerst van dollars naar euro’s om te rekenen.
- Een bruikbare keuze hiervoor is de publieke prijs-API van Bitvavo.
-
Voeg in
app.pydeze constante toe:
BTC_API_URL = "https://api.bitvavo.com/v2/ticker/price?market=BTC-EUR"
- Maak daarna een functie die de prijs ophaalt:
def fetch_bitcoin_price():
response = requests.get(BTC_API_URL, timeout=10)
response.raise_for_status()
data = response.json()
price = float(data["price"])
return {
"price_eur": price,
"source": "bitvavo",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
-
Met
requests.get(...)sturen we een HTTP-request naar de API. -
Met
raise_for_status()laten we Python een fout geven als de server geen goede response terugstuurt. -
Met
response.json()zetten we de JSON-response om naar een Python-dictionary. -
De prijs komt uit het veld
priceen zetten we om naar eenfloat.
- Een API kan soms traag reageren of tijdelijk niet bereikbaar zijn.
-
Met
timeout=10wacht Python maximaal 10 seconden op een antwoord. - Zonder timeout kan je programma onnodig lang blijven hangen.
Hoe ziet de API-response eruit?
- De API geeft bij deze aanvraag een klein JSON-object terug.
- Dat ziet er ongeveer zo uit:
{
"market": "BTC-EUR",
"price": "64250"
}
- JSON lijkt veel op een Python-dictionary.
-
Daarom kunnen we in Python eenvoudig werken met
data["price"]. -
Let op dat de prijs hier vaak als
stringterugkomt. -
Daarom zetten we hem met
float(...)om naar een numerieke waarde.
07 - API-data opslaan en uitlezen
- De volgende stap is het opslaan van de opgehaalde prijs in SQLite.
- Zo blijft de data bewaard, ook als je de browser of Flask-app afsluit.
-
Voeg in
app.pydeze functie toe:
def save_bitcoin_price(price_data):
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"""
INSERT INTO bitcoin_prices (price_eur, source, created_at)
VALUES (?, ?, ?)
""",
(
price_data["price_eur"],
price_data["source"],
price_data["created_at"]
)
)
conn.commit()
-
De
?-tekens zijn placeholders. - De waarden worden veilig apart meegegeven in een tuple.
- Dat is beter dan SQL-strings zelf aan elkaar plakken.
- Voeg ook een functie toe om de laatst opgeslagen prijs weer op te halen:
def get_latest_price():
with sqlite3.connect(DB_PATH) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute(
"""
SELECT price_eur, source, created_at
FROM bitcoin_prices
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
return dict(row) if row else None
-
Hier sorteren we op
id DESC. - Dat betekent: nieuwste record eerst.
-
Met
LIMIT 1halen we alleen het laatste record op.
- Voeg daarna nog een functie toe die alles achter elkaar uitvoert:
def refresh_bitcoin_data():
price_data = fetch_bitcoin_price()
save_bitcoin_price(price_data)
return price_data
- Deze functie is handig omdat je later op meerdere plekken precies dezelfde stap wilt uitvoeren: ophalen, opslaan en teruggeven.
08 - Flask routes en HTML-dashboard
- Nu de data opgehaald en opgeslagen kan worden, maken we de routes van de app.
- We gebruiken in dit project twee routes:
-
/voor de webpagina. -
/api/bitcoinvoor JSON-data die we later met JavaScript kunnen ophalen.
-
Zet dit in
app.pyonder de functies:
@app.route("/")
def index():
latest_price = get_latest_price()
if latest_price is None:
latest_price = refresh_bitcoin_data()
return render_template("index.html", bitcoin=latest_price)
@app.route("/api/bitcoin")
def api_bitcoin():
latest_price = refresh_bitcoin_data()
return jsonify(latest_price)
- Als er nog geen data in de database staat, halen we bij het openen van de homepage eerst één keer nieuwe data op.
-
De route
/api/bitcoinis bedoeld voor JavaScript. - Die route geeft geen HTML terug, maar JSON.
-
Zet daarna deze basis in
templates/index.html:
<!doctype html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bitcoin Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="container">
<h1>Bitcoin Dashboard</h1>
<section class="card">
<h2>Actuele prijs</h2>
<p id="bitcoin-price">€ {{ bitcoin.price_eur }}</p>
<p id="bitcoin-time">Laatst bijgewerkt: {{ bitcoin.created_at }}</p>
<button id="refresh-button">Ververs prijs</button>
</section>
</main>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>
- In deze HTML laat Flask meteen de laatst bekende Bitcoin-prijs zien.
-
Dankzij de
id-attributen kunnen we straks met JavaScript precies deze onderdelen aanpassen.
-
Voeg in
static/style.cssbijvoorbeeld dit toe:
body {
font-family: Arial, sans-serif;
background: #f4f7fb;
margin: 0;
padding: 40px;
}
.container {
max-width: 720px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}
#bitcoin-price {
font-size: 2rem;
font-weight: bold;
color: #1b4332;
}
button {
padding: 12px 18px;
border: none;
border-radius: 10px;
background: #1d3557;
color: white;
cursor: pointer;
}
09 - Een klein beetje JavaScript voor verversen
- Tot nu toe werkt het dashboard vooral server-side via Flask.
- Met een klein beetje JavaScript kunnen we de prijs verversen zonder de hele pagina opnieuw te laden.
- Dit is een mooie eerste stap richting een meer interactieve webapp.
-
Zet in
static/app.jsde volgende code:
const priceElement = document.getElementById("bitcoin-price");
const timeElement = document.getElementById("bitcoin-time");
const refreshButton = document.getElementById("refresh-button");
async function refreshBitcoinPrice() {
try {
const response = await fetch("/api/bitcoin");
const data = await response.json();
priceElement.textContent = `€ ${data.price_eur}`;
timeElement.textContent = `Laatst bijgewerkt: ${data.created_at}`;
} catch (error) {
timeElement.textContent = "Fout bij ophalen van nieuwe prijs.";
}
}
refreshButton.addEventListener("click", refreshBitcoinPrice);
-
Met
fetch("/api/bitcoin")vraagt JavaScript nieuwe JSON-data op bij Flask. -
Met
await response.json()zetten we die response om naar een JavaScript-object. - Daarna vullen we de HTML-elementen opnieuw met de nieuwste gegevens.
Waarom gebruiken we hier JavaScript?
- Zonder JavaScript zou de gebruiker steeds de hele pagina moeten verversen.
- Met JavaScript kun je alleen het onderdeel aanpassen dat echt veranderd is.
- In dit project blijft Flask de backend en blijft Python het belangrijkste onderdeel.
- JavaScript heeft hier dus een kleine ondersteunende rol.
- Je kunt deze functie ook automatisch elke minuut laten draaien.
- Voeg dan onderaan nog deze regel toe:
setInterval(refreshBitcoinPrice, 60000);
10 - App starten en uitbreiden met goud of zilver
-
Voeg helemaal onderaan in
app.pynog dit startblok toe:
if __name__ == "__main__":
init_db()
app.run(debug=True)
-
De functie
init_db()zorgt ervoor dat de database en tabel bestaan voordat de app start. - Start het project daarna met:
python app.py
- Open vervolgens in je browser:
http://127.0.0.1:5000
- Je hebt nu een werkende Flask-app die:
- Bitcoin-data ophaalt uit een gratis API.
- De prijs opslaat in SQLite.
- De laatste prijs toont in HTML.
- De prijs opnieuw kan ophalen via een kleine JavaScript-functie.
Optionele uitbreiding met goud of zilver
- Als uitbreiding op dit project kun je naast Bitcoin ook een tweede onderdeel toevoegen, zoals goud of zilver.
- Daarmee oefen je opnieuw met precies dezelfde werkwijze: data ophalen, data opslaan in SQLite en data tonen in Flask.
-
Een logische vervolgstap is daarom om een extra API-functie te maken voor bijvoorbeeld goud (
XAU) of zilver (XAG). - Je kunt daar dezelfde projectstructuur voor blijven gebruiken.
def fetch_silver_price():
response = requests.get("https://api.gold-api.com/price/XAG", timeout=10)
response.raise_for_status()
data = response.json()
return {
"name": "Silver",
"price": float(data["price"]),
"source": "gold-api",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
-
Voor goud kun je bijna dezelfde functie maken, maar dan met
XAUin de URL. -
Daarna kun je een tweede tabel maken of je bestaande tabel uitbreiden met een extra kolom zoals
symbolofname. - Vervolgens kun je in je HTML-dashboard een tweede kaartje toevoegen voor goud of zilver.
- Mogelijke vervolgstappen na dit project zijn:
- Alleen de laatste 20 records uit SQLite laten zien.
- Een eenvoudige lijn-grafiek maken.
- Meer coins of wisselkoersen toevoegen.
11 - Een eenvoudige Bitcoin-grafiek als uitbreiding
- Als afronding van dit project kun je een eenvoudige grafiek toevoegen van de opgeslagen Bitcoin-koersen.
- Dat sluit goed aan op project A, want daar heb je al gewerkt met het tonen van data in een dashboard en het visualiseren van informatie.
- In project B heb je nu zelf data opgehaald en opgeslagen in SQLite.
- Daarmee heb je precies de basis om ook hier een kleine grafiek te maken.
- Een logische aanpak is:
-
meerdere Bitcoin-prijzen bewaren in de tabel
bitcoin_prices - de laatste records ophalen met een SQL-query
- die gegevens doorgeven aan je template
- en ze daarna als grafiek tonen in de browser
- Een simpele query om bijvoorbeeld de laatste 20 metingen op te halen kan er zo uitzien:
def get_recent_prices():
with sqlite3.connect(DB_PATH) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT price_eur, created_at
FROM bitcoin_prices
ORDER BY id DESC
LIMIT 20
"""
).fetchall()
return [dict(row) for row in reversed(rows)]
-
Met
LIMIT 20haal je een kleine reeks op. -
Met
reversed(rows)zet je de lijst weer in chronologische volgorde, zodat de oudste meting links staat en de nieuwste rechts.
- Je kunt deze gegevens daarna in je Flask-route meesturen naar de template:
@app.route("/")
def index():
latest_price = get_latest_price()
if latest_price is None:
latest_price = refresh_bitcoin_data()
chart_data = get_recent_prices()
return render_template(
"index.html",
bitcoin=latest_price,
chart_data=chart_data
)
- Daarna kun je in de HTML een extra onderdeel toevoegen voor de grafiek.
- Je kunt dit bijvoorbeeld oplossen met een eenvoudige JavaScript-chart library, of met dezelfde grafiek-aanpak als in project A.
- Het belangrijkste idee is dat je nu niet alleen de laatste prijs toont, maar ook de ontwikkeling over tijd.
- Daarmee rond je project B logisch af:
- API-data ophalen
- data opslaan in SQLite
- data tonen in Flask
- en als extra stap een eenvoudige grafiek toevoegen van de Bitcoin-koers
Totale code van de eenvoudige eindversie
- Hieronder staat een compacte, werkende totaalversie van het project.
- Deze code hoort bij de eenvoudige leeruitwerking uit de stappen hierboven.
- Cursisten kunnen deze versie gebruiken om hun eigen code te vergelijken en fouten terug te vinden.
-
Bestand:
app.py
import os
import sqlite3
from datetime import datetime
import requests
from flask import Flask, jsonify, render_template
app = Flask(__name__)
BASE_DIR = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_DIR, "data", "coins.db")
BTC_API_URL = "https://api.bitvavo.com/v2/ticker/price?market=BTC-EUR"
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS bitcoin_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
price_eur REAL NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL
)
"""
)
conn.commit()
def fetch_bitcoin_price():
response = requests.get(BTC_API_URL, timeout=10)
response.raise_for_status()
data = response.json()
return {
"price_eur": float(data["price"]),
"source": "bitvavo",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def save_bitcoin_price(price_data):
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"""
INSERT INTO bitcoin_prices (price_eur, source, created_at)
VALUES (?, ?, ?)
""",
(
price_data["price_eur"],
price_data["source"],
price_data["created_at"],
),
)
conn.commit()
def get_latest_price():
with sqlite3.connect(DB_PATH) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute(
"""
SELECT price_eur, source, created_at
FROM bitcoin_prices
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
return dict(row) if row else None
def get_recent_prices():
with sqlite3.connect(DB_PATH) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"""
SELECT price_eur, created_at
FROM bitcoin_prices
ORDER BY id DESC
LIMIT 20
"""
).fetchall()
return [dict(row) for row in reversed(rows)]
def refresh_bitcoin_data():
price_data = fetch_bitcoin_price()
save_bitcoin_price(price_data)
return price_data
@app.route("/")
def index():
latest_price = get_latest_price()
if latest_price is None:
latest_price = refresh_bitcoin_data()
chart_data = get_recent_prices()
return render_template(
"index.html",
bitcoin=latest_price,
chart_data=chart_data,
)
@app.route("/api/bitcoin")
def api_bitcoin():
latest_price = refresh_bitcoin_data()
return jsonify(latest_price)
if __name__ == "__main__":
init_db()
app.run(debug=True)
-
Bestand:
templates/index.html
<!doctype html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bitcoin Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="container">
<h1>Bitcoin Dashboard</h1>
<section class="card">
<h2>Actuele prijs</h2>
<p id="bitcoin-price">€ {{ bitcoin.price_eur }}</p>
<p id="bitcoin-time">Laatst bijgewerkt: {{ bitcoin.created_at }}</p>
<button id="refresh-button">Ververs prijs</button>
</section>
<section class="card">
<h2>Laatste 20 metingen</h2>
<div class="chart">
{% for item in chart_data %}
<div class="bar-group">
<div
class="bar"
style="height: {{ (item.price_eur / 1000) | round(0, 'floor') }}px;"
title="€ {{ item.price_eur }} op {{ item.created_at }}"
></div>
<span class="bar-label">{{ loop.index }}</span>
</div>
{% endfor %}
</div>
</section>
</main>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>
-
Bestand:
static/style.css
body {
font-family: Arial, sans-serif;
background: #f4f7fb;
margin: 0;
padding: 40px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: #1d3557;
}
.card {
background: white;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}
#bitcoin-price {
font-size: 2rem;
font-weight: bold;
color: #1b4332;
}
button {
padding: 12px 18px;
border: none;
border-radius: 10px;
background: #1d3557;
color: white;
cursor: pointer;
}
.chart {
display: flex;
align-items: end;
gap: 8px;
min-height: 220px;
padding-top: 12px;
}
.bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.bar {
width: 22px;
min-height: 10px;
background: #457b9d;
border-radius: 6px 6px 0 0;
}
.bar-label {
font-size: 0.75rem;
color: #555;
}
-
Bestand:
static/app.js
const priceElement = document.getElementById("bitcoin-price");
const timeElement = document.getElementById("bitcoin-time");
const refreshButton = document.getElementById("refresh-button");
async function refreshBitcoinPrice() {
try {
const response = await fetch("/api/bitcoin");
const data = await response.json();
priceElement.textContent = `€ ${data.price_eur}`;
timeElement.textContent = `Laatst bijgewerkt: ${data.created_at}`;
} catch (error) {
timeElement.textContent = "Fout bij ophalen van nieuwe prijs.";
}
}
refreshButton.addEventListener("click", refreshBitcoinPrice);
setInterval(refreshBitcoinPrice, 60000);
-
Bestand:
requirements.txt
flask
requests
- Deze versie is bewust eenvoudig gehouden.
- De grafiek is hier opgebouwd met HTML en CSS-balken, zodat de code begrijpelijk blijft.
- Later kun je dit uitbreiden met een echte chart library of met een aanpak zoals in project A.
12 - Resultaat (uitgebreider eindproduct bekijken)
- Als je wilt zien hoe een uitgebreidere versie van dit project eruitziet, kun je hier het eindresultaat bekijken: www.crossway.nl/coins
- Gebruik die versie vooral als inspiratie voor vormgeving en uitbreiding.
- Voor dit project blijft de kleine Bitcoin-versie met SQLite, Flask en een eenvoudige grafiek het belangrijkste leerdoel.