The CFWheels routing system inspects a request's HTTP verb and URL and decides which controller and action to run.
Consider the following request:
The routing system may match the request to a route like this, which tells CFWheels to load the
show action on the
.get(name="product", pattern="products/[key]", to="products##show")
To configure routes, open the file at
The CFWheels router begins with a call to mapper(), various methods chained from that, and lastly ends with a call to
In many cases, if you need to know where to go in the code to work with existing functionality in an application, the
routes.cfm file can be a handy map, telling you which controller and action to start looking in.
The various route mapping methods that we'll introduce in this chapter basically set up a list of routes, matching URL paths to a controllers and actions within your application.
The terminology goes like this:
A HTTP request method must be defined:
You typically want to require
DELETE when a given action changes the state of your application's data:
- Creating record(s):
- Updating record(s):
- Deleting record(s):
You can permit listing and showing records behind a normal HTTP
GET request method.
A pattern is a URL path, sometimes with parameters in
[squareBrackets]. Parameter values get sent to the controller in the
You'll see patterns like these in routes:
posts/[key]/[slug] posts/[key] posts
In this example,
slug are parameters that must be present in the URL for the first route to match, and they are required when linking to the route. In the controller, these parameters will be available at
When a request is made to CFWheels, the router will look for the first route that matches the requested URL. As an example, this means that if
key is present in the URL but not
slug, then it's the second route above that will match.
Please note that
. is treated as a special character in patterns and should generally not be used (one exception being when you are responding with multiple formats).
In the debugging footer, you'll see a View Routes link next to your application's name:
[Reload, View Routes, Run Tests, View Tests]
Clicking that will load a filterable list of routes drawn in the
config/routes.cfm file, including name, method, pattern, controller, and action.
If you don't see debugging information at the bottom of the page, see the docs for the
showDebugInformation setting in the Configuration and Defaults chapter.
Many parts of your application will likely be CRUD-based (create, read, update, delete) for specific types of records (users, products, categories). Resources allow you to define a conventional routing structure for this common use case.
Resources are important
You'll want to pay close attention to how resource-based routing works because this is considered an important convention in CFWheels applications.
If we have a
products table and want to have a section of our application for managing the products, we can set up the routes using the resources() method like this in
mapper() .resources("products") .end();
This will set up the following routes, pointing to specific actions within the
Display a list of all products
Display a specific product
Display a form for creating a new product
Create a new product record
Display a form for editing an existing product
Update an existing product record
Delete an existing product record
Because the router uses a combination of HTTP verb and path, we only need 4 different URL paths to connect to 7 different actions on the controller.
Whats with the `PUT`?
There has been some confusion in the web community on whether requests to update data should happen along with a
PATCH HTTP verb. It has been settled mostly that
PATCH is the way to go for most situations. CFWheels resources set up both
PATCH to address this confusion, but you should probably prefer linking up
PATCH when you are able.
Standard resources using the resources() method assume that there is a primary key associated with the resource. (Notice the
[key] placeholder in the paths listed above in the Strongly Encouraged: Resource Routing section.)
CFWheels also provides a singular resource for routing that will not expose a primary key through the URL.
mapper() .resource("cart") .end();
This is handy especially when you're manipulating records related directly to the user's session (e.g., a profile or a cart can be managed by the user without exposing the primary key of the underlying database records).
Calling resource() (notice that there's no "s" on the end) then exposes the following routes:
Display the cart
Display a form for creating a new cart
Create a new cart record
Display a form for editing the cart
Update the cart record
Delete the cart
Note that even though the resource path is singular, the name of the controller is plural by convention.
Also, this example is slightly contrived because it doesn't make much sense to create a "new" cart as a user typically just has one and only one cart tied to their session.
As you've seen, defining a resource creates several routes for you automatically, and it is great for setting up groupings of routes for managing resources within your application.
As a refresher, these are the intended purpose for each HTTP verb:
Display a list or record
Create a record
Update a record or set of records
Delete a record
We strongly recommend that you not allow any
GET requests to modify resources in your database (i.e., creating, updating, or deleting records). Always require
DELETE verbs for those sorts of routing endpoints.
Consider a few examples:
mapper() .patch(name="heartbeat", to="sessions##update") .patch( name="usersActivate", pattern="users/[userKey]/activations", to="activations##update" ) .resources("users") .get(name="privacy", controller="pages", action="privacy") .get(name="dashboard", controller="dashboards", action="show") .end();
Rather than creating a whole resource for simple one-off actions or pages, you can create individual endpoints for them.
Notice that you can use the
to="controller##action" or use separate
action arguments. The
to argument allows you to delineate
action within a single string using a
# separator (which must be escaped as a double
## because of CFML's special usage of that symbol within string syntax).
In fact, you could mock a
users resource using these methods like so (though obviously there is little practical reason for doing so):
mapper() // The following is roughly equivalent to .resources("users") .get(name="newUser", pattern="users/new", to="users##new") .get(name="editUser", pattern="users/[key]/edit", to="users##edit") .get(name="user", pattern="users/[key]", to="users##show") .patch(name="user", pattern="users/[key]", to="users##update") .put(name="user", pattern="users/[key]", to="users##update") .delete(name="user", pattern="users/[key]", to="users##delete") .post(name="users", to="users##create") .get(name="users", to="users##index") .end();
mapper() // Only offer endpoints for cart show, update, and delete: // - GET /cart // - PATCH /cart // - DELETE /cart .resource(name="cart", only="show,update,delete") // Offer all endpoints for wishlists, except for delete: // - GET /wishlists // - GET /wishlists/new // - GET /wishlists/[key] // - GET /wishlists/[key]/edit // - POST /wishlists // - PATCH /wishlists/[key] .resources(name="wishlists", except="delete") .end();
While web standards advocate for usage of these specific HTTP verbs for requests, web browsers don't do a particularly good job of supporting verbs other than
To get around this, the CFWheels router recognizes the specialized verbs from browsers (
DELETE) in this way:
- Via a
POSTrequest with a
_methodspecifying the specific HTTP verb (e.g.,
See the chapter on Linking Pages for strategies for working with this constraint.
Note that using CFWheels to write a REST API doesn't typically have this constraint. You should confidently require API clients to use the specific verbs like
The CFWheels router allows for namespaces: the ability to add a route to a "subfolder" in the URL as well as within the
controllers folder of your application.
Let's say that we want to have an "admin" section of the application that is separate from other "public" sections. We'd want for all of the "admin" controllers to be within an admin subfolder both in the URL and our application.
That's what the namespace() method is for:
mapper() .namespace("admin") .resources("products") .end() .end();
In this example, we have an admin section that will allow the user to manage products. The URL would expose the products section at
/admin/products, and the controller would be stored at
Let's say that you want to group a set of controllers together in a subfolder (aka package) in your application but don't want to affect a URL. You can do so using the
package mapper method:
mapper() .package("public") .resources("articles") .resource("profile") .end() .end();
With this setup, end users will see
/profile in the URL, but the controllers will be located at
You'll often find yourself implementing a UI where you need to manipulate data scoped to a parent record. Creating nested resources allows you to reflect this nesting relationship in the URL.
Let's consider an example where we want to enable CRUD for a
customer and its children
In this situation, we'd perhaps want for our URL to look like this for editing a specific customer's appointment:
To code up this nested resource, we'd write this code in
mapper() .resources(name="customers", nested=true) .resources("appointments") .end() .end();
That will create the following routes:
Display a form for creating a new appointment for a specific customer
Display an existing appointment for a specific customer
Display a form for editing an existing appointment for a specific customer
Update an existing appointment record for a specific customer
Delete an existing appointment record for an specific customer
List appointments for a specific customer
Create an appointment record for a specific customer
Display a form for creating a customer
Display an existing customer
Display a form for editing an existing customer
Update an existing customer
Delete an existing customer
Display a list of all customers
Create a customer
Notice that the routes for the
appointments resource contain a parameter named
customerKey. The parent resource's ID will always be represented by its name appended with
Key. The child will retain the standard
You can nest resources and routes as deep as you want, though we recommend considering making the nesting shallower after you get to a few levels deep.
Here's an example of how nesting can be used with different route mapping methods:
mapper() // /products/[key] .resources(name="products", nested=true) // /products/[productKey]/promote .patch(name="promote", to="promotions##create") // /products/[productKey]/expire .delete(name="expire", to="expirations##create") // A 2nd-level resource // /products/[productKey]/variations/[key] .resources(name="variations", nested=true) // A 3rd-level resource // /products/[productKey]/variations/[variationKey]/primary .resource("primary") .end() .end() .end();
CFWheels 1.x had a default routing pattern:
[controller]/[action]/[key]. The convention for URLs was as follows:
With this convention, the URL above told CFWheels to invoke the
show action in the
news controller. It also passed a parameter called
key to the action, with a value of
If you're upgrading from 1.x or still prefer this style of routing for your CFWheels 2+ application, you can use the wildcard() method to enable it part of it:
mapper() .wildcard() .end();
CFWheels 2 will only generate routes for
[controller]/[action], however, because resources and the other routing methods are more appropriate for working with records identified by primary keys.
Here is a sample of the patterns that
/news/new /news/create /news/index /news
wildcard method by default will only generate routes for the
GET request method. If you would like to enable other request methods on the wildcard, you can pass in the
mapper() .wildcard(methods="get,post") .end();
method argument to
wildcard with anything other than
get gives you the potential to accidentally expose a route that could change data in your application with a
GET request. This opens your application to Cross Site Request Forgery (CSRF) vulnerabilities.
wildcard is provided for convenience. Once you're comfortable with routing concepts in CFWheels, we strongly recommend that you use resources (
resource) and the other verb-based helpers (
delete) listed above instead.
CFWheels gives precedence to the first listed custom route in your
Consider this example to demonstrate when this can create unexpected issues:
mapper() .resources("users") .get( name="usersPromoted", pattern="users/promoted", to="userPromotions##index" ) .end();
In this case, when the user visits
/users/promoted, this will load the
show action of the
users controller because that was the first pattern that was matched by the CFWheels router.
To fix this, you need the more specific route listed first, leaving the dynamic routing to pick up the less specific pattern:
mapper() .get( name="usersPromoted", pattern="users/promoted", to="userPromotions##index" ) .resources("users") .end();
Sometimes you need a catch-all route in CFWheels to support highly dynamic websites (like a content management system, for example), where all requests that are not matched by an existing route get passed to a controller/action that can deal with it.
Let's say you want to have both
/terms-of-use handled by the same controller and action. Here's what you can do to achieve this.
First, add a new route to
config/routes.cfm that catches all pages like this:
mapper() .get(name="page", pattern="[title]", to="pages##show") .end();
Now when you access
/welcome-to-the-site, this route will be triggered and the
show action will be called on the
pages controller with
params.title set to
The problem with this is that this will break any of your normal controllers though, so you'll need to add them specifically before this route. (Remember the order of precedence explained above.)
You'll end up with a
config/routes.cfm file looking something like this:
mapper() .resources("products") .get(name="logout", to="sessions#delete") .get(name="page", pattern="[title]", to="pages##show") .root(to="dashboards##show") .end();
sessions are your normal controllers. By adding them to the top of the routes file, CFWheels looks for them first. But your catch-all route is more specific than the site root (
/), so your catch-all should be listed before the call to root().