Creating a website using htmx and Turso
Learn how to create a dynamic website using htmx and Turso, the edge database
As of late, htmx has been attracting much attention in the web development space as an alternative to React, Svelte, Vue and similar frameworks .
In this tutorial we are going to create a social links listing website using htmx while storing the website's data in Turso. We'll be using the Python micro-framework, Flask, as the back-end to this website.
htmx is a library that allows you to access modern browser features like AJAX, CSS Transitions, WebSockets, and Server-Sent Events directly from HTML, rather than using JavaScript. It allows you to build user interfaces quickly directly in markup.
The fact that it uses hypertext in server-client communications as opposed to the vastly used non-hypermedia format (JSON) that almost always needs to be converted before being placed inside markup (HTML) for consumption, means we get to write less code.
With htmx, rather than only being able to make GET and POST requests via a and form elements, you can use HTML attributes to send GET, POST, PUT, PATCH, or DELETE requests on any HTML element. We'll see examples of this later.
Flask is a lightweight Web Server Gateway Interface (WSGI) web framework designed to make getting started quick and easy. It is one of the most popular Python web applications.
Turso is the edge database based on libSQL, the open-sourced open-contribution fork of SQLite.
The social links sharing website we are going to create is a remake of the FindMeOn project that was previously created with Qwik & Turso.
We are going to add an account deletion as an additional feature to that website.
Let's get to building.
Pre-requisites
To build the website in this tutorial, you need the following installed:
# Creating a Flask project
Let's start by creating a new project workspace by creating and switching into a new project directory.
# create directory
mkdir find-me-on
# switch to directory
cd find-me-on
We'll be working on our project inside a virtual environment to avoid compatibility issues.
So, let's create one and activate it.
# MacOs/Linux
$ python3 -m venv .venv
$ .venv/bin/activate
# Windows
> py -3 -m venv .venv
> .venv\\Scripts\\activate
All the subsequent terminal commands assume a previously activated virtual environment.
Afterwards, install Flask.
pip install Flask
Create the flask app file app.py
, then create a new directory “/templates” and add the file “index.html” in it. Inside app.py
add the following code.
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
if __name__ == "__main__":
app.run(debug=True)
Here, we've imported the Flask class and created an instance of the Flask app. __name__
, passed as the first argument of the Flask class is a convenient shortcut used for the application's module name in most cases. It helps Flask know where to look for resources such as templates and static files.
To the “index.html” file add the following code.
<div>Hello World</div>
Deploy a local server by running flask run.
You should see “Hello Word!” when you open “http://127.0.0.1:5000”.
# Setting up and bundling TailwindCSS and htmx
Since we'll be using TailwindCSS to style the website, let's install the standalone Tailwind CSS CLI — pytailwindcss and download it's binary:
pip install pytailwindcss
tailwindcss
Create a new directory “static/src”, then create a new “main.css” file and in it add the styles found in this file. Run tailwindcss init
to create the Tailwind configuration file.
Inside the Tailwind configuration file, update the content:
property passing ”./templates/*.html”
in the list of content sources.
Run tailwindcss -i ./static/src/main.css -o static/dist/main.css — watch
in a separate terminal to generate the final styles for our app templates, the — watch
flag makes sure the CLI regenerates the styles whenever changes are made to the content source templates.
For htmx, download the library and place the file inside “static/src”.
Let's install and use Flask-Assets to bundle the app's JavaScript and CSS files.
First install Flask-Assets by running.
pip install Flask-Assets
Then, add the following code before the routes inside app.py
to create bundles for JavaScript and CSS files for our flask app.
from flask_assets import Bundle, Environment
# Bundle CSS & JS assets & register
assets = Environment(app);
css = Bundle("src/main.css", output="dist/main.css")
js = Bundle("src/*.js", output="dist/main.js")
assets.register("css", css)
assets.register("js", js)
css.build()
js.build()
Copy the contents inside “base.html” found in this GitHub repository and place them in a similarly named file inside the “/templates” directory.
In the head section of “base.html” you should see how the assets are being imported to the template file.
Update “index.html” with the following code.
{% extends "base.html" %}
{% block content %}
<div>Hello World</div>
{% endblock %}
When you fire-up the local server by running flask run
, you should see the following.
# Setting up Turso
Create a new turso database.
turso db create find-me-on
Create a “.env” file at the root of the project. This is where our database credentials will be stored.
Get the database url.
turso db show - url find-me-on
Assign the database url to the “TURSO_DB_URL” key inside the “.env” file.
TURSO_DB_URL=<obtained-db-url>
Create a database authentication token.
turso db tokens create find-me-on
Assign the database authentication token to the “TURSO_DB_AUTH_TOKEN” key inside the “.env” file.
TURSO_DB_AUTH_TOKEN=<obtained-db-auth-token>
# Setting database models
We'll be using SQLAlchemy as the query builder for the Turso database.
There is a libSQL dialect for SQAlchemy which requires the python packages SQLAlchemy (version 2.0 or later) and libsql_client. They are specified as requirements so pip will install them if they are not already in place. Install the sqlalchemy-libsql library by running:
pip install sqlalchemy-libsql
Create a new models.py
file. Inside this file we'll create our database models.
Add the following code to this file.
from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255))
full_name: Mapped[str] = mapped_column(String(100))
user_name: Mapped[str] = mapped_column(String(50), unique=True)
delete_id: Mapped[str] = mapped_column(String(255))
created_at: Mapped[int]
links: Mapped[List["Link"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"Question(id={self.id!r}, email={self.email!r}, full_name={self.full_name!r}, user_name={self.user_name!r}, created_at={self.created_at})"
class Link(Base):
__tablename__ = "links"
id: Mapped[str] = mapped_column(primary_key=True)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'))
website: Mapped[str] = mapped_column(String(100))
link: Mapped[str]
created_at: Mapped[int]
user: Mapped["User"] = relationship(back_populates="links")
def __repr__(self) -> str:
return f"Choice(id={self.id!r}, user_id={self.user_id!r}, website={self.website!r}, link={self.link!r}, created_at={self.created_at!r})"
In summary, we've created two database models, User and Links, with their specific rows and relationships.
To use SQLAlchemy as Turso's query builder we'll need to construct a special URL that SQLAlchemy can use to locate a Turso database. The URL will be different from the usual HTTP or websocket URLs that you normally use with the libSQL client SDKs. This is because you construct it using the unique scheme — “sqlite+libsql”, the database hostname, and the database token passed as the value of the “authToken” query param.
To create the special URL we'll need to obtain our database credentials from the “.env” file we created earlier. To do so, first install Python-dotenv, which is a library that will help us get the key value pairs from “.env” files to use the above database credentials in our Flask app.
pip install python-dotenv
Then, we can proceed to constructing the database URL. Copy the following code and add it right before routes inside “app.py”.
# Add imports
import os
from sqlalchemy import create_engine
from dotenv import load_dotenv
load_dotenv()
# Get environment variables
TURSO_DB_URL = os.environ.get("TURSO_DB_URL")
TURSO_DB_AUTH_TOKEN = os.environ.get("TURSO_DB_AUTH_TOKEN")
# construct special SQLAlchemy URL
dbUrl = f"sqlite+{TURSO_DB_URL}/?authToken={TURSO_DB_AUTH_TOKEN}"
engine = create_engine(dbUrl, connect_args={'check_same_thread': False}, echo=True)
To create our database tables, add the following “/seed” route.
# imports
from sqlalchemy.orm import Session
from sqlalchemy import select
from models import Base
# Other routes
@app.route("seed")
def seed():
# Create all tables in metadata
Base.metadata.create_all(engine)
redirect(url_for("home"))
Open this file to see the whole seeding code which also includes the addition of a single user record and their links.
To learn more about SQLAlchemy, visit it's docs.
# Constructing the routes
Having created the above seeding route, let's create the remaining ones.
In the website we're creating, we need a homepage and a user details page.
For the home page, we need to render a user information form that gets filled and sent to the server.
Update the template “templates/index.html” by copying and pasting the file's final code from GitHub.
When you observe the form tag in this template file you should see the following code.
<form
class="p-4 flex flex-col space-y-2"
hx-post="{{page_url}}"
hx-target="#response"
>
hx-post
here means that we will issue a POST request whenever the form is submitted to the URL provided by page_url
. hx-target
lets us select the element where we want our response to be placed if other than the element making the request.
You will find the target element with the id=”response”
inside the “base.html” template file.
The Flask route code for this page can be found on GitHub.
In this route, we are first checking whether we've received a GET or POST request. If it's a GET request we return the template form, displaying it to the user. If it's a POST request, we are validating the submitted form data then proceed to adding the new user information to our database.
The utility of htmx is demonstrated in this route since you can see that we are returning partial responses in hypertext format that get to be placed on the target element specified in hx-target
as they are, without the need to be processed as would have been the case with most front-end web frameworks.
In the user details page, we are listing the user's social links as submitted in the index file.
We are also giving the user the option to delete their account details. A user can only do so by providing the delete_id that's returned together with the response of a successful account creation request.
The user information deletion option is contained in the following button that triggers a dialog.
<button
class="p-2 mt-16 outline-1 outline-red-400 text-red-600 text-xs"
hx-post="{{page_url}}"
hx-prompt="Provide delete id"
hx-confirm="Are you sure?"
hx-target="#response"
>
Want to delete your information?
</button>
The hx-prompt
and hx-confirm
attributes in the above button are what trigger the dialog. An AJAX request is triggered when the dialog is accepted, otherwise no request is issued.
The value provided to the dialog is sent to the server inside the HX-Prompt
HTTP header and we can get that value in the back-end by checking that header in our Flask code.
There is an obvious improvement in the rebuilding of the FindMeOn project using htmx since there is a significant reduction in the amount of code written mainly inside the template files since we didn't need to write JavaScript to handle the responses from the server as it was already in hypertext.
htmx like any other framework also has its cons. For example, you can see that we cannot iterate through data with it and had to resort to the template's default iteration feature inside our user links listing template. htmx also wouldn't be recommended if the project you're building requires offline functionality or has many dynamic dependencies.
It is apparent that htmx is not appropriate for every single scenario, but it does offer us an option that cuts out all the excess code that we normally need to write when writing front-ends using JavaScript, for instance. It can assist us write less code, with less dependencies, which in turn helps us manage build sizes and other benefits just as highlighted in this blog.
This brings a close to this tutorial. For more information regarding the technologies used, you should visit the following links: