How I Built A Beautiful Web App Purely in Python — with Zero Experience.

Using FastAPI, Jinja2 and DaisyUI.

Another weekend, another itch to scratch. Personally, I love building things, regardless of the medium — whether it’s hardware, software, or even mechanical woodworking projects. The challenge for someone like me is sticking to one thing or one technology. I have an insatiable urge to build stuff, and I want to do it quickly (which might not be something to boast about). In my view, building web apps requires a fair amount of patience and a willingness to constantly adapt and learn. It’s not my favourite thing to do. Perhaps you can relate.

In my quest to explore a new technology every weekend, as you may have read in my last blog post, I delved into FastAPI — a Python web framework. Instead of diving into yet another new technology, I decided to focus on how I could develop a functional web app using FastAPI and exclusively Python (no JavaScript), aiming to keep complexity to a minimum. Before writing this article, I also dabbled a bit in Vue.js and found it intriguing, but it added complexity, and my proficiency in JavaScript is not very high. Went back to square one.

In this article we will be building on the existing FastAPI project of retrieving neighbouring chunks. Please read my previous article to know more about it.

Technologies explored:

FastAPI — Python Web FrameworkJinja2 — Templating EngineDaisyUI — A component library for Tailwind CSS

If you’re a seasoned web app developer or a programming guru, this article might not offer much new insight. However, if you’re someone curious about the nitty-gritty of building web apps with Python that provides you more control compared to something like Streamlit, or if you’ve ever wondered what goes into those job descriptions for Python web developers, you’re in the right spot. Let’s explore the process together.

Let me be honest with you: building the frontend using FastAPI was a breeze, but integrating tailwindcss (daisyUI) with the app turned out to be the most time-consuming part for me. I could have easily skipped it, but let’s face it — the only reason to build web apps, for me, lies in the freedom to showcase your creativity, and who doesn’t appreciate a stunning user interface? So, without further ado, let’s dive in.

What is DaisyUI?

Everyone admires beautiful user interfaces, but let’s be real — the tedious task of writing CSS styles often discourages people from creating visually appealing frontend for web apps. I might sound like a broken record, but I’ll say it again: I’m lazy when it comes to CSS, and it’s usually the last thing on my to-do list. Art class in school? Let’s just say I barely scraped by. And diving into the intricacies of pixel-perfect styling in CSS doesn’t excite me.

For folks like me (and yes, Tailwind is used in production-grade apps too), Tailwind is a lifesaver. It offers pre-defined styles and templates for HTML components, allowing you to customise them easily by adding specific classes.

Tailwind has gained immense popularity and is now a staple in apps across various domains. If you’re adept at it, you can often spot if an app uses vanilla Tailwind CSS. You have the option to meticulously adjust the Tailwind classes to your preferences or, well, take the shortcut and use a component library tailored for Tailwind. It’s like Tailwind for Tailwind, lol. There are many component libraries available and DaisyUI is one of them.

There are numerous features and advantages to using DaisyUI; you can visit their website to explore more. One of my favourites is the ability to select themes. With a variety of themes to choose from, you can rest assured that you won’t struggle to find the perfect colour scheme.

File Structure of the Web App

|- chroma_db
|- chroma_db
|- static
|- css
|- app.css
|- styles
|- app.css
|- templates
|- index.html
|- files
|- samples.pdf

The chroma_db folder,, functions.pywill be carried forward from the previous project we built. If you haven’t already, I would recommend you to read this article in case you aren’t familiar with FastAPI.

User Interface

Before you get bored with all the technicalities, here is a glimpse of the user interface that should keep you pumped up:

HomepageFetching neighbours

Although it isn’t something you could flex on Behance, it does look pretty good, doesn’t it? Personally, I much prefer it to the default Times New Roman, black-and-white theme, which really puts me off web development.

If you’re thinking this would require a lot of effort, let me assure you: I haven’t touched CSS in ages, and I was able to do it in no time. Let’s see how you can do it too!

Setting up Tailwind for the app

