Moderne Web Apps Bouwen met Flask

Flask is een van de meest populaire Python web frameworks, en dat is niet zonder reden. Als "micro-framework" biedt Flask de essentiële tools voor web development zonder je te overweldigen met complexiteit. In mijn 10 jaar ervaring als full-stack developer bij Nederlandse startups en scale-ups, heb ik gezien hoe Flask teams in staat stelt om snel van idee naar werkende applicatie te gaan.

In dit artikel nemen we je mee van je eerste Flask route tot het bouwen van complexe, schaalbare web applicaties. We behandelen real-world voorbeelden en best practices die ik dagelijks gebruik in mijn werk.

Waarom Flask?

Flexibiliteit en Simpliciteit

Flask volgt de filosofie "micro but mighty". Het framework geeft je de vrijheid om je eigen architecturale keuzes te maken, zonder je te dwingen tot een specifieke structuur zoals Django doet.

Ideaal voor Nederlandse Startups

In Nederland's snelle startup ecosystem is Flask populair omdat:

Je Eerste Flask Applicatie

Installatie en Setup


# Virtual environment aanmaken
python -m venv flask_env
source flask_env/bin/activate  # Linux/Mac
# flask_env\Scripts\activate  # Windows

# Flask installeren
pip install Flask

# Optionele packages voor deze tutorial
pip install Flask-SQLAlchemy Flask-Login Flask-WTF
            

Hello World - De Basis


# app.py
from flask import Flask

# Flask app initialiseren
app = Flask(__name__)

# Eerste route
@app.route('/')
def hello_world():
    return '<h1>Hallo, Flask wereld!</h1>'

# Route met parameter
@app.route('/user/<name>')
def show_user_profile(name):
    return f'<h1>Welkom, {name}!</h1>'

# HTTP methoden
@app.route('/submit', methods=['GET', 'POST'])
def submit_data():
    if request.method == 'POST':
        return 'Data ontvangen via POST'
    else:
        return 'Stuur data via POST'

if __name__ == '__main__':
    app.run(debug=True)
            

Start je app met python app.py en bezoek http://localhost:5000.

Templates en Static Files

Jinja2 Templates

Flask gebruikt Jinja2 voor templating. Maak een templates/ map aan:


# templates/base.html
<!DOCTYPE html>
<html lang="nl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Mijn Flask App{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{{ url_for('index') }}">Mijn App</a>
        </div>
    </nav>
    
    <div class="container mt-4">
        {% block content %}{% endblock %}
    </div>
</body>
</html>
            

# templates/index.html
{% extends "base.html" %}

