chi
Technical notes on web development, DevOps, and AI integration.
9 articles
- 00:57backend
The Handler Received %2C: chi Does Not Decode Path Params
A slug with a comma returns 404 while the data exists: chi keeps path params encoded and decoding is the API owner's contract.
TL;DR: Special characters in slug path params broke lookups because chi stores route params in encoded form, so handlers queried the DB with %2C and got empty results. Fix: a DecodePathParams middleware wired with r.With runs after routing, once, with strict rules. Handlers stay clean and the failing slugs now return 200.
#go#chi#routing - 17:43backend
A FastAPI Skeleton of Layer Contracts, Not Empty Folders
Four folders with empty __init__.py files carry real contracts: layer boundaries, a unit of work per request, JWT tenant claims, and the 72-byte bcrypt gate.
TL;DR: Four seemingly empty folders establish layered contracts in a FastAPI skeleton, keeping business logic, technical details, and HTTP translation cleanly separated. A yield-based dependency handles transactions per request, committing only on success so multi-table use cases stay atomic. Multi-tenancy rides in JWT claims, bcrypt limits get enforced at the validation gate, and swappable signal sources await future needs.
#fastapi#python#jwt - 16:24database
Soft Delete Done Right: Predicates, RowsAffected, and Restore
Soft delete is not just DELETE swapped for UPDATE. Predicates on every query, a RowsAffected alarm, and a mirrored restore guard keep it honest.
TL;DR: Switching DELETE to soft delete with an UPDATE on deleted_at left deleted rows visible because SELECTs weren't filtered. Fixing it means adding deleted_at IS NULL to queries, checking RowsAffected to return 404, and mirroring logic for restores. You also need to invalidate cache on every state change, otherwise complexity and leaks quickly outweigh benefits.
#go#mysql#sql - 12:25frontend
Labels That Can Lie: When Status Vocabulary Lives in Two Places
A duplicated STATUS_MAP on the public page now reads labels from one lib: exhaustive types, an honest fallback for unknown values, countKey enforced.
TL;DR: The public status page broke because it kept its own four item map while the backend and shared lib handle six statuses. The fix makes STATUS_LABEL in layanan-flow the single source of truth, with TypeScript enforcing completeness and a runtime fallback to neutral raw text. CountKey is now required, so future status additions propagate automatically without silent display bugs.
#react#typescript#ui - 11:22frontend
Mirroring the Status Transition Map in the Frontend
A tiny frontend mirror of the backend transition map: one union type, one map, and impossible options disabled before the click.
TL;DR: I refactored the tangled status logic into a clean switch that maps six statuses to four display stages. A lean NEXT_STATUSES map only decides which options light up, leaving the backend as the true authority that returns 409s. I added type guards, fallback labels for unknown statuses, and real disabling beyond just aria-disabled.
#frontend#typescript#state-machine - 09:47backend
Not a Race Condition, but a Dual-Write
One ticket skipped two statuses, a citizen got a duplicate email: the root was a dual-write. The fix is a transition map, 409, history, and an outbox.
TL;DR: Duplicate emails weren't a frontend race condition but a dual-write bug where database updates and SMTP sends weren't atomic. The fix uses a transactional outbox, saving the email to an outbox table in the same Postgres transaction for a background worker to deliver. Illegal status jumps are now blocked early with 409 Conflict using a central transition map.
#postgresql#http#outbox - 10:44backend
One Query Param, Three Layers: A Safe ?module= Filter
Adding a ?module= filter is not just a WHERE clause: 400 validation at the handler, a parameterized subquery, and a cache key per filter combination.
TL;DR: A naive SQL-only module filter would poison the shared mappoints:all cache, leaking filtered results to unfiltered requests and vice versa. The fix layers validation against six allowed modules with a 400 error, a parameterized subquery, and branched cache keys by module and category. This isolates each response variant and prevents cross-contamination.
#go#api#caching - 12:32backend
I Almost Deleted My Cache One Key at a Time
Mass-deleting Redis keys felt right until I read the docs. Versioned keys with INCR bumps make stale chat answers unreachable instead.
TL;DR: Editing an article left the Redis-cached AI answers stale, and the author's first fix—scanning and deleting every ask: key—turned out to solve the wrong problem. SCAN is non-blocking; the real pain is tracking key families across workers forever. Versioned keys fix it: bake versions into key names, bump with INCR on edits, and let TTL clean up.
#redis#caching#cache-invalidation - 19:19tooling
One Qwen chat for two articles: followups via chat URL
A --chat-url option lets EN translation reuse the existing Qwen chat, one chat for two articles, plus how force-cache works in Next.js 15.
TL;DR: The author's translation pipeline burned through free Qwen quota because every generate run opened a new chat, so they added a --chat-url option to reuse existing sessions, halving quota use. Next.js 15 stopped caching fetch by default, requiring explicit force-cache settings. A tldr-tolerant push now ignores server metadata, preventing CI failures.
#automation#qwen#nextjs