Routing Deep-Dive: Resources, Nested Callbacks, and Route Model Binding #3268
bpamiri
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
There's a block of code you write the same way in every resourceful controller:
findByKeythe record, check it isn't null, 404 if it is. Three lines, four times (show/edit/update/delete), in every controller. ThefindByKeyis mechanical. The not-found check is the part everyone forgets — and the day you forget it,/posts/999999hands a visitor a stack trace instead of a 404.This post is a complete tour of the Wheels 4.0 routing surface, ending with the feature that takes that boilerplate off your plate.
Read: https://blog.wheels.dev/posts/routing-resources-model-binding
What it covers
resources() — the seven RESTful routes (index/new/create/show/edit/update/delete) from a single call, each as a named route, materialized at
end(). The detail people miss:updateregisters TWO routes — a PATCH and a PUT — both pointing at theupdateaction.only/exceptfilter the set, but a typo in them only throws in development; production silently drops the unknown name.Nested resources — the opposite-behavior trap. Passing a
callbackauto-enables nesting and auto-callsend()for you — you do NOT add your own. The manualnested=trueform suppresses the auto-end(), so you MUST call.end()yourself. They're mirror images, and mixing them (callback + manual end) leaves an extraend()that pops an empty scope stack and throwsWheels.InvalidRoute. The post has the table.Named routes, root, wildcard. Why
to="sessions##new"needs the##escape (a literal#is an expression delimiter in CFML strings). Whyroot()andwildcard()are GET-only by default. And why route order is load-bearing: the dispatcher scans routes in registration order and stops at the first match — sowildcard()goes LAST or it shadows everything below it. (Fun wrinkle: the shippedconfig/routes.cfmputs wildcard before root — that's fine because wildcard doesn't match/, but any named route you add by hand has to go above the wildcard line.)Route model binding — the headline feature.
binding=trueon a resource resolvesparams.postinto a real model instance, and 404s a missing record, before the controller is even instantiated. The action body drops to the part that's actually yours. The post covers:binding=true→params.post) vs explicit (binding="Author"→params.author).scope(path="/api", binding=true)— every nested resource is bound withoutbindingappearing on its own lineset(routeModelBinding=true)switch, and how per-route binding overrides it in both directions (includingbinding=falseto opt out)Wheels.RecordNotFoundis only THROWN in development — production sets 404 and renders the missing-template page. Don'ttry/catchfor the typed exception in prod.The protected-action-names trap. You cannot name a controller action after any public framework helper —
model,redirectTo,linkTo,env, theis*request predicates, the flash helpers. The framework builds the protected list from live component metadata (acrosswheels.Global,wheels.controller.*, andwheels.view.*) at app start; a matching action name throwsWheels.ActionNotAllowed(→ 404) and never dispatches. The seven standard REST names are safe because none of them are helpers.Discuss
set(routeModelBinding=true)switch app-wide? Where did the convention-vs-explicit divergence catch you?nested=true— which form do you reach for, and why?Every claim in the post is grounded in the 4.0 source, with a full worked
config/routes.cfmyou can lift directly. Feedback and war stories welcome in this thread.All reactions