Routing
The routing system in CFWheels encourages a conventional RESTful and resourceful style of request handling.
The CFWheels routing system inspects a request's HTTP verb and URL and decides which controller and action to run.
Consider the following request:
HTTP
1
GET /products/5
Copied!
The routing system may match the request to a route like this, which tells CFWheels to load the show action on the Products controller:
1
.get(name="product", pattern="products/[key]", to="products##show")
Copied!

Configuring Routes

To configure routes, open the file at config/routes.cfm.
The CFWheels router begins with a call to mapper(), various methods chained from that, and lastly ends with a call to end().
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.

How Routes Work

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:

Name

A route name is set up for reference in your CFML code for building links, forms, and such. To build URLs, you'll use this name along with helpers like linkTo(), startFormTag(), urlFor(), and so on.

Method

A HTTP request method must be defined: GET, POST, PATCH, or DELETE.
You typically want to require POST, PATCH, or DELETE when a given action changes the state of your application's data:
  • Creating record(s): POST
  • Updating record(s): PATCH
  • Deleting record(s): DELETE
You can permit listing and showing records behind a normal HTTP GET request method.

Pattern and Parameters

A pattern is a URL path, sometimes with parameters in [squareBrackets]. Parameter values get sent to the controller in the params struct.
You'll see patterns like these in routes:
Example Route Patterns
1
posts/[key]/[slug]
2
posts/[key]
3
posts
Copied!
In this example, key and 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 params.key and params.slug, respectively.
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).

Viewing a List of Routes

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.

Resource Routing

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 config/routes.cfm:
/config/routes.cfm
1
mapper()
2
.resources("products")
3
.end();
Copied!
This will set up the following routes, pointing to specific actions within the products controller:
Name
HTTP Verb
Path
Controller & Action
Description
products
GET
/products
products.index
Display a list of all products
product
GET
/products/[key]
products.show
Display a specific product
newProduct
GET
/products/new
products.new
Display a form for creating a new product
products
POST
/products
products.create
Create a new product record
editProduct
GET
/products/[key]/edit
products.edit
Display a form for editing an existing product
product
PATCH/PUT
/products/[key]
products.update
Update an existing product record
product
DELETE
/products/[key]
products.delete
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 PUT or PATCH HTTP verb. It has been settled mostly that PATCH is the way to go for most situations. CFWheels resources set up both PUT and PATCH to address this confusion, but you should probably prefer linking up PATCH when you are able.

Singular Resources

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.
1
mapper()
2
.resource("cart")
3
.end();
Copied!
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:
Name
HTTP Verb
Path
Controller & Action
Description
cart
GET
/cart
carts.show
Display the cart
newCart
GET
/cart/new
carts.new
Display a form for creating a new cart
cart
POST
/cart
carts.create
Create a new cart record
editCart
GET
/cart/edit
carts.edit
Display a form for editing the cart
cart
PATCH/PUT
/cart
carts.update
Update the cart record
cart
DELETE
/cart
carts.delete
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.

Defining Individual URL Endpoints

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.
But sometimes you just need to define a single one-off route pattern. For this case, you have a method for each HTTP verb: get(), post(), patch(), put(), and delete().
As a refresher, these are the intended purpose for each HTTP verb:
HTTP Verb
Meaning
GET
Display a list or record
POST
Create a record
PATCH/PUT
Update a record or set of records
DELETE
Delete a record

Security Warning

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 POST, PUT, PATCH, or DELETE verbs for those sorts of routing endpoints.
Consider a few examples:
config/routes.cfm
1
mapper()
2
.patch(name="heartbeat", to="sessions##update")
3
4
.patch(
5
name="usersActivate",
6
pattern="users/[userKey]/activations",
7
to="activations##update"
8
)
9
10
.resources("users")
11
12
.get(name="privacy", controller="pages", action="privacy")
13
.get(name="dashboard", controller="dashboards", action="show")
14
.end();
Copied!
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 controller/action arguments. The toargument allows you to delineate controller and 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):
config/routes.cfm
1
mapper()
2
// The following is roughly equivalent to .resources("users")
3
.get(name="newUser", pattern="users/new", to="users##new")
4
.get(name="editUser", pattern="users/[key]/edit", to="users##edit")
5
.get(name="user", pattern="users/[key]", to="users##show")
6
.patch(name="user", pattern="users/[key]", to="users##update")
7
.put(name="user", pattern="users/[key]", to="users##update")
8
.delete(name="user", pattern="users/[key]", to="users##delete")
9
.post(name="users", to="users##create")
10
.get(name="users", to="users##index")
11
.end();
Copied!
If you need to limit the actions that are exposed by resources() and resource(), you can also pass in only or exceptarguments:
config/routes.cfm
1
mapper()
2
// Only offer endpoints for cart show, update, and delete:
3
// - GET /cart
4
// - PATCH /cart
5
// - DELETE /cart
6
.resource(name="cart", only="show,update,delete")
7
8
// Offer all endpoints for wishlists, except for delete:
9
// - GET /wishlists
10
// - GET /wishlists/new
11
// - GET /wishlists/[key]
12
// - GET /wishlists/[key]/edit
13
// - POST /wishlists
14
// - PATCH /wishlists/[key]
15
.resources(name="wishlists", except="delete")
16
.end();
Copied!

