backend
Technical notes on web development, DevOps, and AI integration.
67 articles
- 14:21backend
When API Contract Descriptions Lie
A field description calling jenis a pillar and an admin_note exception that never existed: two lying contract descriptions, plus an undocumented 400.
TL;DR: I noticed the spec wrongly called the jenis field a service pillar instead of its read-only application kind and claimed admin_note was ignored for Submission. The history endpoint also lacked its documented 400 response for malformed IDs. Fixing both descriptions and adding the error response corrected the docs and the generated frontend types.
#api#openapi#typescript - 13:28backend
Three Small Fixes That Made the API More Honest
An empty note got blamed on the wrong field, a missing actor was recorded as zero, and email subjects carried raw submitter text. Three small fixes, one theme.
TL;DR: API returned a generic error for missing notes because the specific check was ordered last, so it now returns a clear note-required message first. Status updates now return 401 when user context is missing instead of silently recording the action as the system. Email subjects are also sanitized to strip control characters and prevent header injection.
#golang#api#security - 13:28backend
A Complaint Born Without a Trace
An empty complaint timeline was not a rendering bug: the first history row was never inserted, because twin functions CreateRegistration and CreateReport had drifted apart.
TL;DR: A new complaint had no status history because CreateReport never seeded the initial NULL-to-submitted row, while its twin CreateRegistration did. The fix wraps report creation in a transaction that inserts both the report and its opening history entry atomically. A new regression test now guards the behavior, highlighting the need to test twin functions side by side.
#golang#mysql#audit-trail - 10:04backend
Counters That Lie: Date Filters and Two Clocks
The counter said 12, the table said 15: the date filter was reading the wrong clock. The fix counts from transition history, not the status column.
TL;DR: The dashboard showed mismatched ticket counts because one query filtered by creation date while the other reflected actual status changes today. Tickets created yesterday but handled today were missed due to timezone and date-boundary quirks in PostgreSQL. Switching all counters to aggregate the transition history by changed_at made the numbers consistent and trustworthy.
#postgresql#dashboard#reporting - 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 - 08:58backend
One Value, Two Meanings: Global Scope Is a Choice, Not Null
Null in a scope filter doing two jobs at once: not chosen yet and choose all. A design note on why an ALL sentinel in the enum is more honest.
TL;DR: The KotaPortal dropdown used null to mean both nothing selected and all units, which created confusing backend logic. Instead an explicit ALL value was added to the BidangScope enum so null only means untouched. This keeps the API contract clear, makes validation reliable, and stops teammates guessing what empty means.
#typescript#openapi#api - 08:45backend
Green Build, Red Screen: The API Contract That Only Lives in Code
A new status value passed the frontend build because the OpenAPI spec never mentioned it. Why the contract has to be explicit in the spec.
TL;DR: The frontend built clean but the UI broke because a new ticket status lived in the handler, not the openapi spec. Since frontend types are generated from the spec, the compiler never saw the missing value. The fix was moving enums and descriptions into the spec and enforcing spec-first as the single source of truth.
#openapi#typescript#api - 07:59database
MySQL ENUM Migration in Three Steps: No Table Copy Required
Swapping a status vocabulary on live MySQL 5.7 tables: expand-migrate-contract, an honestly lossy down file, and an up-down-up proof before production.
TL;DR: Changing submission statuses from four to six values forces a costly MySQL table copy unless new ENUM values are appended. Instead the migration appends all values, remaps the data, then trims to the final list to stay in-place. Rollback is lossy for newer statuses, so history is tracked separately and the flow was verified with an up-down-up test.
#mysql#migration#schema - 11:27backend
Scope filters that must not swallow global rows
Adding a bidang scope filter to a public endpoint sounds like one WHERE clause. Then the global rows disappear.
TL;DR: The new scope filter hid global announcements because WHERE bidang equals value never matches NULL. The fix validates bidang with a 400 on invalid values and changes the query to keep NULL and ALL rows. It also adds bidang to the cache key and moves pillar intros into the settings table with a fallback.
#go#mysql#api