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.error → throw (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
- Scaffold a Stacks app with the default
routes/api.ts (which uses import { route } from '@stacksjs/bun-router')
- Run
./buddy dev
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.
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.
routes/api.tsscaffold importsroutefrom@stacksjs/bun-router, which has norouteexport@stacksjs/router. Do not add a duplicateroutere-export to bun-router — having two ways to import the same singleton is the bug we just hit.log.errorinstacks-router.tsimportRoutes()log.error→throw(or surface via the dev banner). Silent route-loader failures are how this took hours to track down.dev/*subprocesses becauseargs.length === 0loadAutoImportsfrom the preloader so server entrypoints can invoke it explicitly (already done in the local copy in this repo); (b) refactor the skip condition soargs.length === 0doesn't skip whenargv[1]is a real script path. (a) unblocks today; (b) prevents the same trap for any future server entrypoint./api/*pathRewrite usesstripPrefix: false, so requests get forwarded with the/apiprefix that the API server doesn't registerstripPrefix: trueindev.ts:384. Single-line change.The "Bonus: status code lost" mention has been removed — I never reproduced it cleanly, so it shouldn't weight this issue.
When running
./buddy devon a fresh Stacks app, every API endpoint defined inroutes/api.tsreturns 404 — both athttps://<domain>/api/*(proxied) andhttp://localhost:3008/*(direct). Tracking it down surfaced four separate issues that compound. Filing one issue since they share a single user-visible symptom.Repro
routes/api.ts(which usesimport { route } from '@stacksjs/bun-router')./buddy devcurl -X POST http://127.0.0.1:3008/auth/login→{"success":false,"message":"Not Found"}(HTTP 404)Root cause 1 —
@stacksjs/bun-routerdoes not exportroutebun-router's top-level barrel only exportsRouter(the class) androuter(lowercase singleton). It has norouteexport. So:The error is then swallowed silently in
storage/framework/core/router/src/stacks-router.ts:1112:The Stacks
routeinstance lives in@stacksjs/router(which re-exports bun-router and addsroutefrom./stacks-router). The defaultroutes/api.tsscaffold should import from@stacksjs/router, or bun-router should also export aroutesingleton for parity, or the swallowedlog.errorshould be promoted so this fails loudly.Root cause 2 — auto-imports preloader skips subprocess dev servers
storage/framework/defaults/resources/plugins/preloader.ts:./buddy devspawns the API server asbun --watch storage/framework/core/actions/src/dev/api.ts. In that subprocess,--watchis consumed by Bun, soprocess.argv.slice(2)is[]→args.length === 0→ preloader skips auto-imports. Action files like:…then fail to load. Bun reports this through the route handler as
ReferenceError: Cannot access 'default' before initialization(which is misleading — the real error isAction is not definedfrom the TDZ binding never being initialized).The preloader's
args.length === 0short-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 inpreloader.ts, sodev/api.ts(and any other server entrypoint that's spawned as a subprocess) has no way to invoke it explicitly. Either:loadAutoImportsso server entrypoints can call it, ordev/api.ts,dev/server.ts, etc.), orargs.length === 0doesn't skip when a script path is present inargv[1].Root cause 4 — rpx
/api/*pathRewrite usesstripPrefix: falsestorage/framework/core/buddy/src/commands/dev.ts:384:stripPrefix: falseforwards/api/auth/loginverbatim to the API server, but routes inroutes/api.tsare registered without an/apiprefix (the registry maps key'api'to a no-prefix mount). Result: 404 athttps://<domain>/api/*even after fixes 1–3 land. SettingstripPrefix: trueresolves it.Local fixes verified
I patched all four locally and
https://api.<domain>/auth/loginandhttps://<domain>/api/auth/login(after a./buddy devrestart 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.