Nobody has started this yet — be first.
Business impact
There is currently no way to take a short link back once it's been created -- expiresAt is opt-in and set at creation time, but a link created without one lives forever, and there's no "oops, wrong URL" or "this campaign is over, kill the link" path at all. Anyone who creates a link pointing somewhere they later need to disable -- a leaked internal URL, a promo that ended, a typo they didn't catch -- has no way to stop it resolving short of going around the API entirely. That's a real gap for any service whose whole job is controlling where a link points.
Problem
DELETE /:code is registered as a stub in src/routes/redirect.js (right before module.exports). It compiles and the route exists, but its handler unconditionally throws -- hitting it always returns a 500, regardless of :code.
Current behavior
DELETE /:code for any short code, existing or not, always returns HTTP 500.
Expected behavior
DELETE /:code, if the short code exists, permanently removes that Link row from Postgres and evicts any cached entry for it from Redis, then returns 204 No Content. If the short code does not exist (never existed, or was already deleted), it returns 404. After a successful delete, GET /:code for that same code must return 404 -- including on the very next request, even if the link was cached moments earlier. A stale cache entry serving a 302 for a link that was just deleted would defeat the entire point of deleting it.
Steps to reproduce
npm run dev curl -s -o /dev/null -w "%{http_code}\n" -X DELETE http://localhost:3003/1
Why this matters
This is the one ticket in this set that isn't a single self-contained change -- it's a coordination problem across the two stores this project already keeps in sync everywhere else. Every other write path here (POST /shorten's write-through, the redirect handler's cache-aside populate-on-miss) is careful about keeping Postgres and Redis consistent; a delete has to be equally careful in the other direction. Deleting only the Postgres row and forgetting the cache means a window (up to CACHE_TTL_SECONDS) where the "deleted" link still redirects for anyone hitting a cache hit -- silently undermining the whole feature. There's also a not-found case to get right cleanly: Prisma's delete throws (a P2025 "record not found" error) if you try to delete a row that isn't there, rather than silently no-op'ing.
Suggested approach
Add a deleteLink(shortCode) function to src/lib/linkService.js alongside createLink/findByShortCode (follow the same shape: takes a shortCode, does the Prisma call, returns something the route can check). In the route handler, after the delete succeeds, call invalidateCachedLink (already imported at the top of src/routes/redirect.js) before responding -- the ordering matters: the cache must be clear before you tell the client the delete is done, not after. Decide whether to check existence first (findByShortCode, already imported in this file) or let the delete itself fail and map that specific error to a 404 instead of letting it fall through to the generic 500 handler.
Acceptance criteria
Verification
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/06-delete-link.test.js
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b feat/delete-linkFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/delete-linkSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inOr run the whole stack in Docker instead of the steps above -- docker compose up --build (dev, hot-reloaded via a bind-mounted src/) or docker compose -f docker-compose.prod.yml up --build (prod-style multi-stage build, non-root user). Either way the app container runs prisma migrate deploy on startup, so no separate migration step is needed. Host ports are non-default -- app 3003, Postgres 5436, Redis 6380 -- to avoid clashing with sibling projects in this repo.
The project's own real test suite (npm test, Jest + Supertest against real Postgres/Redis) is separate from the practice tickets below -- run it any time to confirm you haven't broken anything already-working.
Each ticket in practice-tickets/tickets/ (01 through 07) names a dedicated test under practice-tickets/tests/, run via its own Jest project (practice-tickets/jest.config.js, excluded from plain npm test):
# a single ticket
npx jest --config practice-tickets/jest.config.js practice-tickets/tests/01-health-check-false-positive.test.js
# all 7, one at a time, with a pass/fail summary
./practice-tickets/run.sh
Fixing tickets 01, 03, 04, and 07 also turns several pre-existing failures in the real suite (tests/health.test.js, tests/redirect.test.js, tests/concurrency.test.js) back to green -- that's expected, not a coincidence, since the injected bugs live in shared code that suite also exercises. tests/concurrency.test.js specifically fails for two unrelated reasons at once (tickets 04 and 07 both touch code paths it exercises), so fixing only one of the two will not turn it green.
Level 2
Implement a feature
Extend the system within its own patterns.