Skip to content

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 uses reference

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.

pascal
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:

pascal
initialization
  TKXResourceRegistry.Instance.RegisterResource(TKXChartHandler);

finalization
  TKXResourceRegistry.Instance.UnregisterResource(TKXChartHandler);

At registration time, TKXResourceRegistry scans the class via RTTI:

  1. Reads [TKXPath] from the class → base path (/kx/view/{ViewName})
  2. For each public method with [TKXPath] + [TKXGET/POST] → creates a TKXMethodInfo
  3. For each parameter → reads [TKXPathParam], [TKXQueryParam], [TKXFormParam], or [TKXContext]
  4. Sorts all registered handlers by specificity (more literal segments = higher priority)

3. Request dispatch

When a request arrives, the TKXActivation engine (created per-request):

  1. Matches the URL against all registered path templates — literal segments match exactly, {Param} segments capture any value
  2. 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
  3. Invokes the method via TRttiMethod.Invoke
  4. 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:

pascal
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:

FilterRole
TKXErrorHandlerFilterTurns any exception escaping the dispatch into a non-fatal modal dialog (session stays alive) via TKWebApplication.RenderErrorDialog
TKXJWTAuthFilterFor 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
TKXNavigationGuardFilterBounces a top-level browser navigation to an HTML-fragment endpoint back to the app root (see Navigation guard)
TKXAuthorizationFilterBlocks 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.

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:

pascal
initialization
  TKXFilterRegistry.Instance.RegisterFilter(TMyAuditFilter.Create);

Attributes reference

AttributeTargetDescription
[TKXPath('/path/{Param}')]Class, MethodURL path template with {Param} placeholders
[TKXGET]MethodResponds to HTTP GET
[TKXPOST]MethodResponds to HTTP POST
[TKXANY]MethodResponds to any HTTP method (GET or POST)
[TKXAnonymous]MethodEndpoint reachable without authentication — the authorization filter skips the auth gate for it (used by login / reset-password / change-password / logout)
[TKXNavigable]MethodEndpoint 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')]ParameterExtracts {Name} from the URL path
[TKXQueryParam('Name')]ParameterExtracts ?Name=value from the query string
[TKXFormParam('Name')]ParameterExtracts a single named field from the POST body (falls back to query string)
[TKXFormBody]ParameterBinds the whole POST body to a TStrings parameter (all name=value pairs, by reference — do not free)
[TKXContext]ParameterDependency 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 typeInjected value
TKWebRequestCurrent thread-local request
TKWebResponseCurrent thread-local response
TKWebSessionCurrent thread-local session
TKConfigApplication configuration singleton
TKAuthenticatorCurrent authenticator
TKDataViewResolved from {ViewName} path parameter
TKViewTableMainTable of the resolved TKDataView

Applications can register custom providers:

pascal
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:

pascal
// In Kitto.Html.ChartPanel.pas
initialization
  TKXControllerRegistry.Instance.RegisterClass('ChartPanel',
    TKXChartPanelController);
  TKXScriptRegistry.Instance.RegisterScript('/js/chart.umd.min.js');
pascal
// 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:

pascal
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:

pascal
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:
pascal
  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:

pascal
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

UnitDescription
Kitto.Web.Routing.AttributesAttribute classes (TKXPath, TKXGET, etc.)
Kitto.Web.Routing.RegistryTKXResourceRegistry — scans RTTI, caches method info; RegisterResource / RegisterOverride
Kitto.Web.Routing.InjectionTKXInjectionRegistry — resolves [TKXContext] by type
Kitto.Web.Routing.ActivationTKXActivation — URL matching, parameter filling, RTTI invoke
Kitto.Web.Routing.RouteTKXRoutingRoute — bridge into the existing route chain
Kitto.Web.Routing.ProvidersDefault injection providers (Request, Session, Config, etc.)
Kitto.Web.Routing.ScriptsTKXScriptRegistry — dynamic JS/CSS injection
Kitto.Web.Routing.SessionIKXSessionProvider — session transport abstraction (cookie/JWT)
Kitto.Web.Routing.FiltersIKXRequestFilter, TKXFilterRegistry, TKXFilterChain — request filter middleware
Kitto.Web.Routing.AppFiltersBuilt-in filters: JWT hydration, authorization gate, error handling
Kitto.Web.Handler.AuthTKXAuthHandlerBase — attribute-routed login / reset / change / logout with virtual hooks
Kitto.Web.Handler.ViewTKXViewHandlerBase — 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

Released under Apache License, Version 2.0.