{% block title %}Home - Mijn Flask App{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8">
        <h1>Welkom bij onze Flask App</h1>
        <p>Dit is een voorbeeld van een moderne Flask applicatie.</p>
        
        {% if users %}
            <h3>Geregistreerde gebruikers:</h3>
            <ul class="list-group">
            {% for user in users %}
                <li class="list-group-item">{{ user.name }} - {{ user.email }}</li>
            {% endfor %}
            </ul>
        {% else %}
            <p>Nog geen gebruikers geregistreerd.</p>
        {% endif %}
    </div>
</div>
{% endblock %}
            

Updated Flask App met Templates


from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

# Mock data (later vervangen door database)
users = [
    {'name': 'Jan Jansen', 'email': '[email protected]'},
    {'name': 'Marie van der Berg', 'email': '[email protected]'}
]

@app.route('/')
def index():
    return render_template('index.html', users=users)

@app.route('/add_user', methods=['GET', 'POST'])
def add_user():
    if request.method == 'POST':
        name = request.form['name']
        email = request.form['email']
        users.append({'name': name, 'email': email})
        return redirect(url_for('index'))
    
    return render_template('add_user.html')

if __name__ == '__main__':
    app.run(debug=True)
            

Database Integratie met SQLAlchemy

Model Definitie


from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'jouw-geheime-sleutel-hier'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# User model
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def __repr__(self):
        return f'<User {self.name}>'

# Post model (voor blog functionality)
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    author = db.relationship('User', backref=db.backref('posts', lazy=True))

# Database aanmaken
with app.app_context():
    db.create_all()
            

CRUD Operaties


@app.route('/')
def index():
    users = User.query.all()
    posts = Post.query.order_by(Post.created_at.desc()).limit(5).all()
    return render_template('index.html', users=users, posts=posts)

@app.route('/add_user', methods=['GET', 'POST'])
def add_user():
    if request.method == 'POST':
        name = request.form['name']
        email = request.form['email']
        
        # Check if email already exists
        existing_user = User.query.filter_by(email=email).first()
        if existing_user:
            return render_template('add_user.html', error='Email bestaat al!')
        
        # Create new user
        user = User(name=name, email=email)
        db.session.add(user)
        db.session.commit()
        
        return redirect(url_for('index'))
    
    return render_template('add_user.html')

@app.route('/user/<int:user_id>')
def user_profile(user_id):
    user = User.query.get_or_404(user_id)
    return render_template('user_profile.html', user=user)

@app.route('/add_post', methods=['GET', 'POST'])
def add_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        author_id = request.form['author_id']
        
        post = Post(title=title, content=content, author_id=author_id)
        db.session.add(post)
        db.session.commit()
        
        return redirect(url_for('index'))
    
    users = User.query.all()
    return render_template('add_post.html', users=users)
            

RESTful API Ontwikkeling

JSON API Endpoints


from flask import jsonify

# API Routes
@app.route('/api/users', methods=['GET'])
def api_get_users():
    users = User.query.all()
    return jsonify([{
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    } for user in users])

@app.route('/api/users', methods=['POST'])
def api_create_user():
    data = request.get_json()
    
    # Validation
    if not data or not data.get('name') or not data.get('email'):
        return jsonify({'error': 'Name and email required'}), 400
    
    # Check if email exists
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email already exists'}), 400
    
    # Create user
    user = User(name=data['name'], email=data['email'])
    db.session.add(user)
    db.session.commit()
    
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    }), 201

@app.route('/api/users/<int:user_id>', methods=['GET'])
def api_get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'created_at': user.created_at.isoformat(),
        'posts': [{
            'id': post.id,
            'title': post.title,
            'content': post.content,
            'created_at': post.created_at.isoformat()
        } for post in user.posts]
    })

@app.route('/api/users/<int:user_id>', methods=['PUT'])
def api_update_user(user_id):
    user = User.query.get_or_404(user_id)
    data = request.get_json()
    
    if 'name' in data:
        user.name = data['name']
    if 'email' in data:
        # Check if new email already exists (excluding current user)
        existing = User.query.filter(User.email == data['email'], User.id != user_id).first()
        if existing:
            return jsonify({'error': 'Email already exists'}), 400
        user.email = data['email']
    
    db.session.commit()
    
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    })

@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def api_delete_user(user_id):
    user = User.query.get_or_404(user_id)
    db.session.delete(user)
    db.session.commit()
    return '', 204
            

Authenticatie en Beveiliging

User Authentication


from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash

# Login manager setup
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

# Update User model voor authentication
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    # Flask-Login required methods
    def is_authenticated(self):
        return True
    
    def is_active(self):
        return True
    
    def is_anonymous(self):
        return False
    
    def get_id(self):
        return str(self.id)

# Authentication routes
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        name = request.form['name']
        email = request.form['email']
        password = request.form['password']
        
        # Check if user exists
        if User.query.filter_by(email=email).first():
            return render_template('register.html', error='Email bestaat al!')
        
        # Create user
        user = User(name=name, email=email)
        user.set_password(password)
        db.session.add(user)
        db.session.commit()
        
        login_user(user)
        return redirect(url_for('index'))
    
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form['email']
        password = request.form['password']
        
        user = User.query.filter_by(email=email).first()
        
        if user and user.check_password(password):
            login_user(user)
            return redirect(url_for('index'))
        else:
            return render_template('login.html', error='Ongeldige inloggegevens!')
    
    return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))

