The following details my experience writing and deploying my first web app. I’m just going to free write this one, with no planned direction or otherwise.
The Project
So me and a few guys from town have been talking about starting a band. We are all exceptional players, share similar musical interests, and all have experience in playing shows and the local scene. So this is an exciting thing - and I really hope it works out. Now some of the guys have already written either entire songs or riffs. So I was asked to learn a few things. The audio was sent over facebook - yet facebook will not let me download it in its raw form. That means I can’t slow it down to get a better listen on the faster riffs, I can’t loop a drumtrack in a DAW to just work overtop of it. I find myself with a 30 second drum beat I have to manually repeat each time it finishes playing, which means I can’t even start right on the first beat as it takes time to press the button, get my hand back in place to start playing.
I love a good excuse for a project- Especially if it can solve a problem and/or help me gain more experience in something - So naturally I decided to brainstorm some excuses to validate doingsomethingto solve this file sharing situation amongst me and the guys. I know there are a billion cloud sharing solutions out there, I’ve even written in detail about one of them (rsync + pixeldrain api), but I’ve been meaning totake python and apply it to a web app project. Plus, it would be pretty cool to have a platform unique to us, which can be altered and tweaked in any way we decide would be beneficial. And it’s always nice to be able to call something your own.
While I don’t always fallow my own advice, it is good practice to have a plan. Some set of goals, and an outline of how you can reach them, especially with respect to any sort of software development. There is nothing worse than getting to a point ain developing something just to realize it is not the best way to be handling it. With a little forethought, a lot of headaches can be saved. When I find myself in those scenarios, I try to stay positive and tell myself ‘there is progress in the struggle’. Navigating those scenarios can be a test of patience and resilience, and almost every time you are faced with hardship you will get through it gaining some kind of insight - even if its that familiar voice in your head reminding you you jumped the gun again!
The goal here is quite straight forward and clear. I wanted to develop a platform from scratch either using go or python. I went with python and flask. The objectives include:
- Easily upload and download files, accessible via my webserver
- Authentication such anything we dont want leaked is secure
- Ways to sort the files.
Now if were to have left it at that,I am still convinced I would be severly overlooking the amount of work this would be. It is so incredibly easy to overlook just how much thought needs to be put into the simplest of things. Now if you are like me, you are likely thinking the entire time how one might be able to exploit a web app like this as well - and we will be getting into that in a bit.
Ultimately, the reason I’ve titled this post as a swift reality check is because, while no specific aspect of this was insanely complicated, it felt at times while debugging issues and facing problems like there were a ton of really easy things making it borderline exhausting.
Lesson #1 — Directory Structure is Mandatory ✅
Planning a project makes all the difference, yet if you are unfamiliar with the framework it is hard to plan effectively. I had never written a Flask app, though I had plenty of Python and shell experience, and I knew that keeping code modular and logically structured is critical.
With a CLI project I would simply drop functions into neatly named modules and call it a day. Flask, however, expects a specific layout for static assets, templates, and application logic; the moment you deploy behind a production server, you discover just how rigid that expectation is.
Lesson Learned:
Adopt a minimal but opinionated directory structure from the start.
project_root/
├── static/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── script.js
├── templates/
│ ├── base.html
│ ├── files.html
│ └── upload.html
└── app.py
app.py
is the brain.
- templates/ provide the skeleton.
- static/ holds appearance and interaction.
Lesson #2 — The Tailwind CSS Priority Nightmare
Tailwind sounded perfect—utility-first, no custom CSS, clean and structured. I did not anticipate how aggressively it overrides styles.
Why Won’t My Background Change?
I tried to change the background of the mobile file-card component but nothing inside style.css worked. Tailwind’s inline utility classes carried higher specificity.
Fixes I Learned the Hard Way
- Use !important as a last resort
.file-card {
background-color: #2d2d2d !important;
}
Override Tailwind with Tailwind
Add your own utility class (e.g., bg-gray-900
) directly in the template.
Check Tailwind Defaults in DevTools
Tailwind often injects bg-white
or bg-gray-800
. Inspect, then override.
Lesson: Tailwind is powerful but stubborn; fight utility classes with utility classes.
NOTE: !important is not just limited to Tailwind CSS. In fact, it is a built in property of CSS. When you are struggling with CSS styles, for which you cannot seem to determine after debugging a page (with developer tools for instance), a quick fix is utilizing !important after a CSS statement but before the semicolon which will specify the end of that statement.
🛡️ Challenge 3 — Security Must Be Considered:
File uploads are a prime attack vector. Coming from pure scripting, I was not used to thinking about:
- Extension spoofing (
file.txt.exe
)
- Overwriting existing files
- Remote-code execution (RCE)
- Odd edge cases (spaces, no extension, special characters)
Security Measures Implemented
Sanitising filenames
from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)
filepath = os.path.join(UPLOAD_FOLDER, filename)
file.save(filepath)
Allow only specific types
ALLOWED_EXTENSIONS = {'jpg', 'png', 'mp4', 'mp3', 'pdf'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
Handle missing extensions or spaces
if not file.filename or '.' not in file.filename:
flash("Invalid file. Files must have an extension.", "error")
filename = filename.replace(" ", "_") # convert spaces
Lesson: Never trust user input; secure_filename()
is non-negotiable.
🔍 MIME Types and MIME-Type Spoofing
Even when your file names and extensions look safe, the MIME type reported by the client can be forged. Browsers set the Content-Type header when uploading — e.g., image/png — but a malicious user can upload a PHP shell renamed to cute.png while still claiming the file is image/png.
Why does this matter?
If your reverse proxy or a future code refactor decides to serve uploaded files directly, the web server may evaluate or embed the payload instead of treating it as plain data.
Defences Implemented
- Server-side MIME sniffing
import magic # python-magic
mime_type = magic.from_file(filepath, mime=True)
if mime_type not in {
'image/jpeg', 'image/png', 'audio/mpeg',
'video/mp4', 'application/pdf'
}:
os.remove(filepath)
flash("Disallowed MIME type!", "error")
return redirect(url_for("upload"))
Storing outside the web root
All user uploads reside in /srv/uploads; Nginx never serves that path directly.
Force-download Content-Disposition
When a file is served, the response header includes Content-Disposition: attachment so the browser treats everything as a download, not something to render or execute.
Extension ⇄ MIME double-checks
The extension and the server-detected MIME type must match an approved mapping.
Lesson: A valid file name is only half the battle; verify the bytes, not the label.
⚙️ Challenge 4 — User-Experience Details
Backend logic was easy; an intuitive UI was harder.
Fixes for Better UX
Sorting table columns
function sortTable(columnIndex) {
const table = document.getElementById("filesTable");
const rows = [...table.tBodies[0].rows];
const ascending = table.dataset.sortAsc === "true";
rows.sort((a, b) => {
const cellA = a.cells[columnIndex].textContent.trim().toLowerCase();
const cellB = b.cells[columnIndex].textContent.trim().toLowerCase();
return ascending ? cellA.localeCompare(cellB) : cellB.localeCompare(cellA);
});
table.dataset.sortAsc = ascending ? "false" : "true";
rows.forEach(row => table.tBodies[0].appendChild(row));
}
Dropdown action menu for mobile
function toggleDropdown(button) {
const card = button.closest(".file-card");
const dropdown = button.nextElementSibling;
document.querySelectorAll(".file-card .action-menu").forEach(menu => {
if (menu !== dropdown) {
menu.classList.add("hidden");
menu.closest(".file-card").classList.remove("expanded-card");
}
});
dropdown.classList.toggle("hidden");
card.classList.toggle("expanded-card");
}
Lesson: Smooth UI takes as much effort as backend code.
⚙️ Challenge 5 - From Basic File Handling to Scalable Storage
📂 File Management Evolution
Version 1 — Direct File-System Reads
UPLOAD_FOLDER = "uploads"
@app.route("/files")
def list_files():
files = os.listdir(UPLOAD_FOLDER)
return render_template("files.html", files=files)
Problems: zero metadata, no user tracking, inefficient.
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class UploadedFile(db.Model):
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
user = db.Column(db.String(100), nullable=False)
upload_time = db.Column(db.DateTime, default=datetime.utcnow)
file_size = db.Column(db.String(50))
file_path = db.Column(db.String(255))
Benefits: metadata, user association, faster look-ups.
Problem: concurrency limits.
Version 3 — PostgreSQL for Production
app.config['SQLALCHEMY_DATABASE_URI'] = \
"postgresql://username:password@localhost/mydatabase"
db.init_app(app)
Improvements: scalability, JSON fields, full-text search.
In progress: migrate existing data seamlessly.
🏁 Conclusion
Writing a Flask web app forced me to juggle Python, HTML, CSS, JS, SQL, Nginx, TLS, monitoring, and actual user feedback. None of those tasks alone were brutal; the combination was a wake-up call. Yet every hurdle—directory layout, Tailwind quirks, security paranoia, deployment —left me with tooling and intuition I simply didn’t have before.
Would I do it again? Absolutely.
Would I change my estimates next time? Also absolutely.
If you’re about to tackle your first full-stack project, remember this:
- The simple parts will multiply.
- Users break things in ways you can’t imagine.
- A clear-eyed security mindset saves you days later.
- Celebrate small wins; they snowball into momentum.
While this article barely covers the myriad of things i encountered, it certainly gives one an idea as to how easy it is to underestimate even the simplest web app projects.