Routing
Introduction
Routes map URLs and HTTP methods to closures or controller actions. JCC Express MVC registers them when the application boots (see app.run() and the request lifecycle): RouteServiceProvider loads each configured route file under a path prefix. There is no route cache—unlike Laravel’s php artisan route:cache, routes are built from your TypeScript modules on every process start. That keeps development simple; production startup cost is the cost of evaluating your route files once per worker.
Basic routing
The smallest route pairs a URI with a handler. Import Route from the framework entry you use in the rest of the app (for example jcc-express-mvc/Core, which re-exports the router).
Controller array syntax matches Laravel’s style: [ControllerClass, "methodName"]. For @Inject (constructor DI), @Method (action DI), and mapping route params with @Method({ params: [...] }), see Controllers.md in this folder.
httpContext and request() / response() helpers
After the framework binds the current HTTP cycle to the container, you can use httpContext (import from jcc-express-mvc or your app’s framework barrel) and the globals request() / response().
Rule: never assign from httpContext inside the function body—no const { next } = httpContext, const { req, res } = httpContext, and so on. next (and the rest of the context) must come from parameter defaults where that pattern applies.
Controller methods (called through the container with @Method()): use default destructuring on the parameters, for example:
request() and response(): global helpers resolve the current request and response from the container. You may call them inside a method or closure body when you do not want to pass req / res around.
Inline route closures (Route.get(..., (req, res, next) => { ... })): Express always passes req, res, and next as arguments. Use that next when you need it. You can still use request() / response() in the body for req / res; do not pull next from httpContext inside the closure (and do not use ({ next } = httpContext) as the first parameter here—Express’s first argument is the request object, not a context bundle).
Where routes live and how they load
Route files live under the project route/ directory (for example route/web.ts, route/api.ts). They are not auto-discovered by filename alone: you declare them in bootstrap/app.ts via Application.configuration().withRouting([...]).
Each entry supplies:
name— Module path used torequirethe file (e.g."route/web").prefix— URI prefix applied to every route in that file (e.g.""for web,"/api"for API).
The RouteServiceProvider wraps each file in Route.basePath(prefix).group(...), so paths inside the file are relative to that prefix. Queue dashboard routes are registered separately by the provider; your app routes are whatever you list in withRouting.
Available router methods
The static Route class registers handlers per HTTP verb:
There is no built-in Route.match, Route.any, or Route.options on this facade. To support multiple verbs or OPTIONS, register separate lines or a small closure that branches on req.method.
Route parameters: :id and {id}
Express traditionally uses :param segments. JCC also accepts Laravel-style braces: {param} is normalized to :param before registration, so both forms behave the same.
You can mix styles in one path if you prefer; the registered path is always Express-compatible (:name).
Dependency injection on route handlers
For controller actions decorated with @Method(), the container resolves constructor and method dependencies (see the service container documentation). Plain route closures receive the usual Express (req, res, next) signature; they do not get automatic container method injection unless you resolve services manually inside the closure. See httpContext and request() / response() helpers for how httpContext may appear only in parameter defaults (controllers) and how closures should use Express’s next.
CSRF and method spoofing
HTML forms cannot send PUT, PATCH, or DELETE natively. The framework provides method spoofing middleware (methodSpoofing from jcc-express-mvc/Core) so a hidden _method field can override the verb when paired with POST.
Routes that change state and are reached from browser forms should sit behind your web middleware stack, which typically includes CSRF verification (csrf from jcc-express-mvc/Core). See CSRF-protection.md in this folder. API-style JSON clients often use a separate route file (often under /api, where CSRF may be skipped by default). Align with your app/Http/kernel (or equivalent) configuration.
“View” routes
If a route only renders a server-side view, you can use Route.view(uri, viewName, data?), which is implemented as a GET that calls res.render. See Views.md for the Blade engine, res.render / view(), paths, and res.locals.
Redirects
There is no Route::redirect helper. Use a closure (or controller) and res.redirect, or return an Inertia redirect from a controller when using Inertia.
Route groups: middleware, prefix, controller
For how global vs route middleware work and how to register aliases, see Middleware.md in this folder.
Middleware — Attach kernel-registered aliases (see your HTTP kernel’s middlewareAliases) or raw Express handlers.
Groups — Share attributes across several routes.
Controller groups — Set a default controller, then pass method names as strings (same idea as Laravel’s Route::controller grouping).
Nested groups merge middleware stacks; prefixes concatenate.
Resource routes
Route.resource(basePath, Controller, options?) registers a conventional REST map (index, store, create, show, edit, update, destroy) using {id} segments in the generated paths. Use only or except in the third argument to trim actions.
Listing routes
Inspect registered routes from the CLI:
Route model binding
When a controller method is marked with @Method() and a parameter is typed as a model class whose prototype extends the framework Model, the container attempts to load a record from the request’s route parameters instead of instantiating an empty model.
Implicit binding (by primary key)
The route segment name must match the camelCase name derived from the model:
- Lucid / typical class
User→ expectreq.params.user - Mongoose uses the schema model name, camel-cased
Resolution depends on the active stack:
- Mongoose —
findById - Sequelize —
findByPk - Lucid / Jcc-eloquent —
find(id)
Ensure your route parameter name aligns with the camelCase model name (e.g. BlogPost → blogPost).
Binding by a specific column
To resolve by another column (slug, email, uuid, etc.), use a parameter whose name ends with $ + the same camelCase model suffix. The part before $ is the column name; the part after $ identifies which model class to hydrate.
Under the hood this becomes a where(column, value).first()-style lookup (or ORM-specific findOne / findOne({ ... }) for Mongoose/Sequelize).
What this is not (yet)
There is no Laravel-style Route::model() or Route::bind() registry in the router facade, no implicit scoped parent/child binding chain like ->scopeBindings(), and no built-in enum segment binding. Nested resources should use explicit column suffixes and your own query constraints in the controller or a policy if you need parent scoping.
Rate limiting
Throttle middleware (for example aliases wired to express-rate-limit in your kernel) can be applied per route or per group, similar in spirit to Laravel’s throttle middleware. Define the alias and attach it with Route.middleware([...]).
CORS
CORS is handled by middleware and configuration at the application level, not by a dedicated Route API. Adjust your global middleware and any CORS package options to match your API consumers.
Route caching
JCC Express MVC does not implement route caching. All routes are registered at runtime from your route modules. There is no route:cache / route:clear equivalent; restarting the Node (or Bun) process reloads routes.
