Why I Built Note Blog with Flask (And Not FastAPI) by dokuDoku system design 🔍 Hi, I recently launched Note Blog — a simple, personal blogging and note-taking site built entirely in Python with **Flask**. I originally considered **FastAPI**, but ended up choosing Flask for very practical reasons. This post shares my reasoning, the Flask features I relied on, and how I structured the app using Blueprints, Flask-SQLAlchemy, and Flask-Login. If you're deciding between Flask and FastAPI in 2026, or just want a reference for building a traditional web app, hopefully this helps. ### Helpful Resources These are resources I found helpful when reading about Flask: - [Flask Website & Documentation](https://flask.palletsprojects.com/en/stable/) - Was quite useful for conceptual learning - [Jinja Introduction](https://jinja.palletsprojects.com/en/stable/intro/) - [SQL Alchemy Documentation](https://docs.sqlalchemy.org/en/20/intro.html) - I have previous experience with this product, may want to ask ChatGPT for more info - It's a great way to use Python to interact with a SQL backend and model the tables as classes - Integrates well with Flask ## Why Flask Over FastAPI for Note Blog? FastAPI is excellent. It is async by default, generates OpenAPI docs automatically, uses Pydantic for validation, and performs extremely well for APIs. It has become the default choice for microservices, AI backends, and high-concurrency systems. Additionally, I have personally used it and have found it intuitive and straightforward to work with, especially if you are doing backend Python development and you are communicating with a Javascript frontend that is being managed separately. However, Note Blog has different needs: * Content does not update in real time * Pages are mostly server-rendered HTML using Jinja * Only light client-side JavaScript is required * Uses standard SQL queries for users and notes * I wanted to stay mostly in Python and avoid heavy JavaScript Flask fits this perfectly. It gives full control without forcing an API-first architecture. I do not need autogenerated docs or async endpoints. I need clean routes that render HTML. In 2026, Flask remains an excellent choice for personal projects, dashboards, admin panels, and multi-page web apps where simplicity matters more than raw throughput. ## Core Features That Made Flask a Great Fit ### Mostly Python and Jinja Templates Most of the logic lives in Python. Jinja templates handle rendering HTML with variables passed from routes. * Data flows from routes to templates * Pages are rendered server-side * Minimal JavaScript is required Example usage: ``` render_template('note.html', note=note, user=current_user) ``` ### Server-Side Rendering Flow 1. Flask receives a request 2. A Jinja template is rendered with data 3. Full HTML is sent to the client Client-side JavaScript is only used for small enhancements like dark mode toggles or lightweight `fetch` calls. This dramatically reduces JavaScript complexity compared to SPA frameworks. ### Quickstart and Basic Routing Flask is extremely easy to start with. ``` from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>" ``` Routing features include: * Route decorators like `@app.route('/user/<username>')` * Variable converters such as `<int:id>` and `<path:filename>` * Explicit HTTP methods * Helper functions like `url_for` Example `url_for` usage: ``` url_for('static', filename='style.css') url_for('auth.login') url_for('notes.view_note', note_id=42) ``` Benefits of `url_for`: * Refactor-safe * Automatically handles paths * Works when apps are mounted under subpaths Static files live in the `static` directory and are referenced using `url_for`. For production, I run Flask behind Gunicorn in Docker: ``` gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 2 src.app:app ``` ## Accessing Request Data Flask exposes request data through the global `request` object. * `request.form` for POST data * `request.args` for query parameters * `request.files`, headers, and JSON Each request has its own context, making this safe with concurrent users. ## Organizing with Blueprints As the app grew, I split routes into Blueprints. Benefits of Blueprints: * Keeps the main app file clean * Groups routes by feature * Makes the project easier to scale Example structure: ``` src/ app.py blueprints/ auth.py notes.py templates/ base.html auth/ login.html ``` Example Blueprint definition: ``` from flask import Blueprint, render_template auth_bp = Blueprint('auth', __name__, url_prefix='/auth') @auth_bp.route('/login', methods=['GET', 'POST']) def login(): return render_template('auth/login.html') ``` Registering Blueprints: ``` app.register_blueprint(auth_bp) app.register_blueprint(notes_bp, url_prefix='/notes') ``` Blueprints integrate cleanly with `url_for`. ## Database with Flask-SQLAlchemy I used **Flask-SQLAlchemy** for convenience. SQLAlchemy I have found works well in general, as long as you get your migrations correct. I use Supabase for my SQL server, and I prefer to run my migrations using the SQL interface on the Supabase website. Migrating from your local client (when you have a team of more than 1) can be frustrating since everyone needs a copy of the same migration history. Setup: ``` from flask_sqlalchemy import SQLAlchemy app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///noteblog.db' db = SQLAlchemy(app) ``` Models: ``` class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200)) content = db.Column(db.Text) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) ``` Queries: ``` notes = Note.query.filter_by(user_id=current_user.id).all() note = Note.query.get_or_404(note_id) ``` This approach scales well and works nicely with migrations. ## User Authentication with Flask-Login For authentication, I used **Flask-Login**. Setup: ``` from flask_login import LoginManager, UserMixin login_manager = LoginManager(app) login_manager.login_view = 'auth.login' class User(db.Model, UserMixin): def get_id(self): return str(self.id) ``` User loader: ``` @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) ``` Routes: ``` from flask_login import login_user, login_required, current_user @auth_bp.route('/login', methods=['GET', 'POST']) def login(): login_user(user, remember=True) return redirect(url_for('notes.dashboard')) @app.route('/dashboard') @login_required def dashboard(): return render_template('dashboard.html', user=current_user) ``` `current_user` is available in routes and templates, and `login_required` protects private pages. ## Wrapping Up Flask gave Note Blog exactly what it needed: simplicity, control, Python-centric development, and easy extensibility through mature extensions. There was no need for async complexity or API-first design. For traditional server-rendered apps, Flask remains an excellent choice in 2026. For heavy APIs or real-time systems, FastAPI is likely the better tool. That being said, typically for a FastAPI service you also write a Javascript frontend which increases development time (tradeoff). There are ways to do HTTP requests with Flask where you can have part of the Flask webpage repeatedly call an endpoint or leave a connection open (Server Sent Events, or possibly WebSocket). Due to this, I feel comfortable building with Flask, since there is a high chance that I do not need to later migrate my application to FastAPI, and can maintain the benefits of a Python first development environment, where I only need a single server repository. The exception being of course when microservices are added an scaled, but this is a separate issue than the webserver itself. Hi, I recently launched Note Blog — a simple, personal blogging and note-taking site built entirely in Python with **Flask**. I originally considered **FastAPI**, but ended up choosing Flask for very practical reasons. This post shares my reasoning, the Flask features I relied on, and how I structured the app using Blueprints, Flask-SQLAlchemy, and Flask-Login. If you're deciding between Flask and FastAPI in 2026, or just want a reference for building a traditional web app, hopefully this helps. ### Helpful Resources These are resources I found helpful when reading about Flask: - [Flask Website & Documentation](https://flask.palletsprojects.com/en/stable/) - Was quite useful for conceptual learning - [Jinja Introduction](https://jinja.palletsprojects.com/en/stable/intro/) - [SQL Alchemy Documentation](https://docs.sqlalchemy.org/en/20/intro.html) - I have previous experience with this product, may want to ask ChatGPT for more info - It's a great way to use Python to interact with a SQL backend and model the tables as classes - Integrates well with Flask ## Why Flask Over FastAPI for Note Blog? FastAPI is excellent. It is async by default, generates OpenAPI docs automatically, uses Pydantic for validation, and performs extremely well for APIs. It has become the default choice for microservices, AI backends, and high-concurrency systems. Additionally, I have personally used it and have found it intuitive and straightforward to work with, especially if you are doing backend Python development and you are communicating with a Javascript frontend that is being managed separately. However, Note Blog has different needs: * Content does not update in real time * Pages are mostly server-rendered HTML using Jinja * Only light client-side JavaScript is required * Uses standard SQL queries for users and notes * I wanted to stay mostly in Python and avoid heavy JavaScript Flask fits this perfectly. It gives full control without forcing an API-first architecture. I do not need autogenerated docs or async endpoints. I need clean routes that render HTML. In 2026, Flask remains an excellent choice for personal projects, dashboards, admin panels, and multi-page web apps where simplicity matters more than raw throughput. ## Core Features That Made Flask a Great Fit ### Mostly Python and Jinja Templates Most of the logic lives in Python. Jinja templates handle rendering HTML with variables passed from routes. * Data flows from routes to templates * Pages are rendered server-side * Minimal JavaScript is required Example usage: ``` render_template('note.html', note=note, user=current_user) ``` ### Server-Side Rendering Flow 1. Flask receives a request 2. A Jinja template is rendered with data 3. Full HTML is sent to the client Client-side JavaScript is only used for small enhancements like dark mode toggles or lightweight `fetch` calls. This dramatically reduces JavaScript complexity compared to SPA frameworks. ### Quickstart and Basic Routing Flask is extremely easy to start with. ``` from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>" ``` Routing features include: * Route decorators like `@app.route('/user/<username>')` * Variable converters such as `<int:id>` and `<path:filename>` * Explicit HTTP methods * Helper functions like `url_for` Example `url_for` usage: ``` url_for('static', filename='style.css') url_for('auth.login') url_for('notes.view_note', note_id=42) ``` Benefits of `url_for`: * Refactor-safe * Automatically handles paths * Works when apps are mounted under subpaths Static files live in the `static` directory and are referenced using `url_for`. For production, I run Flask behind Gunicorn in Docker: ``` gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 2 src.app:app ``` ## Accessing Request Data Flask exposes request data through the global `request` object. * `request.form` for POST data * `request.args` for query parameters * `request.files`, headers, and JSON Each request has its own context, making this safe with concurrent users. ## Organizing with Blueprints As the app grew, I split routes into Blueprints. Benefits of Blueprints: * Keeps the main app file clean * Groups routes by feature * Makes the project easier to scale Example structure: ``` src/ app.py blueprints/ auth.py notes.py templates/ base.html auth/ login.html ``` Example Blueprint definition: ``` from flask import Blueprint, render_template auth_bp = Blueprint('auth', __name__, url_prefix='/auth') @auth_bp.route('/login', methods=['GET', 'POST']) def login(): return render_template('auth/login.html') ``` Registering Blueprints: ``` app.register_blueprint(auth_bp) app.register_blueprint(notes_bp, url_prefix='/notes') ``` Blueprints integrate cleanly with `url_for`. ## Database with Flask-SQLAlchemy I used **Flask-SQLAlchemy** for convenience. SQLAlchemy I have found works well in general, as long as you get your migrations correct. I use Supabase for my SQL server, and I prefer to run my migrations using the SQL interface on the Supabase website. Migrating from your local client (when you have a team of more than 1) can be frustrating since everyone needs a copy of the same migration history. Setup: ``` from flask_sqlalchemy import SQLAlchemy app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///noteblog.db' db = SQLAlchemy(app) ``` Models: ``` class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200)) content = db.Column(db.Text) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) ``` Queries: ``` notes = Note.query.filter_by(user_id=current_user.id).all() note = Note.query.get_or_404(note_id) ``` This approach scales well and works nicely with migrations. ## User Authentication with Flask-Login For authentication, I used **Flask-Login**. Setup: ``` from flask_login import LoginManager, UserMixin login_manager = LoginManager(app) login_manager.login_view = 'auth.login' class User(db.Model, UserMixin): def get_id(self): return str(self.id) ``` User loader: ``` @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) ``` Routes: ``` from flask_login import login_user, login_required, current_user @auth_bp.route('/login', methods=['GET', 'POST']) def login(): login_user(user, remember=True) return redirect(url_for('notes.dashboard')) @app.route('/dashboard') @login_required def dashboard(): return render_template('dashboard.html', user=current_user) ``` `current_user` is available in routes and templates, and `login_required` protects private pages. ## Wrapping Up Flask gave Note Blog exactly what it needed: simplicity, control, Python-centric development, and easy extensibility through mature extensions. There was no need for async complexity or API-first design. For traditional server-rendered apps, Flask remains an excellent choice in 2026. For heavy APIs or real-time systems, FastAPI is likely the better tool. That being said, typically for a FastAPI service you also write a Javascript frontend which increases development time (tradeoff). There are ways to do HTTP requests with Flask where you can have part of the Flask webpage repeatedly call an endpoint or leave a connection open (Server Sent Events, or possibly WebSocket). Due to this, I feel comfortable building with Flask, since there is a high chance that I do not need to later migrate my application to FastAPI, and can maintain the benefits of a Python first development environment, where I only need a single server repository. The exception being of course when microservices are added an scaled, but this is a separate issue than the webserver itself. Comments (0) Please log in to comment. No comments yet. Be the first to comment! ← Back to Blog
Comments (0)
Please log in to comment.
No comments yet. Be the first to comment!