Build a blog platform with Flask: writing and showing posts

In this series, we'll use Flask and PostgreSQL to build a blog platform. We'll add users and authentication, comments, tagging, and tackle deployment of our app using AWS.

I'm writing this series of blog posts for my students who are learning Flask and web development, and want to follow along with a more comprehensive project.

If you're totally new to Flask, Python, or web development, this may not be for you!

In this post we'll begin by:

  • Setting up the Flask app
  • Setting up the database
  • Creating posts
  • Displaying posts

To do this, we'll be using:

  • Python
  • PostgreSQL
  • HTML (no CSS yet!)
  • And a few Python libraries:
    • Flask (and Jinja)
    • SQLModel

Install the required dependencies for this post

Throughout the app we'll be using a few different libraries, but in this post we just need to install two:

pip install flask
pip install sqlmodel

Set up a Flask app using the factory pattern

The factory pattern in Flask allows us to defer creation of the Flask object until we call the function that creates it.

This is helpful because we can easily call the function ourselves to create the Flask object, and we can pass configuration parameters when we do that. This will come in handy when we get to testing the app, as in our tests we may want to pass different configuration to the app than in production.

This is how we'll create our Flask app:

from flask import Flask


def create_app():
    app = Flask(__name__)

    return app

We can run the app just by typing flask run in the terminal. The Flask command-line interface will see that a function called create_app exists, will run it to get our Flask app. It only works with this function name because that's the convention for Flask apps.

Create a Flask Blueprint for our Post routes

As apps start to grow, as our one will, it makes sense to split the route definitions into multiple files. Often we use Blueprints to help with that.

Let's create a routes/post.py file. Inside it, we'll define our blueprint:

from flask import Blueprint, request

post_pages = Blueprint("posts", __name__)


@post_pages.get("/post/<string:title>")
def display_post(title: str):
    return "Display post page."


@post_pages.route("/post/", methods=["GET", "POST"])
def create_post():
    if request.method == "POST":
        pass
    return "Create post page."

To define our blueprint we passed 2 parameters:

  • name, which is used by the url_for function so we can get the endpoints. More on this later.
  • import_name, which is usually __name__, and is used to tell Flask where the Blueprint is located relative to the root path of the application.

There are many other parameters we can pass, but the defaults for the other parameters are what we want.

We have two routes or endpoints defined:

  • /post/, which we'll use to either:
    • Display a form that users can submit to create posts
    • Accept form data when users submit the form, and create the post in our database
  • /post/<string:title>, which will receive a post title and display the post page

For now both endpoints do very little: just show a string that tells us what route we've accessed.

Note that the /post/ route has some logic in there to do something if the request.method == "POST". The request method is what we'll use to determine what to do: if it's POST we will get the form data, and if it's GET we will show the form page.

Register the blueprint with our Flask app

If we run the Flask app right now, it still can't do anything. That's because the blueprint and the app are not linked.

We need to register the blueprint so the Flask app can access its routes, and we can make requests to them from our browser.

To do so, we simply import the blueprint in our app, and register it:

from flask import Flask
+from routes.post import post_pages


def create_app():
    app = Flask(__name__)
+   app.register_blueprint(post_pages)

    return app

Now if we run the app with flask run, the endpoints will work!

Create the form template to add new posts

There are many ways to create a form with Flask and Jinja, from coding it using plain HTML yourself, to using Jinja macros or even libraries like Flask-WTF.

I recommend using Flask-WTF when your app gets a bit larger or if you need some security in your forms. Because the forms submitted in this app are not very important, we can do without it.

I'll code the form using just HTML.

I've created a templates folder, and inside it I'll place new_post.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>New Post</title>
</head>
<body>
  <form method="POST">
    <label for="title">Title:</label>
    <input type="text" name="title" id="title" />

    <label for="content">Content: </label>
    <textarea name="content" id="content"></textarea>
    
    <input type="submit" value="Create post" />
  </form>
</body>
</html>

I've kept it simple for now. Later on when we want to add styling to the form, we'll need to change things a bit.

A lot of the HTML here is boilerplate. Inside the body, we've got a form element which uses a POST request to send data to the current page.

In the form, we have two fields: the title text and the content textarea.

All this together means that when we access /post/ using the browser, we'll display this form. When we submit the form, the content of those two fields will be submitted as a POST request to the /post/ endpoint.

The name property of each field is used when submitting form data, so the submission will look like this: title=TITLE_TEXT&content=CONTENT_TEXT.

Let's go to our Flask endpoint and deal with rendering the template and getting the form data:


from flask import Blueprint, render_template, redirect, url_for, request # New imports added

...

@post_pages.route("/post/", methods=["GET", "POST"])
def create_post():
    if request.method == "POST":
        title = request.form.get("title")
        content = request.form.get("content")
        # TODO: We can create the post in our database here
        return redirect(url_for(".display_post", title=title))
    return render_template("new_post.html")

I've left a "TODO" comment because we still have to handle the database interaction there. For now though, we get the form contents and redirect to our other endpoint, passing the received title as an argument.