# Protected route example
@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html', user=current_user)
            

Real-World Example: Nederlandse Webshop API

Laten we een praktisch voorbeeld bouwen: een API voor een Nederlandse webshop.


# models.py
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    naam = db.Column(db.String(200), nullable=False)
    beschrijving = db.Column(db.Text)
    prijs = db.Column(db.Numeric(10, 2), nullable=False)
    voorraad = db.Column(db.Integer, default=0)
    categorie_id = db.Column(db.Integer, db.ForeignKey('categorie.id'))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    categorie = db.relationship('Categorie', backref='producten')

class Categorie(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    naam = db.Column(db.String(100), nullable=False, unique=True)
    beschrijving = db.Column(db.Text)

class Bestelling(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    klant_email = db.Column(db.String(120), nullable=False)
    totaal_bedrag = db.Column(db.Numeric(10, 2), nullable=False)
    status = db.Column(db.String(50), default='nieuw')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

class BestellingItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    bestelling_id = db.Column(db.Integer, db.ForeignKey('bestelling.id'))
    product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
    aantal = db.Column(db.Integer, nullable=False)
    prijs = db.Column(db.Numeric(10, 2), nullable=False)
    
    bestelling = db.relationship('Bestelling', backref='items')
    product = db.relationship('Product')
            

# webshop_api.py
from flask import Flask, request, jsonify
from models import db, Product, Categorie, Bestelling, BestellingItem
from decimal import Decimal

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///webshop.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db.init_app(app)

# Products API
@app.route('/api/producten', methods=['GET'])
def get_producten():
    categorie_id = request.args.get('categorie_id')
    zoekterm = request.args.get('q')
    
    query = Product.query
    
    if categorie_id:
        query = query.filter_by(categorie_id=categorie_id)
    
    if zoekterm:
        query = query.filter(Product.naam.contains(zoekterm))
    
    producten = query.all()
    
    return jsonify([{
        'id': p.id,
        'naam': p.naam,
        'beschrijving': p.beschrijving,
        'prijs': float(p.prijs),
        'voorraad': p.voorraad,
        'categorie': p.categorie.naam if p.categorie else None
    } for p in producten])

@app.route('/api/producten/<int:product_id>', methods=['GET'])
def get_product(product_id):
    product = Product.query.get_or_404(product_id)
    return jsonify({
        'id': product.id,
        'naam': product.naam,
        'beschrijving': product.beschrijving,
        'prijs': float(product.prijs),
        'voorraad': product.voorraad,
        'categorie': {
            'id': product.categorie.id,
            'naam': product.categorie.naam
        } if product.categorie else None
    })

# Bestellingen API
@app.route('/api/bestellingen', methods=['POST'])
def create_bestelling():
    data = request.get_json()
    
    # Validation
    if not data.get('klant_email') or not data.get('items'):
        return jsonify({'error': 'Klant email en items zijn verplicht'}), 400
    
    # Calculate total and check stock
    totaal_bedrag = Decimal('0')
    
    for item_data in data['items']:
        product = Product.query.get(item_data['product_id'])
        if not product:
            return jsonify({'error': f'Product {item_data["product_id"]} niet gevonden'}), 400
        
        if product.voorraad < item_data['aantal']:
            return jsonify({'error': f'Onvoldoende voorraad voor {product.naam}'}), 400
        
        totaal_bedrag += product.prijs * item_data['aantal']
    
    # Create order
    bestelling = Bestelling(
        klant_email=data['klant_email'],
        totaal_bedrag=totaal_bedrag
    )
    db.session.add(bestelling)
    db.session.flush()  # Get the ID
    
    # Add items and update stock
    for item_data in data['items']:
        product = Product.query.get(item_data['product_id'])
        
        item = BestellingItem(
            bestelling_id=bestelling.id,
            product_id=product.id,
            aantal=item_data['aantal'],
            prijs=product.prijs
        )
        db.session.add(item)
        
        # Update stock
        product.voorraad -= item_data['aantal']
    
    db.session.commit()
    
    return jsonify({
        'id': bestelling.id,
        'totaal_bedrag': float(bestelling.totaal_bedrag),
        'status': bestelling.status
    }), 201

@app.route('/api/bestellingen/<int:bestelling_id>', methods=['GET'])
def get_bestelling(bestelling_id):
    bestelling = Bestelling.query.get_or_404(bestelling_id)
    
    return jsonify({
        'id': bestelling.id,
        'klant_email': bestelling.klant_email,
        'totaal_bedrag': float(bestelling.totaal_bedrag),
        'status': bestelling.status,
        'created_at': bestelling.created_at.isoformat(),
        'items': [{
            'product': {
                'id': item.product.id,
                'naam': item.product.naam
            },
            'aantal': item.aantal,
            'prijs': float(item.prijs)
        } for item in bestelling.items]
    })

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
        
        # Sample data
        if not Categorie.query.first():
            tech = Categorie(naam='Technologie', beschrijving='Elektronische apparaten')
            books = Categorie(naam='Boeken', beschrijving='Fysieke en digitale boeken')
            
            db.session.add_all([tech, books])
            db.session.commit()
            
            laptop = Product(
                naam='MacBook Pro 16"',
                beschrijving='Krachtige laptop voor professionals',
                prijs=Decimal('2499.99'),
                voorraad=10,
                categorie_id=tech.id
            )
            
            python_book = Product(
                naam='Python voor Data Science',
                beschrijving='Complete gids voor data science met Python',
                prijs=Decimal('39.99'),
                voorraad=50,
                categorie_id=books.id
            )
            
            db.session.add_all([laptop, python_book])
            db.session.commit()
    
    app.run(debug=True)
            

Deployment en Production

Configuratie Management


# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///app_dev.db'

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}
            

Application Factory Pattern


# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import config

db = SQLAlchemy()
login_manager = LoginManager()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # Initialize extensions
    db.init_app(app)
    login_manager.init_app(app)
    login_manager.login_view = 'auth.login'
    
    # Register blueprints
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix='/api')
    
    return app
            

