A compact Django + DRF book-tracking API where the same handful of business rules -- a rating is 1-5, a read book needs a finish date -- are deliberately enforced at three independent layers (model validators, serializer validation, a database CheckConstraint), and the interesting bugs live in the gaps between them.
A rating of 6 is silently accepted
The library tracker's whole value proposition is "trust the numbers you put in." Every place a rating is shown -- the admin list view, the browsable API, any future stats/export feature -- assumes 1-5 is the full range, because that's the only range documented anywhere (README, admin panel, the API itself). Right now the API will happily accept and store a rating of 6. That's not just a cosmetic glitch: any future feature built on "ratings are 1-5" (a 5-star widget, a CSV export, an average-rating calculation) will either crash or silently produce wrong numbers the first time it touches one of these out-of-range rows, and by then the bad data is already in the database and mixed in with everything else.
About this project
A small Django REST Framework API for tracking books someone wants to read, is reading, or has finished -- title/author/status/rating/dates, a browsable API, and a fully customized Django admin. Nothing about the domain is complicated; the interesting engineering is entirely in how the same business rules end up enforced redundantly across the stack, and what happens the moment two copies of "the same" rule drift out of sync.
Three problems sit at the center: layered validation that can silently disagree with itself (a in , a hand-written date comparison in , and a database-level all encode the same invariant in three different places, and DRF write paths never call , so only the layer actually on the request path protects it); composing DRF filter backends (adding free-text search alongside an existing filterset without touching the project-wide setting); and aggregation correctness (Django's returns , not , over an empty queryset, and a stats endpoint has to keep that intact rather than coercing it, while scoping the average to exactly the right subset of rows).
Book list shows the oldest additions first, not the newest
This is a personal library tracker -- the whole point of the list view is "what did I just add." Right now the default GET /api/books/ order buries every new addition at the very last page instead of the first, which for anyone with more than a page of books means the app looks "broken" or "stuck" every time they add something, since the thing they just did is invisible until they page all the way through. It's the kind of regression that doesn't throw an error anywhere, so it won't show up in a smoke test -- only in a user actually scrolling and asking "where did my book go?"
MaxValueValidatorbooks/models.pyBookSerializer.validate()CheckConstraintModel.full_clean()django_filtersDEFAULT_FILTER_BACKENDSAvg()None0NoneSeven tickets -- four bugs and three enhancements -- are deliberately injected into an otherwise small, fully tested codebase: some are one-line boundary or comparison mistakes (a MaxValueValidator off by one, a flipped Meta.ordering sign, a <= where the error message next to it clearly means <), one is a two-artifact bug that only breaks once you notice a CheckConstraint's condition has to be re-baked into an actual migration, not just edited in models.py, and the rest are enhancements left as a raise NotImplementedError stub so the app and its own test suite still run before the real implementation is written. Every ticket has a dedicated Django test, deliberately isolated from every other ticket's bug, that goes green when the fix is correct.