Skip to content

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
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 sent

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.

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.

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.

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.

Your code. Reads params, calls models, orchestrates the response. Sets instance variables that views read.

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.

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.

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.

HookFileRuns
Routesconfig/routes.cfmStage 1 (defined once at boot)
Middlewareapp/middleware/*.cfc, registered in config/settings.cfmStage 2, every matched request — unmatched URLs 404 before middleware runs
DI registrationconfig/services.cfmOnce at boot
Controller filtersconfig() in app/controllers/*.cfcStage 3, once per app start/reload (cached)
Filter logicPrivate methods in controllerStages 4 and 6
ActionPublic method in controllerStage 5
Viewapp/views/<controller>/<action>.cfmStage 7
Helpersapp/views/helpers.cfm and globalsStage 7 (called from views)

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.

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 against routes.cfm. Check order: resources before root before wildcard.
  • Could not find the view page for the <action> action for 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 undefined in show/edit/update/delete — stage 1 route model binding wasn’t enabled. Set binding=true on the resource, or set the global routeModelBinding=true in config/settings.cfm. Wheels prints a dev-mode warning hinting at this.
  • 404 with Wheels.ViewNotFound for an action that exists — stage 7 couldn’t find the view file. The path is app/views/<controller>/<action>.cfm all 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 in config().
  • CORS error in browser, curl works fine — stage 2 Cors middleware isn’t registered, or its allowOrigins doesn’t include the browser’s origin.