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
  • Flask gebruiken we voor de webapp.
  • requests gebruiken we om data op te halen van een externe API.
  • Voor SQLite hoef je niets extra’s te installeren, want sqlite3 zit 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 coins de volgende structuur aan:
coins/
│
├── app.py
├── requirements.txt
├── data/
│   └── coins.db
├── templates/
│   └── index.html
└── static/
    ├── style.css
    └── app.js
  • app.py bevat de Flask-app, de API-aanroep en het werken met SQLite.
  • In data/coins.db slaan we de Bitcoin-prijzen op.
  • In templates/index.html komt de HTML van het dashboard.
  • In static/style.css komt de opmaak.
  • In static/app.js komt een klein stukje JavaScript om gegevens te verversen zonder de hele pagina te herladen.
  • Maak eerst de mappen data, templates en static aan.
  • 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.py eerst 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.
  • id is een uniek volgnummer.
  • price_eur bevat de prijs van Bitcoin in euro’s.
  • source bewaart uit welke API de prijs kwam.
  • created_at bevat 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 source is 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.py deze 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 price en zetten we om naar een float.
  • Een API kan soms traag reageren of tijdelijk niet bereikbaar zijn.
  • Met timeout=10 wacht 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 string terugkomt.
  • 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.py deze 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 1 halen 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/bitcoin voor JSON-data die we later met JavaScript kunnen ophalen.
  • Zet dit in app.py onder 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/bitcoin is 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.css bijvoorbeeld 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.js de 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.py nog 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 XAU in de URL.
  • Daarna kun je een tweede tabel maken of je bestaande tabel uitbreiden met een extra kolom zoals symbol of name.
  • 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 20 haal 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.