You could either use the CDN for DaisyUI, which makes the process much simpler but isn’t recommended for production use, or install it as a Tailwind plugin. We’ll go with the recommended approach: installing it as a Tailwind plugin. While this method is slightly more challenging and you may encounter some setup issues, who doesn’t love a good challenge?

To install Tailwind, make sure you have NPM (the package manager for JavaScript) installed. You can read the documentation in case you don’t have NPM installed.

In the terminal of the root directory of your project, you will have to run the following commands:

npm install -D tailwindcss
npx tailwindcss init
npm i -D daisyui@latest

Once executed successfully, a configuration file named tailwind.config.js will be created in the root directory of your project.

In the tailwind.config.js we will have to add DaisyUI as a plugin. The file would look like this:

const { default: daisyui } = require(‘daisyui’);

/** @type {import(‘tailwindcss’).Config} */
module.exports = {
content: [“./app/templates/*.html”],
theme: {
extend: {},
plugins: [
daisyui: {
themes: [“light”, “dim”, “acid”],

I wasted quite a lot of time figuring out why the styles weren’t being applied. After spending over 30 minutes, I discovered it was due to an incorrect file path for HTML in the content section. If you encounter a similar issue, check this first. The DaisyUI themes can also be set here.

Now that we have all the formalities completed, it is time to generate a tailwind CSS file. There will be two files mainly: styles/app.css (input file) and static/css/app.css (generated CSS file with all the styles for components with classes).

In the styles/app.css, define the tailwind directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

To generate the tailwind CSS file, run this command:

npx tailwindcss -i ./styles/app.css -o ./static/css/app.css –watch

That’s it. We have successfully installed Tailwind and DaisyUI.

Creating a template using Jinja2

Jinja2 is a templating engine for Python used to create modular and dynamic HTML content in applications. This is particularly helpful since we won’t be using JavaScript at all in our web application. We’ll be mapping information fetched from the API to our HTML template, which will then be rendered using Jinja2. Jinja2 allows us to use expressions to incorporate loops, conditions, filters, variables, and more directly within our HTML. To learn more about these expressions, you can read the relevant article in the Jinja2 documentation.

<!– Check if results exist –>
{% if not results %}
<h2>No Results Found</h2>
{% endif %}
{% if results %}
<h2>Nearest Neighbours</h2>
<div id=”results”>
<!– Nearest neighbours will be displayed here –>
{% for result in results %}
<input type=”checkbox” />
<p>{{ result.page_content[:25]|safe }}…</p>
<p>{{ result.page_content }}</p>
{% endfor %}
{% endif %}

This template employs conditional expressions to verify the existence of results. If results are found, it iterates over them to generate a modal for each one. If you’re familiar with Python programming, grasping this shouldn’t be too challenging.

Linking the Tailwind CSS file to HTML

To activate the tailwind styles, we have to link it to the HTML by adding this line of code in the <head></head>tag.

<link rel=”stylesheet” href=”http://url_for(‘static’,path=’css/app.css’)”>

Adding tailwind classes to the template

I won’t be delving deep into tailwind classes since it would extend this article by quite a margin. You can read about the different utility classes available here. With tailwind classes, the above HTML would look like this:

<div class=”max-w-md mx-auto”>
<!– Check if results exist –>
{% if not results %}
<h2 class=”text-lg font-semibold mb-2 text-info”>No Results Found</h2>
{% endif %}
{% if results %}
<h2 class=”text-lg font-semibold mb-2 text-info”>Nearest Neighbours</h2>
<div id=”results”>
<!– Nearest neighbours will be displayed here –>
{% for result in results %}
<div class=”collapse bg-base-200 mb-4″>
<input type=”checkbox” />
<div class=”collapse-title text-xl font-medium text-primary”>
<p>{{ result.page_content[:25]|safe }}…</p>
<div class=”collapse-content”>
<p>{{ result.page_content }}</p>
{% endfor %}
{% endif %}

In brief, some of the tailwind utility classes denote the following:

text-lg: Large font-sizemb-2: margin bottom with a scale of Background colour of base class. This will differ based on the theme you have selected or the styling set in static/css/app.css.

To select the theme for the entire HTML or just a specific section of the HTML, use the data-theme attribute.

For eg.

<html data-theme=”cupcake”></html>


<html data-theme=”dark”>
<div data-theme=”light”>
This div will always use light theme
<span data-theme=”retro”>This span will always use retro theme!</span>

Using the API to fetch data

As our API is all set for use, as we created in my previous tutorial, we now need to fetch data to the HTML. To accomplish this, we’ll need to make some simple modifications to our file, and then we’ll be good to go. But before we dive into that, let’s create an HTML form that will act as a trigger to fetch data.


<div class=”max-w-md p-8 mx-auto mb-8 rounded-md shadow-md bg-neutral”>
<form id=”query-form” method=”post” action=”/neighbours/” class=”flex flex-col mb-6″>
<div class=”mb-4″>
<input type=”text” placeholder=”Query” id=”query” name=”query” required
class=”input input-ghost w-full max-w-xs” />
<div class=”mb-4″>
<input type=”range” min=”1″ max=”5″ value=”3″ class=”range” step=”1″ name=”neighbours” id=”neighbours” />
<div class=”w-full flex justify-between text-xs px-2″>
<button type=”submit” class=”btn btn-accent”>Submit</button>

Most of it consists of DaisyUI components. The crucial point to remember is the action attribute in the form, which should point to the /neighbours endpoint of our API and use the POST method.

FastAPI App

To render the Jinja2 template, we’ll need to use the fastapi.templating module.

from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory=”templates”)

Since the HTML templates are rendered dynamically, the folder containing the CSS styles or any other static resources needs to be mounted. This allows us to access the folder within the template.

from fastapi.staticfiles import StaticFiles

app.mount(“/static”, StaticFiles(directory=”static”), name=”static”)

The only tweaks needed for the endpoint functions are to change the response class to HTMLResponse, as we’ll be rendering a webpage instead of a JSON object.

@app.get(“/”, response_class=HTMLResponse)
async def main(request: Request):
return templates.TemplateResponse(“index.html”, {“request”: request})

# Fetch neighbours“/neighbours/”, response_class=HTMLResponse)
async def fetch_item(request: Request, query: str=Form(…), neighbours: int=Form(…)):
embedding_function = SentenceTransformerEmbeddings(model_name=”all-MiniLM-L6-v2″)
db = Chroma(persist_directory=”./chroma_db”, embedding_function=embedding_function)
results = db.similarity_search(query, k=neighbours)
return templates.TemplateResponse(“index.html”, {“request”: request, “results”: results})

FastAPI forms don’t support Pydantic models and instead use Form methods to parse data posted by the form. In the function parameters, query: str = Form(…) denotes that the endpoint expects a form field named query, which is a string. When the function is called, it will return a TemplateResponse and pass the results to the template.

Tada! We’re done. That was definitely one of the easiest ways to build a web app in Python without Streamlit. It could have been even easier if I had opted for a basic ‘Hello World’ application without any Tailwind or DaisyUI CSS, but where’s the fun in that?

Final User Interface


I tried to be as thorough as possible, detailing every important key aspect required to build a web app in Python using FastAPI, with a little twist. If you encounter any issues while building the frontend, you can always rely on ChatGPT (needless to say) and ask it to craft a frontend for you, just as I did. A useful tip would be to provide a sample code from the Tailwind documentation and ask it to integrate it into your app. Once you have a robust boilerplate, you can reverse engineer it and customize it to your liking. This approach simplifies the process and makes it more engaging than spending time reading the entire documentation.

Perhaps next weekend, I’ll proceed to build an entire RAG application using Llama3 or add a file-upload feature to this one. I don’t have experience in implementing and handling file uploads from scratch, so that should be interesting.

I hope you enjoyed reading this tutorial. Since it was written by an inexperienced person, it should be easier to comprehend as well. Ciao!

How I Built A Beautiful Web App Purely in Python — with Zero Experience. was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.