Pages
Contents
@xpulse/controller β Component Spec
Status: CONCEPT Β· Created March 2026 Parent:
GLOBAL_concept-ecosystem.md,GLOBAL_concept-web.md
Overview
Controller discovery and base class for the xPulse MVC stack.
One responsibility: find controllers, derive routes, call views β nothing more.
Discovers src/controllers/**/*.js, derives routes from file path and
method name, registers them with @xpulse/router, and provides the
Controller base class.
Dependencies
| @xpulse/controller |
| βββ @xpulse/router β register routes |
| βββ @xpulse/template β this.render() delegates here |
| βββ @xpulse/event β listen to template:render:before |
API
Initialising
| import controller from '@xpulse/controller'; |
| await controller.init(); |
Recursively discovers src/controllers/**/*.js, derives routes and
registers them with @xpulse/router. Called by @xpulse/app.
Filesystem Routing
The file path determines the base route of the controller. The method name determines the sub-route.
Rules
index()method β no URL suffix (root of the controller route)- Other methods β method name becomes the URL suffix
define()β optional override for individual method routes- Subdirectories β become part of the route
Mapping Examples
| src/controllers/ |
| index.js β IndexController |
| index() β GET / |
| info.js β InfoController |
| index() β GET /info |
| team() β GET /info/team |
| bla() β GET /info/bla |
| tool/ |
| chat.js β ChatController |
| index() β GET /tool/chat |
| guide() β GET /tool/chat/guide |
| contact.js β ContactController |
| index() β GET /contact |
| submit() β POST /contact |
Response Object
All controller methods return a Response Object β
@xpulse/http is the only one that actually sends it.
The controller describes the response, it never sends it directly.
| // Internally β all helpers return this format: |
| { |
| status: 200, |
| headers: { 'Content-Type': 'text/html' }, |
| body: '<html>...', |
| } |
Controller Base Class
| import { Controller } from '@xpulse/controller'; |
| // File: src/controllers/info.js |
| // Base route: /info |
| export default class InfoController extends Controller { |
| define() { |
| this.overrideRoute('team', '/info/ueber-uns'); |
| this.overrideMethod('submit', 'POST'); |
| } |
| // β GET /info |
| async index() { |
| return this.render('info/index', { |
| title: 'Info', |
| }); |
| } |
| // β GET /info/bla |
| async bla() { |
| return this.render('info/bla', { |
| title: 'Bla', |
| items: ['a', 'b', 'c'], |
| }); |
| } |
| // β GET /info/ueber-uns (via overrideRoute) |
| async team() { |
| return this.render('info/team', { |
| title: 'Γber uns', |
| }); |
| } |
| } |
POST + JSON Example
| // File: src/controllers/contact.js |
| export default class ContactController extends Controller { |
| define() { |
| this.overrideMethod('submit', 'POST'); |
| } |
| // β GET /contact |
| async index() { |
| return this.render('contact/index', { |
| title: 'Kontakt', |
| }); |
| } |
| // β POST /contact |
| async submit() { |
| const { name, message } = this.req.body; |
| if (!name || !message) { |
| return this.render('contact/index', { |
| title: 'Kontakt', |
| error: 'Bitte alle Felder ausfΓΌllen.', |
| }); |
| } |
| return this.redirect('/contact/danke'); |
| } |
| // β GET /contact/api β JSON response without template |
| async api() { |
| return this.json({ status: 'ok' }); |
| } |
| } |
Controller Base Class API
Response helpers β always return a Response Object:
| Method | Description |
|---|---|
this.render(view, data) |
Render template via @xpulse/template β Response Object |
this.response(html, options?) |
Custom HTML β Response Object, no template |
this.json(data, options?) |
JSON β Response Object, no template |
this.redirect(url) |
Redirect β Response Object (301) |
Route overrides β only inside define():
| Method | Description |
|---|---|
this.overrideRoute(method, path) |
Override the route for a method |
this.overrideMethod(method, httpMethod) |
Override the HTTP method for a handler |
Request access:
| Property | Description |
|---|---|
this.req.path |
Current URL path β /info/bla |
this.req.params |
URL parameters β { page: 'bla' } |
this.req.query |
Query string β { lang: 'de' } |
this.req.method |
HTTP method β GET, POST |
this.req.body |
Request body β parsed for POST requests |
this.req.headers |
Request headers |
Automatic Route Info in Template Data
@xpulse/controller listens to template:render:before and injects
_route into the template data β without manual effort in every view method,
and without @xpulse/template needing to know about the controller.
| // Internally in @xpulse/controller β once during init: |
| event.on('template:render:before', ({ view, data }) => { |
| data._route = { |
| path: this.req.path, |
| params: this.req.params, |
| query: this.req.query, |
| method: this.req.method, |
| }; |
| }); |
Available in the template at runtime β useful for debugging and for active navigation states:
| <!-- Mark the active nav link --> |
| <a href="/info" class="{{ _route.path === '/info' ? 'active' : '' }}">Info</a> |
| <!-- Debug info --> |
| <p>Route: {{ _route.path }}</p> |
The _ prefix signals: system value, not user data.
Emitted Events
| Event | Payload | When |
|---|---|---|
controller:init |
{ root } |
Discovery begins |
controller:discovered |
{ name, routes } |
Controller found |
controller:ready |
{ count, routes } |
All registered |
controller:called |
{ traceId, controller, action, method, path, name } |
Handler called |
controller:navigation:collected |
{ items } |
Navigation items collected |
controller:navigation:ready |
{ items } |
Navigation ready |
Interaction in the Request Flow
| @xpulse/http receives GET /info/bla |
| β |
| @xpulse/router matched β InfoController@bla |
| β |
| @xpulse/controller calls InfoController.bla() |
| β |
| InfoController this.render('info/bla', { title: 'Bla' }) |
| β |
| @xpulse/template renders HTML |
| β |
| controller returns Response Object { status: 200, body: html } |
| β |
| @xpulse/http sends response |
Debug Integration
When @xpulse/debug is installed as a devDependency, @xpulse/controller
automatically provides a DataCollector. @xpulse/debug discovers it via
node_modules/@xpulse/controller/src/datacollectors/ β no configuration needed.
| "optionalDependencies": { |
| "@xpulse/debug": "^1.0.0" |
| } |
The ControllerCollector (name: 'controller', icon: 'βοΈ') shows in the Web Profiler:
- This Request (kv): Controller, Action, Route, Dispatch Time
- All Controllers (table): all discovered controller routes
Dispatch Time measures the time from http:request to controller:called β
this includes routing and dispatch, not the pure handler execution time.
This is indicated in the panel as (request start β controller called).
The ControllerCollector also writes a Waterfall Span (kind: 'controller')
for the dispatch time.
Badge in the toolbar: InfoController@index Β· 3ms
Package Structure
| @xpulse/controller/ |
| src/ |
| index.js β default export: controller object |
| discovery.js β recursive loading from src/controllers/ |
| resolver.js β derive route from filesystem path |
| base.js β Controller base class |
| test/ |
| discovery.test.js |
| resolver.test.js |
| base.test.js |
| docs/ |
| index.md |
| api.md |
| _meta.json |
| Dockerfile |
| Makefile |
| README.md |
| package.json |
| xpulse.json |