Nobody has started this yet — be first.
Business impact
There is currently no way to see "what short links exist" without querying Postgres directly. Support cannot answer "did the customer actually create this link?" without DB access, and there is no self-serve way for a user (or an internal dashboard) to audit what has been created. Every other resource in this API (ShortURL) can be read individually by slug but never listed -- a gap any real link-management product needs on day one.
Problem
GET /api/urls has a route registered but it is a placeholder that raises NotImplementedError("GET /api/urls is not implemented yet (see TICKET-04)") unconditionally.
Current behavior
Calling GET /api/urls always raises NotImplementedError -- visible as a raw traceback in dev/testing, a generic 500 in production.
Expected behavior
GET /api/urls?page=<n>&per_page=<n> returns a paginated list of short URLs, most-recently- created first, e.g. {"items": [...], "total": 42, "page": 1, "per_page": 20}, with each item shaped like ShortURL.to_dict().
Steps to reproduce
Why this matters
This is a straightforward pagination feature, but worth doing the page/per_page contract properly: sane defaults when the params are missing, clamped bounds so per_page=100000 cannot be used to dump the whole table in one request, and a total count so a client can render pagination controls without a second request.
Suggested approach
Look at how flask-sqlalchemy's Query object exposes pagination (there is a built-in helper for exactly this), and at how analytics/routes.py already clamps a query param (days) into a safe range -- the same pattern applies to per_page. Reuse ShortURL.to_dict() per item rather than hand-rolling the JSON shape again.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_04_list_urls.py -v
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
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/list-urls-endpointFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/list-urls-endpointSubmit 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 indocker-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 2
Implement a feature
Extend the system within its own patterns.