Best Practices en Tips

1. Error Handling


@app.errorhandler(404)
def not_found(error):
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Resource not found'}), 404
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Internal server error'}), 500
    return render_template('500.html'), 500
            

2. Input Validation


from marshmallow import Schema, fields, validate

class UserSchema(Schema):
    name = fields.Str(required=True, validate=validate.Length(min=2, max=100))
    email = fields.Email(required=True)
    age = fields.Int(validate=validate.Range(min=0, max=120))

user_schema = UserSchema()

@app.route('/api/users', methods=['POST'])
def create_user():
    try:
        data = user_schema.load(request.json)
    except ValidationError as err:
        return jsonify({'errors': err.messages}), 400
    
    # Process valid data...
            

3. Database Migrations


# Flask-Migrate installeren
pip install Flask-Migrate

# In je app
from flask_migrate import Migrate

migrate = Migrate(app, db)

# Commands
flask db init      # Initialize migrations
flask db migrate   # Create migration
flask db upgrade   # Apply migration
            

Conclusie

Flask biedt de perfecte balans tussen simpliciteit en kracht voor moderne web development. Met zijn flexibele architectuur en uitgebreide ecosystem kun je alles bouwen, van eenvoudige prototypes tot complexe enterprise applicaties.

De key takeaways uit dit artikel:

Ready to master Flask? Bij ImmenArchl leren onze studenten niet alleen de syntax, maar ook industry best practices en real-world patterns die je direct kunt toepassen in je werk.

Pro Tip van een Senior Developer

"Flask's kracht ligt in zijn flexibiliteit, maar dit kan ook overweldigend zijn voor beginners. Mijn advies: begin met een duidelijke project structuur en voeg functionaliteit stap voor stap toe. Gebruik blueprints vanaf dag één, ook voor kleine projecten. Je toekomstige zelf zal je dankbaar zijn!"

- Marcus Janssen, Full-Stack Developer & Instructeur

Gerelateerde Artikelen