Skip to content

feat: route-level cache rules via Router.withCache() - #682

Merged
lmajano merged 4 commits into
developmentfrom
claude/coldbox-express-nuxt-analysis-wfl4r6
Aug 17, 2026
Merged

feat: route-level cache rules via Router.withCache()#682
lmajano merged 4 commits into
developmentfrom
claude/coldbox-express-nuxt-analysis-wfl4r6

Conversation

@lmajano

@lmajano lmajano commented Aug 17, 2026

Copy link
Copy Markdown
Member

Description

Adds a route-scoped alternative to handler cache="true" annotations, so caching can be declared where the URL is declared instead of being buried on the handler action.

Router.cfc gains a fluent .withCache() modifier and 12 new route-struct keys, mirroring every existing handler-level cache annotation one-for-one:

  • cache, cacheTimeout, cacheLastAccessTimeout, cacheProvider
  • cacheSuffix, cacheInclude, cacheExclude, cacheFilter
  • etag, etagWeak, lastModified, cacheControl (Tier 1 HTTP caching)
// cache for 60 minutes, default RC-based key
route( "/api/products" ).withCache( timeout = 60 ).to( "products.index" );

// add conditional-GET support
route( "/api/products/:id" ).withCache( timeout = 60, etag = true ).to( "products.show" );

Precedence

A route that calls .withCache() takes full precedence over that same event's handler-level cache annotations for any request matching it. HandlerService.getRouteCachingMetadata() reads the matched route's record and is consulted first by both the pre-execution cache lookup (getEventMetadataEntry()) and the post-execution cache write (getEventCachingMetadata()).

It is deliberately not memoized the way the handler-annotation dictionary is - a route record is already a cheap struct read, and recomputing it fresh per request is what lets two different routes to the same event carry two different cache policies, which the event-name-keyed handler dictionary could never do.

Routes that don't opt in fall through unchanged to the existing handler-annotation path - zero behavior change for existing apps.

Implementation notes

  • Reuses the existing EventURLFacade/CacheBox/Bootstrap.cfc caching plumbing - no new subsystem, no other runtime files needed to change.
  • Route-level cacheSuffix closures use signature function( event ), distinct from the handler-level EVENT_CACHE_SUFFIX's function( eventHandlerBean, event ), since a route has no reflected handler action metadata to hand it.
  • Also includes a source-grounded analysis document (docs/analysis/) that this feature was scoped from, examining ColdBox's routing/interception composition model against its own source.

Jira Issues

COLDBOX-1418

Type of change

  • Bug Fix
  • Improvement
  • New Feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • My code follows the style guidelines of this project cfformat
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Generated by Claude Code

claude added 3 commits August 17, 2026 03:21
Grounded pass through system/ to evaluate which ideas from Express 5
(middleware chains) and Nuxt 4/Nitro (layers, route rules, DevTools) are
worth adopting. Confirms route-scoped middleware, HTTP caching primitives,
generalized SSE, and AI conversational context already close what were
previously the sharpest gaps. Narrows remaining recommendations to
route-level cache rules, a first-party introspection/DevTools surface,
and app-level config layers - each cited against actual file/line sources.

Analysis document only. No framework code changes.
Adds a route-scoped alternative to handler cache="true" annotations,
matching Nitro's routeRules idea from the Nuxt/Express analysis. A route
that calls .withCache() gets cache, cacheTimeout, cacheLastAccessTimeout,
cacheProvider, cacheSuffix, cacheInclude, cacheExclude, cacheFilter, and
the Tier 1 HTTP caching flags (etag, etagWeak, lastModified, cacheControl)
on its route record - the same set a handler annotation already supports,
one-for-one.

HandlerService.getRouteCachingMetadata() reads the matched route's record
and, when it declares cache=true, takes full precedence over that handler's
own annotations for the request - checked first by both the pre-execution
cache lookup (getEventMetadataEntry()) and the post-execution cache write
(getEventCachingMetadata()). Deliberately not memoized like the handler-
annotation dictionary: a route record is already a cheap struct read, and
recomputing it fresh per request is what lets two different routes to the
same event carry two different cache policies, which the event-name-keyed
handler dictionary could never do. Falls through unchanged to the existing
handler-annotation path when a route doesn't opt in - zero behavior change
for existing apps.

Rides the same EventURLFacade/CacheBox/Bootstrap.cfc plumbing a cache="true"
annotation already uses, so no other file needed to change.

Updates the Nuxt/Express analysis doc to mark this recommendation shipped.
… return

private struct function getRouteCachingMetadata() returned a bare `return;`
(null) for the common "route doesn't opt into caching" case. Lucee enforces
the declared struct return type strictly and throws
UDFCasterException: Cannot cast null value to value of type [struct]
on every request, which is why CI failed with 62 TestBox failures on
lucee@5/lucee@6 - any spec touching HandlerService (event execution,
handler bean lookups, etc) exercises getEventMetadataEntry(), which now
always calls this function.

Drop the explicit struct return type, matching the same
"function that may return null" convention RequestService.cfc's
getContextFromScope() already uses elsewhere in this codebase. Runtime
behavior is unchanged - callers already null-check with isNull().
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

Test Results

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit 09b1e8c. ± Comparison against base commit 688739c.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces route-level caching declarations via Router.withCache(), allowing cache policy to live alongside URL definitions while reusing ColdBox’s existing Event Caching + Tier 1 HTTP caching plumbing.

Changes:

  • Added Router.withCache() and expanded the route record shape with cache/Tier-1 HTTP caching keys (etag, lastModified, cacheControl, etc.).
  • Updated HandlerService to prefer route-derived caching metadata (from the matched route record) over handler action cache annotations for the current request.
  • Added/updated specs covering route cache defaults, stored values, closure suffix behavior, and precedence.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/specs/web/services/HandlerServiceTest.cfc Adds integration coverage for route-driven caching metadata and precedence in HandlerService.
tests/specs/web/routing/RouterTest.cfc Adds router specs verifying defaults and withCache() persistence on the route record.
system/web/services/HandlerService.cfc Implements getRouteCachingMetadata() and consults it from both read/write caching metadata paths.
system/web/routing/Router.cfc Adds route-record cache fields and implements the fluent withCache() modifier.
docs/analysis/nuxt-express-coldbox-analysis.md Adds a source-grounded analysis document describing motivations and related framework comparisons.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +207 to +224
function buildRouteRecord( struct overrides = {} ){
var base = {
"cache" : true,
"cacheTimeout" : 60,
"cacheLastAccessTimeout" : "",
"cacheProvider" : "template",
"cacheSuffix" : "",
"cacheInclude" : "*",
"cacheExclude" : "",
"cacheFilter" : "",
"etag" : false,
"etagWeak" : false,
"lastModified" : false,
"cacheControl" : ""
};
base.append( arguments.overrides, true );
return base;
}
buildRouteRecord() was declared as a named function nested inside a
describe() closure. This spec suite's convention (and TestBox specs
generally) is to assign test helpers to a local var closure instead,
sidestepping any engine differences around nested named function
declarations. No behavior change - same signature, same call sites.
@lmajano
lmajano merged commit 3bfdbfa into development Aug 17, 2026
28 checks passed
@lmajano
lmajano deleted the claude/coldbox-express-nuxt-analysis-wfl4r6 branch August 17, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants