xPulse
πŸ‡¬πŸ‡§ EN

@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

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:

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
en/spec.md 2026-04-10