Skip to content

API routes 404 on ./buddy dev — four cascading issues in router/preloader/rpx wiring #1835

@glennmichael123

Description

@glennmichael123

Editor's note (added after filing): the original body listed alternative fixes side-by-side without picking one, which made the issue hard to act on. Recommended fixes for each root cause are now stated up-front; alternatives are kept below for context only.

Recommended fixes (the actionable summary)

For each root cause below, this is the preferred direction. Not all alternatives need landing — pick one per root cause unless noted.

# Root cause Recommended fix Effort
1 routes/api.ts scaffold imports route from @stacksjs/bun-router, which has no route export Update the scaffold/template to import from @stacksjs/router. Do not add a duplicate route re-export to bun-router — having two ways to import the same singleton is the bug we just hit. Trivial (one line in the scaffold)
1b The error gets swallowed by log.error in stacks-router.ts importRoutes() Promote it: log.errorthrow (or surface via the dev banner). Silent route-loader failures are how this took hours to track down. Small
2 Auto-imports preloader skips dev/* subprocesses because args.length === 0 Two small changes, both: (a) export loadAutoImports from the preloader so server entrypoints can invoke it explicitly (already done in the local copy in this repo); (b) refactor the skip condition so args.length === 0 doesn't skip when argv[1] is a real script path. (a) unblocks today; (b) prevents the same trap for any future server entrypoint. Small
3 rpx /api/* pathRewrite uses stripPrefix: false, so requests get forwarded with the /api prefix that the API server doesn't register stripPrefix: true in dev.ts:384. Single-line change. Trivial

The "Bonus: status code lost" mention has been removed — I never reproduced it cleanly, so it shouldn't weight this issue.


When running ./buddy dev on a fresh Stacks app, every API endpoint defined in routes/api.ts returns 404 — both at https://<domain>/api/* (proxied) and http://localhost:3008/* (direct). Tracking it down surfaced four separate issues that compound. Filing one issue since they share a single user-visible symptom.

Repro

  1. Scaffold a Stacks app with the default routes/api.ts (which uses import { route } from '@stacksjs/bun-router')
  2. Run ./buddy dev
  3. curl -X POST http://127.0.0.1:3008/auth/login{"success":false,"message":"Not Found"} (HTTP 404)

Root cause 1 — @stacksjs/bun-router does not export route

bun-router's top-level barrel only exports Router (the class) and router (lowercase singleton). It has no route export. So:

import { route } from '@stacksjs/bun-router'  // route === undefined
route.post('/auth/login', 'Actions/Auth/LoginAction')  // throws TypeError

The error is then swallowed silently in storage/framework/core/router/src/stacks-router.ts:1112:

async importRoutes(): Promise<void> {
  try {
    const { loadRoutes } = await import('./route-loader')
    const routeRegistry = (await import('../../../../../app/Routes')).default
    await loadRoutes(routeRegistry)
  }
  catch (error) {
    log.error('Failed to load route registry:', error)  // never surfaces in dev output
  }
  // …
}

The Stacks route instance lives in @stacksjs/router (which re-exports bun-router and adds route from ./stacks-router). The default routes/api.ts scaffold should import from @stacksjs/router, or bun-router should also export a route singleton for parity, or the swallowed log.error should be promoted so this fails loudly.

Root cause 2 — auto-imports preloader skips subprocess dev servers

storage/framework/defaults/resources/plugins/preloader.ts:

const args = process.argv.slice(2)
const fastCommands = ['dev', 'build', 'test', 'lint', ...]
const skipPreloader = args.length === 0 || fastCommands.some(cmd => args[0] === cmd || ...)

./buddy dev spawns the API server as bun --watch storage/framework/core/actions/src/dev/api.ts. In that subprocess, --watch is consumed by Bun, so process.argv.slice(2) is []args.length === 0 → preloader skips auto-imports. Action files like:

export default new Action({ ... })  // ReferenceError: Action is not defined

…then fail to load. Bun reports this through the route handler as ReferenceError: Cannot access 'default' before initialization (which is misleading — the real error is Action is not defined from the TDZ binding never being initialized).

The preloader's args.length === 0 short-circuit was probably meant to skip the bun REPL but accidentally skips every direct script invocation.

Root cause 3 — no easy way for server entrypoints to opt back in

loadAutoImports() is a non-exported local function in preloader.ts, so dev/api.ts (and any other server entrypoint that's spawned as a subprocess) has no way to invoke it explicitly. Either:

  • Export loadAutoImports so server entrypoints can call it, or
  • Detect server scripts in the preloader skip logic (e.g., always run for dev/api.ts, dev/server.ts, etc.), or
  • Refactor the skip condition so args.length === 0 doesn't skip when a script path is present in argv[1].

Root cause 4 — rpx /api/* pathRewrite uses stripPrefix: false

storage/framework/core/buddy/src/commands/dev.ts:384:

{ from: `localhost:${frontendPort}`, to: domain, cleanUrls: false,
  pathRewrites: [{ from: '/api', to: `localhost:${apiPort}`, stripPrefix: false }] },

stripPrefix: false forwards /api/auth/login verbatim to the API server, but routes in routes/api.ts are registered without an /api prefix (the registry maps key 'api' to a no-prefix mount). Result: 404 at https://<domain>/api/* even after fixes 1–3 land. Setting stripPrefix: true resolves it.

Local fixes verified

I patched all four locally and https://api.<domain>/auth/login and https://<domain>/api/auth/login (after a ./buddy dev restart for the rpx config) both reach the action handler now. Happy to open a PR if the proposed direction looks right — wanted to file the issue first since (1) and (3) feel like architectural decisions worth a maintainer eye.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions