This is a double feature, because both stories share a villain: the assumption that order and shape don’t matter. They matter. They matter so much.


Feature 1: The route that got swallowed

We had two FastAPI routes:

@router.get("/rules/{rule_id}")     # declared first
def get_rule(rule_id: str): ...

@router.get("/rules/available")     # declared second
def list_available_rules(): ...

Then we wondered why GET /rules/available was returning a baffling error instead of the list of available rules.

Here’s the thing: FastAPI matches routes in declaration order, top to bottom, first match wins. So when a request for /rules/available comes in, FastAPI walks the list, hits /rules/{rule_id} first, shrugs, and goes: “Sure, rule_id = "available". Makes sense to me.” The literal route never even gets a turn. It’s standing in line behind a pattern that matches everything.

/rules/{rule_id} is the friend who answers every question in the group chat before anyone else can type. /rules/available never had a chance.

The fix: declare literal paths before parametric ones.

@router.get("/rules/available")    # specific first
def list_available_rules(): ...

@router.get("/rules/{rule_id}")    # greedy catch-all last
def get_rule(rule_id: str): ...

Specific before greedy. Always. The catch-all goes at the bottom, like the “anything else?” line on a form.


Feature 2: The envelope that wasn’t there

Meanwhile, on the frontend, we had a helper called api.get. And a very reasonable-looking line:

const res = await api.get("/budgets");
setBudgets(res.data);   // 💥

This is the kind of code that looks correct in every language you’ve ever used. res.data — the data is in .data, right? That’s the envelope. That’s how responses work.

Except api.get already unwraps the envelope. It returns the body directly, not { data: T }. So res is the array. And res.data? That’s undefined.

So setBudgets(undefined) runs without complaint. The app doesn’t crash here. It crashes later, somewhere downstream, when something tries to .map over undefined — far from the scene of the actual crime. And because we’d persisted that state, undefined got written into localStorage, which then poisoned the next page load too. The bug outlived the session. It haunted refreshes.

The fix:

const budgets = await api.get("/budgets");   // already the body
setBudgets(Array.isArray(budgets) ? budgets : []);

Two lessons in one: know whether your helper unwraps, and never trust an unverified shape. Which leads to the bonus...

Bonus crime: (x || []).map is not shape-safe

(x || []).map(...) only saves you when x is null/undefined. If x is an object {} instead of an array, x || [] happily returns the object, and .map is not a function. Guard the actual shape:

(Array.isArray(x) ? x : []).map(...)

The shared moral

Both bugs come from treating distinct things as interchangeable:

  • Routes: /rules/available and /rules/{id} are not peers — one is specific, one is greedy. Order encodes priority.
  • Responses: the body and { data: body } are not the same shape. Know which one your helper hands you.
  • Truthiness ≠ shape. || [] checks for missing, not for array. Use Array.isArray.

The unifying theme: the bugs that hurt most aren’t the ones where you wrote something wrong — they’re the ones where you wrote something plausible. Plausible code passes the eyeball test and fails at runtime, usually in a different file, ideally on a Friday.


Amit Jethva is the CTO and co-founder of Nuvika Technologies Pvt Ltd, makers of Fintropy, a multi-cloud FinOps platform. Learn more at nuvikatech.com.