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:
The routing system may match the request to a route like this, which tells CFWheels to load the show
action on the Products
controller:
Configuring Routes
To configure routes, open the file at app/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:
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 characters in patterns and should generally not be used (one exception being when you are responding with multiple formats). If your parameters may have .
in their value, please use the long form URL format: /?controller=[controller_name]&action=[action_name]&[parameter_name]=[parameter_value]
Viewing a List of Routes
In the debugging footer, you'll see a Routes link:
[info, View Routes, Docs, Tests, Migrator, Plugins]
Clicking that will load a filterable list of routes drawn in the app/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 app/config/routes.cfm
:
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.
What's 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.
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:
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 to
argument 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):
If you need to limit the actions that are exposed by resources() and resource(), you can also pass in only
or except
arguments:
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 aPOST
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:
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 app/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:
With this setup, end users will see /articles
and /profile
in the URL, but the controllers will be located at app/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:
To code up this nested resource, we'd write this code in app/config/routes.cfm
:
That will create the following routes:
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:
Wildcard Routes
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 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:
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:
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:
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 app/config/routes.cfm
file.
Consider this example to demonstrate when this can create unexpected issues:
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:
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 app/config/routes.cfm
that catches all pages like this:
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 app/config/routes.cfm
file looking something like this:
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.
Constraints can also be used as a wrapping function:
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.
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.
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.
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.
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
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 behavior 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:
\
Last updated