Python Flask SQLite JavaScript Complete

Habit Tracker

A full-stack web application for building and tracking daily habits. Flask REST API, SQLite persistence, a streak algorithm designed from first principles, 25 automated tests, and a vanilla JavaScript single-page frontend.

Habit Tracker app screenshot

Overview

Habit Tracker is a full-stack web application that lets users build daily habits, check them off each day, and track progress through streak counters. Built from scratch using Python, Flask, SQLite, and vanilla JavaScript, with no frameworks, no external UI libraries, and no deployment server required.

The goal was to build something real: a working product with a database, a REST API, a tested backend, and a functional frontend, all connected and running together. The project started with a written spec before any code was written and used test-driven development for the algorithmic core.

Today View

Every habit with a color-coded dot, current streak count, and a checkbox. Clicking logs the habit as complete; clicking again undoes it. The streak updates immediately.

Detail Modal

Clicking a habit opens current streak, longest streak, and full completion history. Edit and Delete buttons included.

Add / Edit Modal

Create or update a habit with a name, optional description, and a color picker. All data persists in a local SQLite file.

Tech Stack

Python 3 Flask SQLite Vanilla JavaScript HTML / CSS pytest

Architecture

Three key decisions shaped the design:

SQLite (habits + completions)
Flask REST API
Vanilla JS SPA
  • Single-page, API-driven frontend. Flask serves one HTML file. After that, everything is fetch() calls. No full-page reloads, no Jinja templating for data. The frontend is fully decoupled from the backend.
  • Pure functions for streak logic. streaks.py takes a list of date strings and returns an integer with no database calls, no side effects, no dependencies. This made it trivially testable and kept the algorithmic logic isolated from the web layer.
  • Database-level constraint enforcement. A UNIQUE(habit_id, completed_date) constraint makes it physically impossible to log the same habit twice on the same day. The database enforces the rule, not the application code.

The Streak Algorithm

The most interesting engineering problem in the project. A naive approach of counting all completions gives the wrong answer: a habit logged 100 times over two years but not at all this month should have a streak of 0, not 100.

Key insight: walk backwards through time from today, counting only consecutive days. The algorithm is a backwards traversal, not a count.
Edge case: if it is 7am and you have not checked in yet today, your streak should not break just because today has not been logged. The function starts from yesterday if today has no entry, so the streak stays intact until you check off today (which adds to it) or until the next morning if you forgot (which breaks it).

Longest streak is a separate function that scans the entire history for the longest consecutive run, correctly handling gaps of any size.

REST API

7 endpoints, all returning JSON with appropriate HTTP status codes:

Method Route Description
GET /api/habits All habits with today's status and current streak
POST /api/habits Create a habit
PUT /api/habits/<id> Update name, description, color
DELETE /api/habits/<id> Delete habit and all its history
POST /api/habits/<id>/complete Log today as complete
DELETE /api/habits/<id>/complete Undo today's completion
GET /api/habits/<id>/stats Current streak, longest streak, full history

Testing

25 tests total, all passing.

  • 14 unit tests (test_streaks.py): test the streak functions in complete isolation. No Flask, no database, no HTTP. Just Python functions called with inputs. Covers empty history, single entries, consecutive days, gaps, duplicate dates, and longest-streak tracking. These run in milliseconds and never touch the filesystem.
  • 11 integration tests (test_api.py): test the full stack from HTTP request through Flask route to SQLite and back. Each test runs against a fresh temporary database, deleted after the test. Covers habit CRUD, completion toggling, idempotency of double-completion, the completed_today flag, stats endpoint, and 404 handling.
  • Test-driven development for the streak module. Tests were written first and verified to fail before the implementation was written. This forced thinking about edge cases (duplicate dates, "streak alive if today not logged") before any code existed. Tests written after-the-fact tend to only cover the happy path.

Frontend

The entire frontend is a single JavaScript file (~130 lines) with no framework, no build step, and no node_modules. The HTML file is a static shell with no data. JavaScript fetches from the API on load and after every action, rebuilds the DOM via innerHTML, and re-attaches event listeners after each render.

This is the same pattern React and Vue use, just without the framework. Writing it by hand made the data flow explicit and visible, and required solving problems that frameworks typically abstract: event delegation on dynamic elements, XSS prevention on user-provided content, and state management via API round-trips.

Challenges & What I Learned

  • Streak counting is a backwards traversal problem, not a counting problem. The insight that you walk backwards from today rather than forwards from the first entry is the key to getting it right. Designing this upfront in the spec prevented a re-write mid-project.
  • Pure functions are easier to test. Moving streak logic into its own module with no dependencies made it possible to write 14 focused tests in minutes. If that logic lived inside a Flask route, you would need a running server to test any of it.
  • Database constraints as business rules. The UNIQUE constraint enforces correctness everywhere automatically. It cannot be bypassed by a bug in application code, and it does not need to be re-implemented in every layer that touches the data.
  • Flask blueprints and SQLite context managers. Splitting routes across files via blueprints keeps each module focused. Using with get_db() as conn gives automatic commit/rollback without manual transaction management.
  • XSS prevention in vanilla JS. Any user-provided text injected via innerHTML must be escaped. If someone names their habit <script>alert(1)</script>, the app should display that text, not execute it.