routes
Installation
SKILL.md
Routes
Each module owns its routes.ts. The file gets discovered by the preload wired in [[module-scaffolding]] — there is no autoloader. Routes have three loads to bear: they're the source of truth for URL generation (typed clients like Tuyau and the server-side URL builder read the named routes), for the middleware chain that guards the endpoint, and for the param typing that keeps garbage input out of the ORM.
Rules
- Numeric params get
router.matchers.number(). Without it, a non-numeric URL reaches the controller,Number('foo') → NaN, and Postgres throwsinvalid input syntax for type integer— the response is a 500. With the matcher the router 404s upstream, before boot. - CRUD verbs go through
router.resource(). Custom verb actions (activate, publish, finalize) are separaterouter.post(...)in the same file, not extra methods on the resource controller. - Route names follow the URL hierarchy:
parents.children.action. Named routes are the single source of truth for both the typed frontend URL client and the server-sideurlFor(...)used inside jobs and listeners. Both derive the URL from the name; the name is a contract. - Param names match across parents: for a nested resource named
parents.childrenuse.params({ parents: 'parent_id' })and pin every id:.where('parent_id', router.matchers.number()).where('id', router.matchers.number()). Snake_case, semantic, matching what the URL client expects. - Group by shared middleware. Put every route that shares the same auth/middleware stack inside one
router.group(() => {...}).middleware(...)block. Public routes live outside, guarded ones inside.