# /rules/available Got Eaten by /rules/{id}, and Other Crimes of Ordering

_Two bugs, one theme: things that look interchangeable are not._

June 14, 2026 · 4 min · Amit Jethva

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:

```python
@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.

```python
@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:

```ts
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:**

```ts
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:

```ts
(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](https://www.nuvikatech.com/Fintropy_Overview.html), a multi-cloud FinOps platform. Learn more at [nuvikatech.com](https://www.nuvikatech.com)._
