For the past 1.5 years, I've been developing TodoX in my free time -- a task manager and time tracker. Now the project is in the state I am not too ashamed to show to other people.
This post is not only a shameless plug 🥸, but a story about reasons for creating TodoX, choosing the stack, testing, DevOps, challenges, good and bad decisions.
The main question is why create one more task manager? There is already a gazillion of them. Three reasons.
First and foremost, for the past 10+ years (with some gaps) I track time spent on most projects, work, etc. Why I do this is a topic deserving a separate post. In short: we reach goals by (1) investing enough time and effort in (2) what matters. The results suffer if any of these two factors lags.
Tracking time increases focus, promotes self-accountability, and allows for better retrospection. In turn, all of this helps me invest time in what matters and not what feels the most urgent or easy right now.
Most productivity tools concentrate on either task management or time tracking, leaving out the second part, or having it only as an afterthought. In addition, larger tools were too bloated with unnecessary (for me) features.
Second, in my product I can introduce some quality of life features from smaller, like a time counter in the favicon or a global timer, to larger, like analytics and customizable widgets.
Finally, it was a rather ambitious real-life product I can learn a lot from, use myself, and add to my portfolio. Regardless of whether it turns into something more than a personal project.
I decided to go for a classic design: a frontend consuming a backend via REST API. It is flexible and allows for a good separation of concerns with less code coupling. In other words, no server-side rendering.
For the backend, I chose Python, mostly due to personal preferences and available skills. It could have as easily been any other language.
The next question is which framework to choose: Django, Flask, or FastAPI. Django is ruled out immediately -- it is too opinionated and has too many "batteries included" for my taste and use case.
Both Flask and FastAPI would suffice. At that time, I had more experience with Flask but wanted to dive deeper into the new and shiny FastAPI. In addition, FastAPI offers a lot of convenience around type hints and data validation. Though it is not free: this convenience comes at a cost of some magic.
Database: both relational and document-based databases will work. After some prototyping and thought experiments, I decided to go with MongoDB: document structure fitted the use case better, namely storing almost all information about a task in one document, and not spreading it over several tables.
Choosing the frontend stack was more complicated. A task manager (outside of certain circumstances) must be multi-platform: work on virtually any device and operating system. As I saw it, there were three options to achieve it:
Build separate web and native apps for all platforms. It's too resource-heavy and unfeasible for a small project.
Use a cross-platform framework that can be compiled for several platforms (React Native, Flutter, etc.). I did not have enough expertise in them to be sure of this solution. Moreover, I didn't want to stray too far from regular web technologies, vendor-locking myself with a particular multi-purpose framework.
Develop a regular Single-Page Application (SPA) accessible via any modern browser. SPA can do almost everything that a regular app can, plus it is possible to mimic the native app experience by employing service workers and making it a Progressive Web App (PWA), though with some limitations. Should the need arise, one can wrap it in a native app to overcome these limitations, or start developing a "real" native app.
After zeroing in on an SPA/PWA approach, choosing React.js as the main framework was no-brainier. It is the most popular (i.e. extensive code and knowledge base) and simple, especially after the introduction of hooks and functional components.
Now I am transitioning the project to TypeScript. It is a good thing that you can do it gradually. However, the transition was more difficult for an existing code base with a lot of ad hoc types and temporary
any's at first. Although, the more TypeScript there is, the easier it is to convert other parts.
The final frontend question: build all UI/UX from scratch or use an existing design system and component base. I decided to use Material UI for React.js. First, it allows building the frontend fast, focusing on features, rather than basic elements, styles, and layout. Second, I don't have a designer bone in my body, so Material UI with some tweaks is definitively better than what I could do in any reasonable amount of time.
There are two main drawbacks:
These general-purpose components tend to have too much functionality built-in, making the app bundle larger than it should be, and in some cases, performance leaves much to be desired (on very complex pages). Again, this is manageable, but you need to keep an eye on it. Spoiler alert: now I am gradually rewriting components from scratch to improve overall performance and experience.
Some people might say that the interface looks too generic. It may be true to a degree, but at the moment the project lives in the mode of "function over form".
When starting a new pet project, there is a great temptation to skip tests to develop faster. In my experience, it never works. Of course, you can compromise in some places, but tests allow you to develop faster since you are more sure of the quality when making changes.
Test-Driven Development is a different issue. I practice it more often than not, but it is not the goal in itself.
Unit tests only for complex logic that has many edge cases. All other functionality is tested via API end-to-end tests. Nothing fancy: pytest + TestClient provided by FastAPI.
The story of frontend tests is more complicated. For the longest time, I was skeptical about covering the frontend with tests. "Frontend is too dynamic and fluid to cover it with tests, they will be flaky and I will have to rewrite them often," told I myself. To some degree it is true: frontend tests differ from backend and you have to rewrite them more often.
After several WTFs breaking the frontend in unexpected places with innocent-looking changes, I decided to invest time in frontend testing. The approach is the same: more end-to-end tests (Cypress) and a few unit tests (Jest) for complex logic.
At first, it took a lot of time to write Cypress tests. However, after I've written some project-specific convenience functions, added
data-test attributes to the most elements, and got a hang of it, writing Cypress tests became a breeze. Often writing the tests itself unearths bugs and rough edges.
From the get-go, I wanted to build the architecture the "right way" with all the components running in separate Docker containers, orchestrated by docker-compose. Static content (frontend SPA) is served via Nginx. All the containers communicate with the outside world via Traefik reverse proxy. It all runs on a regular Ubuntu with a clear path for scaling.
Self-hosted MongoDB or managed? While the project is small I decided in favor of a self-hosted solution: it is simply cheaper. When the user base grows switching to a managed solution may be a viable path forward.
Deploy via GitHub Actions: pipelines automate everything and save a lot of time, allowing to release effortlessly.
Backups: of course, regular backups. Simple cron + bash script + mongodump. With some system snapshots sprinkled here and there.
As with any project some parts were more interesting or challenging to work on:
React.js state management. Always fun, especially since I wanted to do without external libraries, only with contexts, reducers, and hooks. This may change in the future as state management grows more complex.
Background sync. It is based on Server-Sent Events which are simple enough with two caveats. First, I needed to avoid "echoes" when an event is pushed to the client that caused it. Second, changes must be reconciled before being pushed to the store, otherwise, the user will see jitters in the app state and other unwelcome effects.
Date and priority recognition in the taskbar. Regular regexp would have done the trick, but I wanted to have a simple and extensible solution, without resorting to all-powerful external libraries.
Text editor. It was the most painful part. I've tried many, but only one fit the bill. Then it tuned out that got discontinued. After all, I settled for plaintext editor + rendering markdown. It is definitely not the final solution, but for the time being, it should suffice.
There were many things I wasn't sure about. Now I see that some of them were right:
End-to-end testing, both for the backend and frontend. The best thing since sliced bread.
Using GitHub Actions (or any other CI/CD). Even for a small project it automates away the friction of testing and deploying. Saves much more time than requires setting up.
Betting SPA and PWA. It's not perfect by far, but has outstanding ROI.
Using Material UI component base. I guess this is the most controversial decision. At this stage of the project, the pros outweigh the cons.
If I were to start again I would do several things differently:
Use TypeScript from the very beginning. It would have saved me a lot of time.
Start frontend end-to-end testing with Cypress earlier. +10 to confidence in a release.
Invest less time in less important features with low ROI. The most prominent example is the text editor.
TodoX is a fully functional task manager and time tracker:
usual task manager stuff: tasks, projects, deadlines, repeated tasks, notes
time tracking: time can be recorded separately on any task
customizable widgets panel: notes, time by projects, time by tasks, and general stats
analytics: aggregate time by tasks or projects for any period and other stats
background sync between devices
works on any platform, installable as PWA on iOS and Android
A complete description with screenshots is on the website.
The main limitation: at the moment there are no collaboration features, although it is something on the horizon.
There is a long list of features to be implemented, from straightforward, like tags, notifications, and reminders, to more complex, like collaboration, better analytics, performance tracking, and predictive assistance.