Browser Support for PUT, PATCH, and DELETE

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 GET or POST.
To get around this, the CFWheels router recognizes the specialized verbs from browsers (PUT, PATCH, and DELETE) in this way:
  • Via a POST request with a
  • POST variable named _method specifying the specific HTTP verb (e.g., _method=delete)
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 PATCH and DELETE.

Namespaces

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:
1
mapper()
2
.namespace("admin")
3
.resources("products")
4
.end()
5
.end();
Copied!
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 controllers/admin/Products.cfc.

Packages

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:
1
mapper()
2
.package("public")
3
.resources("articles")
4
.resource("profile")
5
.end()
6
.end();
Copied!
With this setup, end users will see /articles and /profile in the URL, but the controllers will be located at controllers/public/Articles.cfc and controllers/public/Profiles.cfc, respectively.

Nested Resources

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 appointment records.
In this situation, we'd perhaps want for our URL to look like this for editing a specific customer's appointment:
HTTP
1
GET /customers/489/appointments/1909/edit
Copied!
To code up this nested resource, we'd write this code in config/routes.cfm:
1
mapper()
2
.resources(name="customers", nested=true)
3
.resources("appointments")
4
.end()
5
.end();
Copied!
That will create the following routes:
Text
HTTP Verb
Path
Controller & Action
Description
newCustomerAppointment
GET
/customers/[customerKey]/appointments/new
appointments.new
Display a form for creating a new appointment for a specific customer
customerAppointment
GET
/customers/[customerKey]/appointments/[key]
appointments.show
Display an existing appointment for a specific customer
editCustomerAppointment
GET
/customers/[customerKey]/appointments/[key]/edit
appointments.edit
Display a form for editing an existing appointment for a specific customer
customerAppointment
PATCH/PUT
/customers/[customerKey]/appointments/[key]
appointments.update
Update an existing appointment record for a specific customer
customerAppointment
DELETE
/customers/[customerKey]/appointments/[key]
appointments.delete
Delete an existing appointment record for an specific customer
customerAppointments
GET
/customers/[customerKey]/appointments
appointments.index
List appointments for a specific customer
customerAppointments
POST
/customers/[customerKey]/appointments
appointments.create
Create an appointment record for a specific customer
newCustomer
GET
/customers/new
customers.new
Display a form for creating a customer
customer
GET
/customers/[key]
customers.show
Display an existing customer
editCustomer
GET
/customers/[key]/edit
customers.edit
Display a form for editing an existing customer
customer
PATCH/PUT
/customers/[key]
customers.update
Update an existing customer
customer
DELETE
/customers/[key]
customers.delete
Delete an existing customer
customers
GET
/customers
customers.index
Display a list of all customers
customers
POST
/customers
customers.create
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 key ID.
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:
1
mapper()
2
// /products/[key]
3
.resources(name="products", nested=true)
4
// /products/[productKey]/promote
5
.patch(name="promote", to="promotions##create")
6
// /products/[productKey]/expire
7
.delete(name="expire", to="expirations##create")
8
9
// A 2nd-level resource
10
// /products/[productKey]/variations/[key]
11
.resources(name="variations", nested=true)
12
// A 3rd-level resource
13
// /products/[productKey]/variations/[variationKey]/primary
14
.resource("primary")
15
.end()
16
.end()
17
.end();
Copied!

Wildcard Routes

CFWheels 1.x had a default routing pattern: [controller]/[action]/[key]. The convention for URLs was as follows:
HTTP
1
GET /news/show/5
Copied!
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 5.
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:
1
mapper()
2
.wildcard()
3
.end();
Copied!
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 wildcard generates:
1
/news/new
2
/news/create
3
/news/index
4
/news
Copied!
The 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 method or methods argument:
1
mapper()
2
.wildcard(methods="get,post")
3
.end();
Copied!

Security Warning

Specifying a 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 (resources, resource) and the other verb-based helpers (get, post, patch, put, and delete) listed above instead.

Order of Precedence

CFWheels gives precedence to the first listed custom route in your config/routes.cfm file.
Consider this example to demonstrate when this can create unexpected issues:
1
mapper()
2
.resources("users")
3
4
.get(
5
name="usersPromoted",
6
pattern="users/promoted",
7
to="userPromotions##index"
8
)
9
.end();
Copied!
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:
1
mapper()
2
.get(
3
name="usersPromoted",
4
pattern="users/promoted",
5
to="userPromotions##index"
6
)
7
8
.resources("users")
9
.end();
Copied!

