A Beginner's Guide to Creating a Simple Blog Manager from a Single File.
Flask is a "micro" web framework for Python, meaning it provides the essentials for web development without imposing a rigid structure. It's perfect for learning and for building everything from small projects to large, complex applications.
In this tutorial, we will build a complete **Blog Post Manager** that demonstrates the four fundamental **CRUD** operations: **C**reate, **R**ead, **U**pdate, and **D**elete posts using Flask and Python's built-in SQLite database engine.
To create a web application where a user can view a list of blog posts, add new posts, edit existing ones, and delete them.
Let's get our project folder and dependencies ready.
Create a new folder for your project. Inside it, create a file named `app.py` and a folder named `templates`.
mkdir flask_blog
cd flask_blog
touch app.py
mkdir templates
It's highly recommended to use a virtual environment. Open your terminal in the `flask_blog` folder and run:
# Create and activate a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
# Install Flask
pip install Flask
We'll use a separate script to create our database and the `posts` table. This is a clean way to handle one-time setup tasks.
Create a new file named `init_db.py` in your project folder.
# init_db.py
import sqlite3
connection = sqlite3.connect('database.db')
with open('schema.sql') as f:
connection.executescript(f.read())
connection.commit()
connection.close()
This script requires a file named `schema.sql`. Create `schema.sql` and add the SQL to define our table:
-- schema.sql
DROP TABLE IF EXISTS posts;
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
content TEXT NOT NULL
);
Now, run the script from your terminal to create the `database.db` file:
python init_db.py
This is where all our application logic will live. We will build this file up, section by section. Here is the initial setup and the "Read" functionality.
Open `app.py` and add the following code:
import sqlite3
from flask import Flask, render_template, request, url_for, flash, redirect
from werkzeug.exceptions import abort
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your secret key' # Change this for production
# Function to get a database connection.
# This function will be used by all our routes.
def get_db_connection():
conn = sqlite3.connect('database.db')
conn.row_factory = sqlite3.Row # This allows us to access columns by name
return conn
# Function to get a single post by its ID
def get_post(post_id):
conn = get_db_connection()
post = conn.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone()
conn.close()
if post is None:
abort(404) # Abort with a 404 Not Found error if post doesn't exist
return post
# --- Routes ---
@app.route('/')
def index():
conn = get_db_connection()
posts = conn.execute('SELECT * FROM posts ORDER BY created DESC').fetchall()
conn.close()
return render_template('index.html', posts=posts)
# ... We will add more routes for Create, Update, and Delete below ...
Now let's create the HTML files in the `templates` folder that our Flask app will render.
This file contains the common HTML structure that all other pages will extend.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %} {% endblock %} - FlaskBlog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/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') }}">FlaskBlog</a>
</div>
</nav>
<div class="container mt-4">
{% for message in get_flashed_messages() %}
<div class="alert alert-success">{{ message }}</div>
{% endfor %}
{% block content %} {% endblock %}
</div>
</body>
</html>
This template displays all the posts.
{% extends 'base.html' %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
<a href="{{ url_for('create') }}" class="btn btn-primary mb-3">Create New Post</a>
{% for post in posts %}
<div class="card mb-3">
<div class="card-body">
<h2>
<a href="{{ url_for('edit', id=post['id']) }}">
{{ post['title'] }}
</a>
</h2>
<span class="text-muted">{{ post['created'] }}</span>
</div>
</div>
{% endfor %}
{% endblock %}
Now let's add the routes and templates for the remaining CRUD operations.
Append the following routes to your `app.py` file.
@app.route('/create', methods=('GET', 'POST'))
def create():
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
if not title:
flash('Title is required!')
else:
conn = get_db_connection()
conn.execute('INSERT INTO posts (title, content) VALUES (?, ?)', (title, content))
conn.commit()
conn.close()
flash('"{}" was successfully created!'.format(title))
return redirect(url_for('index'))
return render_template('create.html')
@app.route('/<int:id>/edit', methods=('GET', 'POST'))
def edit(id):
post = get_post(id)
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
if not title:
flash('Title is required!')
else:
conn = get_db_connection()
conn.execute('UPDATE posts SET title = ?, content = ? WHERE id = ?', (title, content, id))
conn.commit()
conn.close()
flash('"{}" was successfully updated!'.format(title))
return redirect(url_for('index'))
return render_template('edit.html', post=post)
@app.route('/<int:id>/delete', methods=('POST',))
def delete(id):
post = get_post(id)
conn = get_db_connection()
conn.execute('DELETE FROM posts WHERE id = ?', (id,))
conn.commit()
conn.close()
flash('"{}" was successfully deleted!'.format(post['title']))
return redirect(url_for('index'))
{% extends 'base.html' %}
{% block title %}Create a New Post{% endblock %}
{% block content %}
<h1>Create a New Post</h1>
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea name="content" class="form-control" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block title %}Edit "{{ post['title'] }}"{% endblock %}
{% block content %}
<h1>Edit "{{ post['title'] }}"</h1>
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" class="form-control" value="{{ request.form['title'] or post['title'] }}">
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea name="content" class="form-control" rows="5">{{ request.form['content'] or post['content'] }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<hr>
<form action="{{ url_for('delete', id=post['id']) }}" method="post">
<input type="submit" value="Delete Post" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this post?')">
</form>
</form>
{% endblock %}
Your application is now complete! To run it, go to your terminal (make sure your virtual environment is activated) and use the `flask` command.
# The --debug flag enables auto-reloading when you save changes
flask --app app run --debug
Now, open your web browser and navigate to the address provided (usually http://127.0.0.1:5000).
You should see your blog application. You can now create new posts, see them on the main page, click on them to edit, and delete them from the edit page.
You have successfully created a complete, database-driven web application using only Flask and Python's standard libraries. This is a powerful demonstration of how quickly you can build functional web apps with Flask.
You learned how to:
This project is a solid foundation. You can now explore adding features like user authentication, comments, or using a more advanced database tool like SQLAlchemy.