Core Concepts
The Request Lifecycle
When a request arrives at your Wheels app, it passes through eight stages on the way to a response. Knowing the order tells you where to put code: auth in middleware, record loading in filters, per-action logic in the action itself, display logic in views.
You’ll learn:
- The eight stages a request flows through
- Where your code hooks into each stage
- Why the order matters for design decisions
- How to diagnose failures from symptoms
The pipeline
Section titled “The pipeline”HTTP request │ ▼(1) Dispatch + route match — pick controller + action; unmatched URLs 404 here │ ▼(2) Middleware — before any controller exists │ ▼(3) Controller instantiation — filters registered, services resolved │ ▼(4) beforeAction filters — load records, enforce auth │ ▼(5) Controller action — your code │ ▼(6) afterAction filters │ ▼(7) View rendering — layout, partials, helpers │ ▼(8) Response sentStage by stage
Section titled “Stage by stage”1. Dispatch and route match
Section titled “1. Dispatch and route match”The router resolves the URL in two steps: placeholder-free patterns are checked against an exact-path index first (a literal path beats a placeholder route regardless of declaration order), then everything else is scanned top-to-bottom — first match wins (see How Routing Works). A match resolves to a controller name, an action name, and a set of route params (including params.key from [key] segments). Route model binding (when binding=true on a resource) loads params.<singular> from the database here — before your action runs. An unmatched URL throws Wheels.RouteNotFound (a 404) right here — nothing further in the pipeline runs for it.
2. Middleware
Section titled “2. Middleware”Runs after the route has matched but before the controller is instantiated. Each middleware gets the request struct (which carries the matched route and params) and a next function. It can inspect, mutate, short-circuit, or pass through. Ordering is significant — middleware registered first sees the request first and sees the response last.
One carve-out: a CORS preflight (OPTIONS) request is handed to the Cors middleware before route matching, so preflights succeed even for routes that only declare GET/POST.
Typical uses: rate limiting, CORS, security headers, request IDs, auth gate, logging.
3. Controller instantiation
Section titled “3. Controller instantiation”Wheels constructs the controller object for this request. Its config() method — where filters are registered and services injected — runs once per application lifetime (on the first request after a start or reload) and the result is cached. What happens per-request is instance initialization and DI service resolution: the services you declared in config() are looked up from the container fresh for each request.
4. beforeAction filters
Section titled “4. beforeAction filters”Registered private methods that run before the action. This is where you put cross-action logic: checking authentication, loading a parent record, enforcing ownership. A filter can call redirectTo() to short-circuit — the action never runs.
5. Controller action
Section titled “5. Controller action”Your code. Reads params, calls models, orchestrates the response. Sets instance variables that views read.
6. afterAction filters
Section titled “6. afterAction filters”Rare. Use for logging, analytics, cleanup. The action’s render has already produced the response body by this point, but it is not locked in — an afterAction filter that calls renderText() or renderView() replaces it. Avoid render calls in after-filters unless replacing the body is exactly what you want.
7. View rendering
Section titled “7. View rendering”The default behavior looks up app/views/<controller>/<action>.cfm and renders it through app/views/layout.cfm. The action can override with renderView(action="other") or bypass rendering entirely with redirectTo(...). Helpers called from views access the same params, flash, and session the action saw.
8. Response sent
Section titled “8. Response sent”Headers set by middleware on the way out (security headers, CORS, request ID). The response body is written to the socket and the request is done.
Where your code hooks in
Section titled “Where your code hooks in”| Hook | File | Runs |
|---|---|---|
| Routes | config/routes.cfm | Stage 1 (defined once at boot) |
| Middleware | app/middleware/*.cfc, registered in config/settings.cfm | Stage 2, every matched request — unmatched URLs 404 before middleware runs |
| DI registration | config/services.cfm | Once at boot |
| Controller filters | config() in app/controllers/*.cfc | Stage 3, once per app start/reload (cached) |
| Filter logic | Private methods in controller | Stages 4 and 6 |
| Action | Public method in controller | Stage 5 |
| View | app/views/<controller>/<action>.cfm | Stage 7 |
| Helpers | app/views/helpers.cfm and globals | Stage 7 (called from views) |
Why the order matters
Section titled “Why the order matters”Middleware runs before the controller exists. That’s why rate limiting and auth checks go there — they can reject a request without paying the cost of instantiating a controller. But middleware runs after route matching, so it only sees matched requests; an unmatched URL 404s before any middleware fires.
beforeAction filters run after config(). That’s why they can rely on injected services (this.emailService) — the DI container has already resolved them.
View rendering is inside the request, not after it. Helpers and views see the same params, flash, and session the action saw. A view is not a separate subprocess.
redirectTo() in a filter skips everything below. Auth check fails → redirectTo("login") → no action, no view, just a 302 response. Filters can short-circuit cleanly.
When something breaks
Section titled “When something breaks”Common symptoms and which stage caused them:
Could not find a route that matched this request.(Wheels.RouteNotFound) — stage 1 (dispatch) couldn’t match the URL againstroutes.cfm. Check order: resources before root before wildcard.Could not find the view page for the <action> actionfor an action you never meant to render — the action isn’t declared on the controller, or the controller wasn’t reloaded (wheels reload). A missing action doesn’t error at stage 5; it falls through to auto-render and surfaces at stage 7 as a missing view (Wheels.ViewNotFound, HTTP 404).params.post is undefinedin show/edit/update/delete — stage 1 route model binding wasn’t enabled. Setbinding=trueon the resource, or set the globalrouteModelBinding=trueinconfig/settings.cfm. Wheels prints a dev-mode warning hinting at this.- 404 with
Wheels.ViewNotFoundfor an action that exists — stage 7 couldn’t find the view file. The path isapp/views/<controller>/<action>.cfmall lowercase. A missing view is never a blank 200 — it’s a loud 404: the full error page in development, the generic 404 page in production. - Redirect happens but you expected a render — a filter (stage 4) or the action itself called
redirectTo()and short-circuited. Check the filters list inconfig(). - CORS error in browser, curl works fine — stage 2 Cors middleware isn’t registered, or its
allowOriginsdoesn’t include the browser’s origin.
See also
Section titled “See also”- MVC in Wheels — where each layer lives and what it owns
- How Routing Works — stage 1 in depth
- Middleware Pipeline — stage 2 in depth
- The Dependency Injection Container — what
config()resolves - Controllers and Actions — the hands-on how-to for stages 3-6