Making a Catch-All Route

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 /welcome-to-the-site and /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:
1
mapper()
2
.get(name="page", pattern="[title]", to="pages##show")
3
.end();
Copied!
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 welcome-to-the-site.
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:
1
mapper()
2
.resources("products")
3
.get(name="logout", to="sessions#delete")
4
.get(name="page", pattern="[title]", to="pages##show")
5
.root(to="dashboards##show")
6
.end();
Copied!
products and 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().

Constraints

The constraints feature can be added either at an argument level directly into a resources() or other individual route call, or can be added as a chained function in it's own right. Constraints allow you to add regex to a route matching pattern, so you could for instance, have /users/1/ and /users/bob/ go to different controller actions.
1
mapper()
2
// users/1234
3
.resources(name = "users", constraints = { key = "\d+" })
4
// users/abc123
5
.resources(name = "users", constraints = { key = "\w+\d+" })
6
.end();
Copied!
Constraints can also be used as a wrapping function:
1
mapper()
2
.constraints( key = "\d+")
3
.resources("users")
4
.resources("cats")
5
.resources("dogs")
6
.end()
7
.end()
Copied!
In this example, the key argument being made of digits only will apply to all the nested resources

Wildcard Segments

Wildcard segments allow for wildcards to be used at any point in the URL pattern.
1
mapper()
2
// Match /user/anything/you/want
3
.get(name="user/*[username]", to="users##search")
4
// Match /user/anything/you/want/search/
5
.get(name="user/*[username]/search", to="users##search")
6
.end()
Copied!
In the above example, anything/you/want you gets set to the params.username including the /'s. The second example would require /search/ to be on the end of the URL

Shallow Resources

If you have a nested resource where you want to enforce the presence of the parent resource, but only on creation of that resource, then shallow routes can give you a bit of a short cut. An example might be Blog Articles, which have Comments. If we're thinking in terms of our models, let's say that Articles Have Many Comments.
1
mapper()
2
.resources(name="articles", nested=true, shallow=true)
3
.resources("comments")
4
.resources("quotes")
5
.resources("drafts")
6
.end()
7
.end()
Copied!
Without shallow routes, this block would create RESTful actions for all the nested resources, for example /articles/[articleKey]/comments/[key]
With Shallow resources, we can automatically put the index, create and new RESTful actions with the ArticleKey in the URL, but then separate out edit, show, update and delete actions into their own, and simpler URL path; When we edit or update a comment, we're doing it on that object as it's own entity, and the relationship to the parent article already exists.
So in this case, we get index, new and create with the /articles/[articleKey]/ part in the URL, but to show, edit, update or delete a comment, we can just fall back to /comments/

Member and Collection Blocks

A member() block is used within a nested resource to create routes which act 'on an object'; A member route will require an ID, because it acts on a member. photos/1/preview is an example of a member route, because it acts on (and displays) a single object.
1
mapper()
2
// Create a route like `photos/1/preview`
3
.resources(name="photos", nested=true)
4
.member()
5
.get("preview")
6
.end()
7
.end()
8
.end();
Copied!
A collection route doesn't require an id because it acts on a collection of objects. photos/search is an example of a collection route, because it acts on (and displays) a collection of objects.
1
mapper()
2
// Create a route like `photos/search`
3
.resources(name="photos", nested=true)
4
.collection()
5
.get("search")
6
.end()
7
.end()
8
.end();
Copied!

Redirection

As of CFWheels 2.1, you can now use a redirect argument on GET, POST, PUT, PATCH, and DELETE requests. This will execute before reaching any controllers, and perform a 302 redirect immediately after the route is matched.
CFScript
1
mapper()
2
.get(name="foo", redirect="https://www.google.com")
3
.post(name="foo", redirect="https://www.google.com")
4
.put(name="foo", redirect="https://www.google.com")
5
.patch(name="foo", redirect="https://www.google.com")
6
.delete(name="foo", redirect="https://www.google.com")
7
.end()
Copied!
This is useful for the occasional redirect, and saves you having to create a dedicated controller filter or action just to perform a simple task. For large amounts of redirects, you may want to look into adding them at a higher level - e.g in an Apache VirtualHost configuration, as that will be more performant.

Disabling automatic [format] routes

Note

Introduced in CFWheels 2.1
By default, CFWheels will add .[format] routes when using resources(). You may wish to disable this behaviour to trim down the number of generated routes for clarity and performance reasons (or you just don't use this feature!).
You can either disable this via mapFormat = false on a per resource basis, or more widely, on a mapper basis:
1
// For all chained calls
2
mapper(mapFormat=false)
3
.resources("users")
4
.end()
5
6
// or just for this resource
7
mapper()
8
.resources(mapFormat=false, name="users)
9
.end()
Copied!