As an argument to url_for we pass ".display_post", and that will calculate the URL for the display_post function in the current blueprint (that's what the . means).

Display posts using a Jinja template

Although we used a "template" in the new_post.html file, we didn't actually write any Jinja code there. Technically, it's not really a template. It's just an HTML file that we're sending the client.

Let's use Jinja to display posts. I'll create a templates/post.html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
</head>
<body>
  <article>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </article>
</body>
</html>

And again, for now, it's very simple. Just an article element with a title (h1) and content (p). Later on, when we use markdown, we'll be getting the post content as HTML from Flask, and we'll insert that directly into the template.

In our Flask app all we have to do is get the post title and content, and pass that in to our render_template call:

@post_pages.get("/post/<string:title>")
def display_post(title):
    content = "..." # How do we get the content?
    return render_template("post.html", title=title, content=content)

In this endpoint we have the post title, so we can pass that to the template. We don't have access to the post content yet though.

Before we can get access to the post content (and any other info about the post, such as publishing date), we need a database.

As a side note, the syntax @post_pages.get(...) is new in Flask 2.0. If you haven't seen it before, check out this link!

Create a SQLModel model to define the database table

We'll be using the SQLModel library, which itself uses SQLAlchemy and Pydantic. With this library, we can define a single class that acts as a database table definition.

I'll make models/post.py, and write this code:

from typing import Optional
from datetime import datetime
from sqlmodel import Field, SQLModel


class Post(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    content: str
    publish_date: datetime = Field(default_factory=datetime.today)

What this tells SQLAlchemy is to create a post table with 4 columns:

  • id, which will be the primary key and auto-generated.
  • title, a non-null, unlimited-length VARCHAR.
  • content, a non-null, unlimited-length VARCHAR.
  • publish_date, a DATETIME which will, when we create a Post object in Python, get the value of datetime.today().

With this we have our table defined, and we'll get a publishing date populated for us. Nice!

Let's use this model in our blueprint. It's very easy!

Connect to the database using SQLModel

Let's run create_engine from SQLModel to actually connect to the database and give us something that we can use to interact with it.

Let's go to app.py:

+from sqlmodel import SQLModel, create_engine
+from models.post import Post

def create_app():
    app = Flask(__name__)
+   app.engine = create_engine("sqlite:///database.db")

Now that we've got that, we have to use the app.engine to create our table. The best way I've found is to make sure the table exists before you deal with any requests. I'll add this before registering the blueprint:

@app.before_first_request
def create_db():
    SQLModel.metadata.create_all(app.engine)

It's important that before we run that, the Post model has been imported. Otherwise SQLModel won't know what tables to create.

Use SQLModel to add posts to the database

Now that we've got that, we can go to our blueprint and make use of the engine variable there, together with SQLModel's Session, to interact with the database.

We need some new imports in routes/post.py:

from flask import current_app  # Add this to the existing flask imports
from sqlmodel import Session, select
from models.post import Post

Let's begin with adding posts:

@post_pages.route("/post/", methods=["GET", "POST"])
def create_post():
    if request.method == "POST":
        title = request.form.get("title")
        content = request.form.get("content")
        with Session(current_app.engine) as session:
            session.add(Post(title=title, content=content))
            session.commit()
        return redirect(url_for("display_post", title=title))
    return render_template("new_post.html")

The new code here is:

with Session(current_app.engine) as session:
    session.add(Post(title=title, content=content))
    session.commit()

We first create a session by giving it the engine we want to use.

A database session then allows us to call multiple database interactions. Here we're just doing one: session.add(). We could do more, and they would all run when we do session.commit().

This means that if we want to add a lot of data at once, it's faster because we don't have to save to the database disk every time. It only happens on commit.

Note that what we added to the session isn't some SQL code, but a Post model object. SQLModel will take care of turning that into the appropriate SQL query!

Retrieve posts from the database using SQLModel

We can do something similar in our display_post endpoint, using SQLModel's select function to fetch data from the database:

@post_pages.get("/post/<string:title>")
def display_post(title):
    with Session(current_app.engine) as session:
        statement = select(Post).where(Post.title == title)
        post = session.exec(statement).first()
        return render_template("post.html", title=title, content=post.content)

Here we create a session once again, and then use it to fetch a post matching the title provided.

The statement holds the SQL query, but it doesn't actually run until we execute it with session.exec().

Running .first() in a result set gives us just the first row returned. Here we expect to only have one post matching the title provided.

I've placed the render_template call inside the context manager because if anything fails we won't have any data to display. We could wrap this in some error handling and display a different template if anything goes wrong with the database connection.

Next steps

We've done a lot in this post! We have:

  • Set up our app.
  • Set up our SQLite database using SQLModel.
  • Created our first un-styled templates.
  • Added a blueprint for handling creating and displaying posts.

In the next post in this series we'll handle user authentication, so that we can register users as we'll need that for the authors and comments parts of this system.

If you enjoyed this post and want to learn more about web development using Python, consider joining our Web Developer Bootcamp with Flask and Python. It's a complete video-course that guides you through building multiple projects!