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 theurl_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-lengthVARCHAR
.content
, a non-null, unlimited-lengthVARCHAR
.publish_date
, aDATETIME
which will, when we create aPost
object in Python, get the value ofdatetime.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!