Bugtracking.io is a bug tracking platform featuring user-managed teams and support for multiple projects. It is a full stack web application using React on the front end and a containerized API built with Django Rest Framework on the back end, alongside Google Firebase for user authentication.
It was well worth it in the end, and I was left with a modern, full stack web application that I use to manage all my ongoing projects – including work on Bugtracking.io itself!
More than anything else, rewriting my Django stack taught me the value of large-scale refactoring. When I started working on the API for the updated version of Bugtracking.io, I began by attempting to expand upon the Django stack I had already written, adding model serializers and JSON responses to my existing codebase. I quickly ran into many headaches with this process, partly because I was learning Django as I wrote that codebase, and so had made mistakes along the way, but also because that codebase was written as a full stack Django application. Trying to convert it into an API was trying to fit a square peg in a round hole.
So I started from scratch. Instantly, I felt the constraints of my older codebase lifted away. I was able to incorporate the lessons I had learned and design my models with an eye towards API integration. This meant thinking through the API endpoints I would need and incorporating the business logic necessary to implement those endpoints in the models themselves. This involved complex permission checks on nested resources, as users have specific membership roles in relation to tickets, projects, and teams, with object-level permissions being determined by the complete set of these roles (for example, a ticket’s assigned developer has edit permissions on that ticket, while the manager of a ticket’s project has edit permissions on all project tickets, including the ability to assign developers to tickets – and a team administrator has a further superset of permissions for all objects under that team).
I quickly learned the value of robust model methods, agnostic to any view logic, to manage these permissions and return only the objects that a user has permission to interact with. This not only enhanced security but also obviated the need for the front end to worry about which objects a user should be able to view and edit.
Verifying all these model methods and permissions soon turned into a massive undertaking, and this led me to test-driven development. I learned to love the sense of stability I got from a well written test suite, confident that any added feature or tweaked line of code wasn’t breaking my application.
As this was my first front end project, I faced all the usual challenges of learning React – learning about state management and where, in a set of nested components, state should be stored; writing reusable and maintainable components; and keeping business logic and presentational logic separate. I adopted a controller-view paradigm, with controller components fetching data and managing state, passing that state down as props to view components that were as “dumb” as possible.
I also had to learn how to update the UI based on a user’s permissions. For example, a team administrator’s UI must display components related to managing that team’s members. I realized the cleanest way to implement this logic was to return to the back end and have each endpoint return an object representing that user’s permissions in relation to the model currently being viewed. Then the front end implementation was as straightforward as investigating this permissions object to see which UI elements to display. Again, I kept as much business logic as possible on the back end.
The real challenge, of course, was connecting my React app to the back end. I needed a way to implement many different variations on similar queries. For instance, a request to get a list of tickets must take into account the user making the request, as well as the project and team by which to filter the list. The back end handled the filtering itself, but I needed a way to pass all this information to the back end, as well as keeping variations on these queries (for instance, a single user requesting the list of tickets for two different projects) distinct for caching purposes.
I opted to use React-Query alongside the axios library for its straightforward implementation of caching, full featured devtools, and the ability to intelligently prefetch queries based on a standard user’s flow through the site. Whenever a user views a list of objects, the details of those objects are fetched in the background and cached, keeping the application fast and minimizing loading time.
The list of lessons I learned working on this project is almost endless, but perhaps the most important lesson was this: I love development. When I finished this project, I couldn’t wait to start the next – and more than that, I couldn’t wait to implement all the lessons I learned, to design the next project from the ground up to be cleaner, more robust, more maintainable. This process of growth and continual improvement is why I code, and I hope it never stops.