Nobody has started this yet — be first.
Business impact
Every other resource in this API (expenses, budgets) supports fetching a single record by id -- categories is the only one where a client has to fetch the entire list just to check whether one category id is still valid, or get its current name (e.g. before rendering a single expense's category, or confirming a delete). Any frontend built against this API has to work around the missing endpoint with extra round trips.
Problem
internal/categories/handlers.go has List, Create, Update, Delete -- no Get. No route for GET /categories/{id} is registered in cmd/server/main.go's NewRouter, so today that URL simply 404s from Go's ServeMux default ("no matching route"), not from any application logic.
Current behavior
GET /categories/{id} returns a bare 405/404 from the router itself -- there is no handler at all, and the obvious repo method to reach for (GetOwned) would silently exclude every global category if wired in naively.
Expected behavior
GET /categories/{id} (auth required) returns the category if it is visible to the caller -- global (user_id IS NULL) or owned by the caller -- exactly the same visibility rule List already uses, not merely "owned by the caller." A non-owner's or nonexistent id's request gets 404, same as every other resource.
Steps to reproduce
GET /categories/1 (a global "Food" category), with any valid token.
Why this matters
internal/categories/repo.go already has a GetOwned(ctx, id, userID) method -- it looks like the obvious thing to reach for, but it is not sufficient here: its WHERE clause is id = $1 AND user_id = $2, which only matches a user's private categories, and would incorrectly 404 on every global category (id 1 "Food", etc.) even though List includes globals for every user. Wiring this endpoint straight to GetOwned ships a subtly-broken feature that passes a shallow "can I get my own category" smoke test while being wrong for the common case.
Suggested approach
Add a repo method whose visibility rule matches List's / CategoryVisible's user_id IS NULL OR user_id = $N pattern instead of GetOwned's owner-only rule, returning the full row (unlike CategoryVisible/IsVisible, which only check existence). Wire it into the router at GET /categories/{id} (no conflicting pattern exists yet).
Acceptance criteria
Verification
go test ./practicetickets/... -run TestTicket06 -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
$git checkout -b feat/category-get-by-idFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/category-get-by-idSubmit 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-compose.ymlmigrations/0001_init.sqldocker-entrypoint-initdb.dexpense_trackerdbappcurl http://localhost:8082/healthz
To run locally against your own Postgres instead: cp .env.example .env (edit if your Postgres isn't on localhost:5434), then make migrate (applies migrations/*.sql via psql) and make run (go run ./cmd/server).
Work the tickets in PRACTICE_TICKETS.md (TICKET-01 through TICKET-10); each names one Go test in practicetickets/:
go test ./practicetickets/... -run TestTicket01 -v # a single ticket
./practice_tickets_run.sh # all 10, clean pass/fail summary
This project's own integration suite (cmd/server/*_test.go) needs a real, reachable Postgres -- point TEST_DATABASE_URL at any empty/disposable database (migrations are applied automatically, and every test truncates+reseeds before it runs) and run make test. The practicetickets/ suite defaults to postgres://postgres:postgres@localhost:5434/expense_tracker_test if TEST_DATABASE_URL is unset.
Level 2
Implement a feature
Extend the system within its own patterns.