DEV Community

Cover image for Day 85 of #100DaysOfCode — Building a Mini Flask App: Expense Tracker
M Saad Ahmad
M Saad Ahmad

Posted on

Day 85 of #100DaysOfCode — Building a Mini Flask App: Expense Tracker

Flask learning is done. Today I built a complete mini Flask application from scratch: an expense tracker where users can register, log in, add expenses, filter them by category, and see a running total. Everything from days 78 to 84 came together in one day of building. Here's how it's architected and how it works.


Why an Expense Tracker

I wanted something small enough to finish in a day but real enough to actually use. An expense tracker hits that balance, it solves a genuine problem, it uses every Flask concept I've covered, and it's different enough from DevBoard and the other things I've built that it shows range rather than repetition.

Structurally, it's similar to a to-do app: a list of items that belong to a user. What pushes it beyond tutorial territory is the fields on each expense, the category filter, and the running total. Those three things make it feel like a real tool rather than a practice exercise.


What the App Does

The app has two sides: authentication and expense management.

On the authentication side, users can register with a username, email, and password. They can log in, stay logged in with a remember me option, and log out. Unauthenticated users are redirected to the login page.

On the expense side: logged-in users can add an expense with an amount, category, optional description, and date. They see all their expenses in a list, newest first, with the total at the top. They can filter the list by category, and the total updates will reflect only the filtered expenses. They can delete any expense. That's the entire feature set. Nothing more was needed.


Project Structure

The app lives in five Python files and a templates folder.
app.py is the entry point. It creates the Flask app, configures the database connection and secret key from environment variables, initializes SQLAlchemy, Flask-Migrate, and Flask-Login, registers the user loader function, and imports routes at the very bottom.

models.py defines the two database tables: User and Expense. User has the standard authentication fields. Expense has an amount, category, description, date, and a foreign key linking it to the user who created it. The User model inherits from UserMixin for Flask-Login compatibility and has methods for setting and checking the password hash.

forms.py defines the three forms: RegisterForm, LoginForm, and ExpenseForm. RegisterForm validates the uniqueness of username and email by querying the database during validation. LoginForm is straightforward. ExpenseForm has a SelectField for category with hardcoded choices, no separate Category model needed at this scale.

routes.py contains all seven route functions: three for auth, four for expenses. It imports from all other files and is itself imported by app.py.

templates/ contains base.html and subdirectories for auth and expense templates.


The Data Models

Two models, one relationship.

The User model stores credentials and links to expenses through a one-to-many relationship. One user has many expenses. Password is never stored in plain text, only the hash generated by werkzeug's generate_password_hash function.

The Expense model stores the actual expense data. The category field is a plain string; the choices are enforced at the form level, not the database level. This keeps the schema simple. The amount is a float. The date is a proper date field, not a string, so ordering and filtering by date works correctly.

The relationship between User and Expense is a standard foreign key, author_id on Expense pointing to the User's primary key. SQLAlchemy's db.relationship on the User side creates a Python-level accessor so you can do current_user.expenses to get all expenses for the logged-in user.


The Authentication Flow

Registration creates a new User, hashes the password, saves to the database, logs the user in immediately, and redirects to the expense list. No email verification, that would be scope creep for a one-day build.

Login queries the user by email, checks the password hash, and calls login_user() with the remember flag. After login, the user is redirected to wherever they were trying to go; the next query parameter from Flask-Login handles this automatically.

Logout calls logout_user() and redirects to login. Three lines.

Every expense route is decorated with @login_required. Unauthenticated requests are redirected to the login page with the original URL stored in ?next= so they return there after logging in.


The Expense List — The Most Interesting Part

The list route does more than just fetch expenses. Here's the full logic:

First, it reads the category filter from the query string; this is optional and defaults to empty. Then it builds a query for all expenses belonging to current_user. If a category filter is set, it adds a .filter_by(category=...) clause. The results are ordered by date descending, so the newest expenses appear first.

Then it calculates the total. Instead of fetching all expenses and summing in Python, it uses SQLAlchemy's db.func.sum() to do the calculation in the database; one query, one number returned. If no expenses match, the sum returns None, so it defaults to zero.

Both the expense list and the total respect the category filter simultaneously, if you filter by Food, you see only Food expenses, and the total reflects only Food expenses. This felt like the most important UX detail to get right.

The template receives the filtered expenses, the calculated total, the current filter value to keep the dropdown showing the right selection, and the list of available categories to populate the dropdown.


The Delete Flow

Delete is intentionally simple. No confirmation page, that would add another route and another template for something that doesn't need it at this scale. Instead, each expense row in the list has a small form with a hidden submit button. Clicking delete submits that form as a POST request.

The delete route fetches the expense by ID, checks that it belongs to current_user, an ownership check that prevents one user from deleting another's expenses by manipulating the URL, then deletes it and redirects back to the list. The category filter is preserved in the redirect, so you don't lose your current filter position after deleting.


The Import Chain and Circular Import Problem

This was the one architectural challenge worth mentioning. Flask apps with multiple files have a specific import order problem.

models.py needs to import db from app.py. routes.py needs to import db and models from app.py and models.py. app.py needs to import routes so Flask registers them. If app.py imports routes at the top and routes imports from app, Python hits a circular import error.

The solution is simple once you know it: import routes.py at the very bottom of app.py, after everything else is defined. By the time Python reaches that line, db, login_manager, and everything else in app.py is already defined, so when routes.py imports from app.py it finds what it needs. Bottom import, problem solved.


What the App Looks Like at the End

Six pages total:

Login and Register — clean centered forms with flash message support and cross-links between the two pages.

Expense List — a table of all expenses with amount, category, description, and date columns. A total at the top. A category dropdown filter above the table. A delete button on each row. An "Add Expense" button in the header.

Add Expense — a form with amount, category dropdown, optional description, and date. Submits and redirects back to the list.

Every page extends base.html, which has the navigation bar showing the current user's name and a logout link.


What I Learned from Building It

Flask rewards keeping things simple. The entire app: models, forms, routes, templates, is readable in under an hour. There's no magic, no framework doing things behind the scenes you don't understand. Every piece is where you put it.

The contrast with Django is real. Django would have had this done faster with ModelForms, class-based views, and the built-in auth system. But Flask forced me to understand every piece: password hashing, session management, query building, the circular import problem, in a way that Django abstracts away. Both are valuable. They teach different things.


What's Next

Flask learning is wrapped up. DevBoard is still being finished in parallel, on the candidate side, API polish, Tailwind, and deployment. A separate post will go up when it's live.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)