Attribute-Based Routing (RTTI)
Kittox features an attribute-based routing system inspired by MARS / WiRL, the popular Delphi REST libraries. Handler classes are decorated with custom Delphi attributes; the framework discovers them via RTTI at startup and dispatches HTTP requests to the matching method automatically.
This architecture enables:
- Extensibility — applications register custom URL handlers without modifying the framework
- Modularity — each feature (chart, calendar, map) lives in its own unit with zero coupling to the core
- Enterprise separation — enterprise modules can be included or excluded by simply adding/removing a
usesreference
How it works
1. Declare a handler class
A handler is a plain Delphi class decorated with [TKXPath] on the class (base path) and on each public method (sub-path). HTTP method attributes ([TKXGET], [TKXPOST]) specify which requests the method handles.
unit Kitto.Web.Handler.Chart;
{$RTTI EXPLICIT METHODS([vcPublic, vcPublished])
PROPERTIES([vcPublic, vcPublished])}
type
[TKXPath('/kx/view/{ViewName}')]
TKXChartHandler = class
public
[TKXPath('/chart-data')]
[TKXGET]
procedure HandleChartData(
[TKXPathParam('ViewName')] const AViewName: string;
[TKXContext] ADataView: TKDataView);
end;RTTI directive required
The {$RTTI EXPLICIT ...} directive is mandatory in every handler unit. Without it, Delphi does not emit attribute metadata for methods and parameters, and the routing engine cannot discover the handler.
2. Register the handler
Registration happens in the initialization section — the same pattern used by TKXControllerRegistry for UI controllers:
initialization
TKXResourceRegistry.Instance.RegisterResource(TKXChartHandler);
finalization
TKXResourceRegistry.Instance.UnregisterResource(TKXChartHandler);At registration time, TKXResourceRegistry scans the class via RTTI:
- Reads
[TKXPath]from the class → base path (/kx/view/{ViewName}) - For each public method with
[TKXPath]+[TKXGET/POST]→ creates aTKXMethodInfo - For each parameter → reads
[TKXPathParam],[TKXQueryParam],[TKXFormParam], or[TKXContext] - Sorts all registered handlers by specificity (more literal segments = higher priority)
3. Request dispatch
When a request arrives, the TKXActivation engine (created per-request):
- Matches the URL against all registered path templates — literal segments match exactly,
{Param}segments capture any value - Fills method parameters from the appropriate source:
[TKXPathParam('ViewName')]→ extracted from the URL path[TKXQueryParam('key')]→ from the query string (?key=...)[TKXFormParam('_op')]→ from the POST body[TKXContext]→ resolved by type from the injection registry
- Invokes the method via
TRttiMethod.Invoke - Cleans up — the handler instance is created and destroyed within the same request
Per-request context
The attribute router is evaluated before the legacy request handler in the route chain: a matched request is served by the handler, while an unmatched one falls through to the legacy routing — so the two coexist as built-in endpoints are progressively migrated to attribute routing.
Before invoking the method, the framework activates the same per-request context as the legacy pipeline (the authenticator, access controller and macro engine are made current), then runs the dispatch through the shared request filter chain described below. So a handler can rely on [TKXContext] TKWebSession, TKConfig and TKAuthenticator being fully populated — including the correct active database when the app uses multiple databases — exactly as a legacy handler would.
Request filters
Cross-cutting concerns — JWT session hydration, the authentication gate and error handling — are not coded inside each handler. They live in a filter chain that wraps the dispatch of both the attribute pipeline and the legacy request handler, so the behaviour is defined once and applied uniformly.
A filter implements IKXRequestFilter:
IKXRequestFilter = interface
procedure BeforeInvoke(const AContext: IKXRequestContext);
procedure AfterInvoke(const AContext: IKXRequestContext);
function OnException(const AContext: IKXRequestContext; E: Exception): Boolean;
end;Filters are registered globally with TKXFilterRegistry. Registration order = nesting: the first registered filter is the outermost layer (its BeforeInvoke runs first, its OnException runs last). A filter's BeforeInvoke can fully satisfy the request (e.g. write a 404) by setting AContext.Handled := True, which short-circuits the dispatch.
The framework registers four built-in filters, outermost first:
| Filter | Role |
|---|---|
TKXErrorHandlerFilter | Turns any exception escaping the dispatch into a non-fatal modal dialog (session stays alive) via TKWebApplication.RenderErrorDialog |
TKXJWTAuthFilter | For Auth: JWT, validates the token cookie and hydrates the session from the verified claims (user, roles, language, database/environment); no-op for non-JWT auth |
TKXNavigationGuardFilter | Bounces a top-level browser navigation to an HTML-fragment endpoint back to the app root (see Navigation guard) |
TKXAuthorizationFilter | Blocks a protected endpoint served to an unauthenticated session with a 404 (indistinguishable from not-found); raises on a lost session. Skipped for [TKXAnonymous] endpoints and for views declared public via ACName |
Because the gate is a filter reading TKWebSession.Current.IsAuthenticated, authentication is decoupled from the built-in login: an application can register its own filter that trusts an identity minted by a host web app (shared JWT or a proxy header) and populate the session as authenticated — see the embedded scenario.
Navigation guard
Every kx/… endpoint returns an HTML fragment meant to be loaded into the single-page app, not a standalone page. The SPA always sends the header X-KittoX: true on its requests (via the body hx-headers for HTMX and an explicit header on every fetch). A top-level browser navigation — pasting or typing a kx/… URL in the address bar, an opened link, a window.open — never sends it.
TKXNavigationGuardFilter uses that difference: when a request reaches a non-navigable endpoint without X-KittoX: true, it issues a 302 redirect to the application root (/), which renders the login page for an anonymous session or the home page for an authenticated one. So a bare fragment is never served to a manual navigation, and there is no difference between "logged in" (which used to leak the fragment) and "not logged in" (which used to 404). Running before the authorization filter, the redirect wins over the 404 the gate would otherwise produce.
An endpoint opts out of the guard with [TKXNavigable] — used by the blob/file endpoint (/blob/{Field}), because the client opens downloads via window.open and renders previews via <img> / <iframe> src, none of which can send a custom header. [TKXAnonymous] endpoints (login / logout / reset / change) and the home page are exempt as well. Using the X-KittoX marker (rather than the Sec-Fetch-* headers) keeps the check browser-agnostic.
Because the SPA marks every legitimate call, the guard is transparent to normal use: menu navigation, grids, forms, save, master-detail, charts and downloads all keep working unchanged.
Custom filters
Applications register their own filters (audit logging, rate limiting, tenant routing, upstream-identity trust) in initialization:
initialization
TKXFilterRegistry.Instance.RegisterFilter(TMyAuditFilter.Create);Attributes reference
| Attribute | Target | Description |
|---|---|---|
[TKXPath('/path/{Param}')] | Class, Method | URL path template with {Param} placeholders |
[TKXGET] | Method | Responds to HTTP GET |
[TKXPOST] | Method | Responds to HTTP POST |
[TKXANY] | Method | Responds to any HTTP method (GET or POST) |
[TKXAnonymous] | Method | Endpoint reachable without authentication — the authorization filter skips the auth gate for it (used by login / reset-password / change-password / logout) |
[TKXNavigable] | Method | Endpoint reachable by a top-level browser navigation (address bar / opened link / window.open) — opts out of the navigation guard. Used by the blob/file download endpoint |
[TKXPathParam('Name')] | Parameter | Extracts {Name} from the URL path |
[TKXQueryParam('Name')] | Parameter | Extracts ?Name=value from the query string |
[TKXFormParam('Name')] | Parameter | Extracts a single named field from the POST body (falls back to query string) |
[TKXFormBody] | Parameter | Binds the whole POST body to a TStrings parameter (all name=value pairs, by reference — do not free) |
[TKXContext] | Parameter | Dependency injection by type |
Dependency injection
Parameters decorated with [TKXContext] are resolved by their Delphi type from TKXInjectionRegistry. The framework pre-registers providers for common types:
| Parameter type | Injected value |
|---|---|
TKWebRequest | Current thread-local request |
TKWebResponse | Current thread-local response |
TKWebSession | Current thread-local session |
TKConfig | Application configuration singleton |
TKAuthenticator | Current authenticator |
TKDataView | Resolved from {ViewName} path parameter |
TKViewTable | MainTable of the resolved TKDataView |
Applications can register custom providers:
TKXInjectionRegistry.Instance.RegisterProvider(TypeInfo(TMyService),
function(const AType: TRttiType;
const AActivation: IKXActivationContext): TValue
begin
Result := TValue.From<TMyService>(TMyService.Create);
end);Dynamic script/CSS injection
Enterprise modules that require client-side libraries (e.g., Chart.js, EventCalendar) register them via TKXScriptRegistry in their initialization section:
// In Kitto.Html.ChartPanel.pas
initialization
TKXControllerRegistry.Instance.RegisterClass('ChartPanel',
TKXChartPanelController);
TKXScriptRegistry.Instance.RegisterScript('/js/chart.umd.min.js');// In Kitto.Html.CalendarPanel.pas
initialization
TKXControllerRegistry.Instance.RegisterClass('CalendarPanel',
TKXCalendarPanelController);
TKXScriptRegistry.Instance.RegisterStylesheet('/css/event-calendar.min.css');
TKXScriptRegistry.Instance.RegisterScript('/js/event-calendar.min.js');The page template (_Page.html) emits the registered tags via TemplatePro placeholders (dynamicStyles and dynamicScripts). If the enterprise module is not included in UseKitto.pas, no scripts are registered and no tags are emitted.
Custom application handlers
Applications can define their own URL handlers following the same pattern:
unit MyApp.Handler.Report;
{$RTTI EXPLICIT METHODS([vcPublic, vcPublished])
PROPERTIES([vcPublic, vcPublished])}
type
[TKXPath('/api/report/{ReportName}')]
TMyReportHandler = class
public
[TKXPath('/generate')]
[TKXPOST]
procedure Generate(
[TKXPathParam('ReportName')] const AName: string;
[TKXFormParam('format')] const AFormat: string;
[TKXContext] AConfig: TKConfig);
end;
implementation
uses Kitto.Web.Routing.Registry;
procedure TMyReportHandler.Generate(const AName, AFormat: string;
AConfig: TKConfig);
begin
// Custom report generation logic
end;
initialization
TKXResourceRegistry.Instance.RegisterResource(TMyReportHandler);
finalization
TKXResourceRegistry.Instance.UnregisterResource(TMyReportHandler);Include the unit in your application's UseKitto.pas or .dpr file — no framework modification required.
Overriding an endpoint (RegisterOverride)
The framework handlers are virtual, so an application can replace a built-in endpoint — or hook into it — by subclassing the handler and registering the subclass with RegisterOverride instead of RegisterResource:
unit MyApp.Handler.View;
{$RTTI EXPLICIT METHODS([vcPublic, vcPublished])
PROPERTIES([vcPublic, vcPublished])}
interface
uses
Kitto.Store, Kitto.Web.Handler.View;
type
TMyViewHandler = class(TKXViewHandlerBase)
protected
// Hook-only override: no routing attributes needed.
procedure OnBeforeSave(const ARecord: TKViewTableRecord;
const AIsInsert: Boolean); override;
end;
implementation
uses Kitto.Web.Routing.Registry;
procedure TMyViewHandler.OnBeforeSave(const ARecord: TKViewTableRecord;
const AIsInsert: Boolean);
begin
inherited;
// e.g. audit, extra validation, stamping fields …
end;
initialization
TKXResourceRegistry.Instance.RegisterOverride(TMyViewHandler);RegisterOverride(AClass) removes the framework resource(s) sharing AClass's base [TKXPath] and registers AClass in their place. The base path is inherited from the ancestor, and RTTI surfaces every inherited route, so:
- Hook-only overrides (
OnBeforeSave/OnAfterSave/OnBeforeDelete/OnAfterDelete, and the other virtual steps) need nothing extra — all endpoints keep working and the overridden hook is called via virtual dispatch on the per-request handler instance. - Replacing a whole endpoint requires re-declaring that method's routing attributes on the override, because Delphi RTTI does not inherit method attributes to overrides:
public
[TKXPath('/data')] [TKXPOST]
procedure HandleData(
[TKXPathParam('ViewName')] const AViewName: string); override;This lets an application customize persistence, rendering or access rules for its own views without forking the framework.
Module structure
The UseKitto.pas pattern controls which modules are active:
unit UseKitto;
interface
uses
Kitto.Html.All, // Core controllers (Apache 2.0)
Kitto.Web.Enterprise, // Enterprise modules (AGPL-3.0 / Commercial)
Kitto.Auth.DB; // Pluggable authenticator
implementation
end.Without Kitto.Web.Enterprise:
- No Chart.js, EventCalendar, or Google Maps JS loaded in the browser
- No enterprise data handlers registered
- No enterprise controller classes available
- Application compiles and runs with core features only
Architecture units
| Unit | Description |
|---|---|
Kitto.Web.Routing.Attributes | Attribute classes (TKXPath, TKXGET, etc.) |
Kitto.Web.Routing.Registry | TKXResourceRegistry — scans RTTI, caches method info; RegisterResource / RegisterOverride |
Kitto.Web.Routing.Injection | TKXInjectionRegistry — resolves [TKXContext] by type |
Kitto.Web.Routing.Activation | TKXActivation — URL matching, parameter filling, RTTI invoke |
Kitto.Web.Routing.Route | TKXRoutingRoute — bridge into the existing route chain |
Kitto.Web.Routing.Providers | Default injection providers (Request, Session, Config, etc.) |
Kitto.Web.Routing.Scripts | TKXScriptRegistry — dynamic JS/CSS injection |
Kitto.Web.Routing.Session | IKXSessionProvider — session transport abstraction (cookie/JWT) |
Kitto.Web.Routing.Filters | IKXRequestFilter, TKXFilterRegistry, TKXFilterChain — request filter middleware |
Kitto.Web.Routing.AppFilters | Built-in filters: JWT hydration, authorization gate, error handling |
Kitto.Web.Handler.Auth | TKXAuthHandlerBase — attribute-routed login / reset / change / logout with virtual hooks |
Kitto.Web.Handler.View | TKXViewHandlerBase — attribute-routed view domain: view / data / form / save / save-cache / delete / enter-edit / form-close / lookup / wizard-finish / detail data·save·delete / tool / upload / notify / blob, with virtual CRUD hooks |
See also
- Routing and Request Flow — complete URL routing reference
- Kittox Enterprise — enterprise feature matrix
- Licensing — Open Core licensing model
