Basics
Routing
Define the URL surface of your app in config/routes.cfm. This page shows how to wire simple routes, resource routes, nested resources, namespaced sections, and pattern constraints — then how to call the matching helpers from your views and controllers.
You’ll learn:
- How to define a single route with
get,post,patch,delete - How
resources()produces the seven REST actions in one line - How to nest resources and namespace an admin section
- How to constrain placeholder patterns with regex
- Which helper reads which route at call time
Define a simple route
Section titled “Define a simple route”config/routes.cfm is a plain CFM file loaded at boot. Wrap the mapper DSL in <cfscript>; call mapper(), chain matchers, and close with .end(). The minimum useful route is a single get with a name, a pattern, and a controller#action target.
<cfscript>mapper() .get(name="hello", pattern="/hello", to="main##hello") .wildcard().end();</cfscript>name="hello" is the handle your helpers pass — linkTo(route="hello") builds the URL, redirectTo(route="hello") navigates to it. pattern="/hello" is the URL shape. to="main##hello" is the double-hash form of controller##action (two hashes because CFML treats a single # as an expression delimiter). .wildcard() is the catch-all that always comes last.
Resource routes
Section titled “Resource routes”resources() is where most routing happens. One call generates routes for the seven REST actions plus the named helpers that go with them.
<cfscript>mapper() .resources(name="posts", binding=true) .root(to="posts##index", method="get") .wildcard().end();</cfscript>This gives you GET /posts, GET /posts/new, POST /posts, GET /posts/:key, GET /posts/:key/edit, PATCH/PUT /posts/:key (both verbs map to update), and DELETE /posts/:key — plus the named routes posts, newPost, post, and editPost. Each route is also registered with a .[format] twin (/posts.json, /posts/:key.xml, …), so the actual table holds more rows than seven. binding=true turns on route model binding: the member actions (show, edit, update, delete) see a pre-loaded params.post instance without calling findByKey themselves.
Trim the resource with only or except
Section titled “Trim the resource with only or except”Skip the actions you don’t need. Pass only="..." to allow-list, or except="..." to deny-list.
<cfscript>mapper() .resources(name="comments", only="create") .resources(name="posts", except="delete") .wildcard().end();</cfscript>comments gets one route (POST /comments). posts gets six (every REST action except DELETE /posts/:key). The named routes that correspond to trimmed-away actions don’t exist — linkTo(route="newComment") would throw Wheels.RouteNotFound in the first example.
Nested resources
Section titled “Nested resources”To build a URL like /posts/:postKey/comments, nest the inner resource via the callback argument. Wheels passes the mapper in; call .resources() on that parameter, not on the outer mapper().
<cfscript>mapper() .resources(name="posts", callback=function(map) { map.resources(name="comments", only="create"); }) .wildcard().end();</cfscript>The nested resource produces POST /posts/:postKey/comments → comments##create with named route postComments. Inside the Comments controller, the parent key is available as params.postKey. Rails-style inline blocks (.resources("posts", function(r) { ... })) are not supported — use the callback= named argument.
Namespaced sections
Section titled “Namespaced sections”Group a set of controllers under a shared URL prefix and a subfolder. .namespace("admin") prefixes URLs with /admin and loads controllers from app/controllers/admin/. Declare the namespaced routes between the .namespace() call and a matching .end():
<cfscript>mapper() .namespace("admin") .resources("posts") .end() .resources("posts") .wildcard().end();</cfscript>URLs become /admin/posts, /admin/posts/:key, and so on; the controller class is admin/Posts.cfc. The outer .resources("posts") still serves the public /posts URLs — namespaces don’t replace the non-namespaced routes, they sit alongside them.
namespace() (and scope(), package(), controller()) also accept a callback= argument, mirroring resources(): the routes declared inside the callback register under the namespace, and the scope closes automatically when the callback returns — no .end() needed:
<cfscript>mapper() .namespace(name="admin", callback=function(map) { map.resources("posts"); }) .resources("posts") .wildcard().end();</cfscript>Controllers that live in app/controllers/admin/ must extend the base controller with its full path — extends="app.controllers.Controller". The bare extends="Controller" fails to resolve from the subfolder on Lucee (invalid component definition, can't find component [Controller]).
Constrain pattern placeholders
Section titled “Constrain pattern placeholders”Pattern placeholders are written as [name]. By default a placeholder matches any string without a slash or dot ([^\./]+ — dots are reserved for the optional .[format] suffix). Pass a constraints struct to restrict a placeholder to a regex.
<cfscript>mapper() .get( name="postArchive", pattern="/posts/[year]/[month]", to="posts##archive", constraints={year="\d{4}", month="\d{2}"} ) .resources("posts") .wildcard().end();</cfscript>With the constraint, /posts/2026/04 matches and fills params.year="2026", params.month="04". /posts/foo/bar does not match — the router moves on to the next route. Without the constraint, /posts/foo/bar would match and params.year="foo".
Route helpers
Section titled “Route helpers”Every route with a name is reachable by name from anywhere in your app. The helpers take the name, resolve it to a URL, and fill in placeholders from named arguments.
linkTo(route="post", key=post.id, text="View")— renders an<a href="/posts/123">urlFor(route="newPost")— returns the URL stringredirectTo(route="posts")— 302 redirect in a controller actionbuttonTo(route="post", key=post.id, text="Delete", method="delete")— small form for non-GET verbsstartFormTag(route="post", key=post.id, method="put")— opens a form that posts to the route
Rename the resource once in routes.cfm and every helper updates. See Forms and Form Helpers for startFormTag examples in context.
Listing all routes
Section titled “Listing all routes”When a route isn’t matching what you expect, print the registered route table. wheels routes shows every route’s method, path, controller#action, and name. It reads the table from your running app — with no server up, it refuses and tells you to wheels start first:
wheels routesStart the server, then run wheels routes in your app directory to see the full table.