A production-quality Node/Express/Prisma Blog API whose central engineering lesson is that authentication and authorization are two separate, sequentially-enforced checks -- a valid JWT proves who you are, a server-side ownership re-check proves what you're allowed to do to this specific resource.
Nobody has started this yet — be first.
GET /posts always skips the first page of results
Anyone browsing posts loses the newest content. GET /posts (and GET /posts/:id/comments) always renders newest-first, so "page 1" is supposed to be the freshest, most relevant page -- instead it silently skips exactly limit records before returning anything. On a blog with fewer posts than the page size, the homepage just renders empty. On a busier one, users land on page 1 and see content that's already one page stale. This is the kind of bug that looks like "the site is broken" to a reader and generates support/bug reports with no obvious cause, since nothing errors -- it just quietly returns the wrong slice.
About this project
A Node.js + Express + Prisma blog API -- Posts, one level of threaded Comments, JWT authentication, and bcrypt password hashing -- built around one deliberate lesson: "authenticated" and "authorized" are not the same question. requireAuth (JWT verification) answers "who are you?"; requireOwnership, run strictly afterward, re-derives ownership from the resource's own authorId column in the database -- never from anything the client sends -- and answers "are you allowed to do this?" A logged-in user holding a perfectly valid token for their own account still gets a clean 403, not a silent success, the moment they try to mutate someone else's post or comment.
Several smaller correctness problems sit around that central lesson: page->offset math in the pagination helper that has to get the page=1 boundary exactly right; a resource-existence check that has to run before an ownership check, not after, so a stale or already-deleted id 404s instead of crashing; a one-level-deep threaded-reply rule enforced only in application code, since Prisma's own relations can't express "must be the same post as its parent"; and a Prisma unique-constraint violation whose error shape (a top-level .code, not one nested under .meta) has to be read correctly to turn a concurrent duplicate-registration race into a clean 409 instead of a raw 500.
Empty and whitespace-only comments are accepted
Anyone can post a comment with no actual content -- an empty string, or a string of only spaces/tabs/newlines that renders as a blank line in any UI. On a public blog that's a moderation and trust problem: blank comments clutter every post's comment thread, make it harder for real readers to follow a discussion, and are exactly the shape of noise automated spam/abuse tools send first to probe whether validation exists at all.
Two of the seven tickets go further than a one-line fix. One is a genuine concurrency bug that a sequential "register twice in a row" test cannot catch -- it only reproduces when two requests for the same brand-new email race the same findUnique-then-create sequence -- and the other is a missing piece of security infrastructure entirely: nothing in this codebase tracks per-request state across requests, so there is currently no limit at all on login attempts, and building one correctly (per-email, time-windowed, safe under repeated hits) is the project's hardest ticket.
Seven tickets -- four bugs and three enhancements -- are deliberately injected into an otherwise working, fully tested codebase (tests/*.test.js), ordered easy to hard. Each has its own dedicated Jest test under practice-tickets/tests/, run against the same real Postgres database the main suite uses, that goes green the moment the fix -- or the feature -- is correct.