Skip to content
Back to skills

Dmvcframework Webapp

ASecurity

Use when building a server-side web application with DelphiMVCFramework — HTML pages and HTMX fragments rendered by TemplatePro. Covers view engine setup, template inheritance and blocks, ViewData and its ownership rules, fragments, custom filters, cookie/JWT login, static files, and the HTMX request/response helpers on the Delphi side. Triggers on "TemplatePro", "RenderView", "ViewData", "baselayout", "template inheritance", "HTML fragment", "web controller", "HTMX", "hx-get", "server-side v...

  • 20 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 4, 2026
developmentgosqlreactexpressgitapibackendsecurityperformancedocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add danieleteti/delphi-ai-skills --skill dmvcframework-webapp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dmvcframework Webapp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dmvcframework Webapp
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danieleteti-dmvcframework-webapp/badge)](https://www.skillsdirectory.com/skills/danieleteti-dmvcframework-webapp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dmvcframework-webapp
description: Use when building a server-side web application with DelphiMVCFramework — HTML pages and HTMX fragments rendered by TemplatePro. Covers view engine setup, template inheritance and blocks, ViewData and its ownership rules, fragments, custom filters, cookie/JWT login, static files, and the HTMX request/response helpers on the Delphi side. Triggers on "TemplatePro", "RenderView", "ViewData", "baselayout", "template inheritance", "HTML fragment", "web controller", "HTMX", "hx-get", "server-side view", "web application", "cookie auth", "static files".
---

# DMVCFramework — Server-Side Views with TemplatePro + HTMX

Reference project: `webapplication_with_htmx` wizard template.
TemplatePro repo: https://github.com/danieleteti/templatepro
HTMX docs: https://htmx.org/docs/

**REQUIRED COMPANION SKILL:** `dmvcframework` — covers the REST/ActiveRecord/middleware
layer that underpins every web application. If the user asks about controllers, entities,
JWT middleware configuration, or engine setup beyond what is in this skill, invoke `dmvcframework`.

**COMPANION SKILL (on demand):** `htmx-skill` — indexes every page of the official htmx.org
documentation. For any attribute, trigger/swap modifier, event, header or extension beyond the few
patterns in Section 7, invoke it and read the linked page. Do not write HTMX from memory.

**COMPANION SKILL (on demand):** `dmvcframework-ui` — the presentation layer the wizard generates:
Bootstrap 5.3, `baselayout.html` blocks, `style.css` brand tokens, dark mode via `data-bs-theme`, toasts.
Invoke it before writing markup or CSS.

---

**REQUIRED REFERENCE — `dmvcframework-security`.** Any endpoint that accepts input from a client (body,
query string, header, cookie, upload, URL) must follow it: access control and IDOR, mass assignment,
SQL injection, XSS, CSRF, path traversal, uploads, security headers, JWT, secrets. Invoke it whenever you
write or review such an endpoint — not only when the user says "security".

---

## When in doubt about an API — verify it, never guess

Never invent an identifier or answer from memory. If you need a signature this skill does not cover, ask the
user for the path to their **DelphiMVCFramework checkout** and read `sources/` (and the matching `samples/`
project); failing that, read the official repository —
[sources](https://github.com/danieleteti/delphimvcframework/tree/master/sources) ·
[samples](https://github.com/danieleteti/delphimvcframework/tree/master/samples).
If you still cannot verify it, **say so**. Where a skill and a sample disagree, **the sample wins**.

---

## 1. STOP — this skill works inside a wizard-generated project

**Never create the project from scratch. Never hand-write the `.dpr` or the base templates.**
The wizard generates a running web application — layout, static files, session, view engine, HTMX — and your
job is to add pages to it.

### Step 1 — Detect the project in the current folder

The user is expected to have run the wizard and started the agent **from inside the project folder**. A web
wizard project has:

```
*.dpr                    Boot + RunServer  (Indy Direct by default; a WebModule here means WebBroker/ISAPI/Apache — also fine)
BootConfigU.pas          dotEnv, LoggerPro, TemplatePro init
EngineConfigU.pas        view engine, middleware/filters, UseExceptionHandler
Controllers.HomeU.pas    web controller — actions returning RenderView(...)
Controllers.APIU.pas     JSON sidecar (if present)
TemplateProHelpersU.pas  custom TemplatePro filters
ServicesU.pas            DI registrations
bin/.env                 port, secrets, view settings
bin/templates/           baselayout.html + pages
bin/www/                 static files (css/, favicon, manifest)
```

Read `EngineConfigU.pas`, the web controller and `bin/templates/baselayout.html` **before writing anything**.

### Step 2 — If there is no wizard project, stop and say so

Do not scaffold one. Tell the user:

> This skill works on a project generated by the DMVCFramework IDE wizard, and I do not see one in this
> folder. Please create it first:
>
> **Delphi IDE → File → New → Other → Delphi Projects → DelphiMVCFramework → New DMVCFramework Application**
>
> Pick **Web Application** (or **Minimal API WebApp** for lambda handlers). Accept the defaults — the server
> backend is **Indy Direct**. Compile and run it once and check the page opens in the browser. Then `cd` into
> the project folder, start me there, and tell me what page to add.

Then wait. Do not generate project files in the meantime.

**The host may be WebBroker (ISAPI, Apache) — that is fine.** Keep it, do not migrate it, do not suggest
migrating it. Everything above the host is identical on every backend. See `dmvcframework`,
`reference/servers.md`.

### Step 3 — Follow the scaffolding workflow for new pages

See Section 16 for the step-by-step workflow.

---

## 2. Architecture Overview

```
Browser
  │  HTML pages + HTMX attributes
  │
  ├─ GET /web/...         → TWebController   → RenderView('path/template')
  │                                            ViewData['key'] := value
  │
  ├─ GET /web/...         → TWebController   → same action, fragment template
  │    (HTMX request,                          IsHTMX and not HXIsBoosted → RenderView('x/table')
  │     not boosted)                           otherwise                  → RenderView('x/index')
  │
  └─ GET /api/...         → TAPIController   → OkResponse(StrDict(...))
       (JSON sidecar)
```

Key invariants:
- All controller actions are **functions**, never procedures.
- `[MVCProduces(TMVCMediaType.TEXT_HTML)]` is **optional** — the samples omit it and set the default
  content type once in the engine config (`Config[TMVCConfigKey.DefaultContentType] := TMVCMediaType.TEXT_HTML`)
  or in `OnBeforeAction`. Use it only when one action must differ from the app default.
- Full-page actions call `Result := RenderView('folder/template')`.
- **Fragment actions also call `RenderView`**: the same action renders `x/table` or `x/index` (§7). Do not
  hand-build HTML strings.
- `procedure` is allowed for redirects (`Redirect('/people')`), which produce no body.
- The JSON API sidecar (`/api`) uses standard `IMVCResponse` factory methods.

---

## 3. Engine Configuration (`EngineConfigU.pas`)

```delphi
procedure ConfigureEngine(AEngine: TMVCEngine);
var
  LJWTClaimsSetup: TJWTClaimsSetup;
  LWwwPath: string;
begin
  LWwwPath := TPath.Combine(AppPath, 'www');

  // Controllers
  AEngine.AddController(TWebController);
  AEngine.AddController(TAPIController);

  // Server-Side View Engine
  AEngine.SetViewEngine(TMVCTemplateProViewEngine);

  // JWT claims setup (what goes into every issued token)
  LJWTClaimsSetup := procedure(const JWT: TJWT)
    begin
      JWT.Claims.Issuer := dotEnv.Env('JWT_ISSUER', 'MyApp');
      JWT.Claims.ExpirationTime := Now + OneHour;
      JWT.Claims.NotBefore := Now - OneMinute * 5;
      JWT.Claims.IssuedAt := Now;
    end;

  // Middleware — order matters
  AEngine.AddMiddleware(TMVCRedirectMiddleware.Create(['/'], '/web'));
  AEngine.AddMiddleware(
    UseJWTCookieAuthentication(
      TAuthentication.Create,
      LJWTClaimsSetup,
      dotEnv.Env('JWT_SECRET'),  // no fallback: a missing key must fail, not sign with a public string
      '/login',   // POST credentials here
      '/logout',  // GET or POST to log out
      [TJWTCheckableClaim.ExpirationTime,
       TJWTCheckableClaim.NotBefore,
       TJWTCheckableClaim.IssuedAt],
      300         // Leeway seconds
    )
    .SetCookieSecure(dotEnv.Env('JWT_COOKIE_SECURE', True))
  );
  AEngine.AddMiddleware(
    UseFileSessionMiddleware(0, True, TPath.Combine(AppPath, 'sessions'),
      dotEnv.Env('SESSION_COOKIE_SECURE', True)));   // HttpOnly, Secure
  AEngine.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', LWwwPath));
  AEngine.AddMiddleware(TMVCCompressionMiddleware.Create);

  // Browser-friendly error pages: renders templates/error.html for clients that prefer HTML,
  // leaves the JSON error body untouched for API clients. One call — no handler to hand-write.
  AEngine.UseExceptionHandler('error', 'MyApp');
end;
```

`UseExceptionHandler(AErrorViewName = 'error'; AAppName = ''; AOptions = [])` renders
`<ViewPath>/error.<DefaultViewFileExtension>` and exposes `status`, `statustext`, `app_name`,
`dmvc_version`, `current_year` to the template. **`error` is empty unless you pass `ehShowDetails`** in
`AOptions` — which you want in development and almost certainly not in production, since it leaks the
exception message to the browser. Reach for `SetExceptionHandler(proc)` only when you need
behaviour this cannot express.

Required units for `EngineConfigU.pas`:
```delphi
uses
  TemplatePro,
  MVCFramework.View.Renderers.TemplatePro,
  MVCFramework.Middleware.JWT,
  MVCFramework.Middleware.Session,
  MVCFramework.Middleware.Redirect,
  MVCFramework.Middleware.StaticFiles,
  MVCFramework.Middleware.Compression,
  MVCFramework.JWT,
  System.IOUtils,
  System.DateUtils;
```

---

## 4. Web Controller Pattern

```delphi
unit Controllers.HomeU;

interface

uses MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons;

type
  [MVCPath('/web')]
  TWebController = class(TMVCController)
  protected
    procedure OnBeforeAction(Context: TWebContext;
      const AActionName: string; var Handled: Boolean); override;
  public
    [MVCPath]
    [MVCHTTPMethod([httpGET])]
    [MVCProduces(TMVCMediaType.TEXT_HTML)]
    function Index: String;

    [MVCPath('/about')]
    [MVCHTTPMethod([httpGET])]
    [MVCProduces(TMVCMediaType.TEXT_HTML)]
    function About: String;

    // Fragment endpoint — a partial TEMPLATE, rendered with RenderView (see section 9)
    [MVCPath('/fragment/clock')]
    [MVCHTTPMethod([httpGET])]
    function GetClockFragment: String;
  end;

implementation

uses System.SysUtils, System.DateUtils, MVCFramework.Logger;

procedure TWebController.OnBeforeAction(Context: TWebContext;
  const AActionName: string; var Handled: Boolean);
begin
  inherited;
  // Set ViewData available to ALL views via baselayout.html
  ViewData['app_name']     := 'MyApp';
  ViewData['dmvc_version'] := DMVCFRAMEWORK_VERSION;
  ViewData['current_year'] := YearOf(Now);
  ViewData['page_id']      := AActionName.ToLower; // for aria-current="page"
end;

function TWebController.Index: String;
begin
  ViewData['current_date'] := FormatDateTime('dddd, dd mmmm yyyy', Now);
  Result := RenderView('home/index');  // renders templates/home/index.html
end;

function TWebController.About: String;
begin
  Result := RenderView('about/index');
end;

function TWebController.GetClockFragment: String;
begin
  ViewData['time'] := FormatDateTime('hh:nn:ss', Now);
  ViewData['date'] := FormatDateTime('dddd, dd mmmm yyyy', Now);
  Result := RenderView('home/_clock');   // templates/home/_clock.html — no {{extends}}, so no chrome
end;
```

### Passing objects and lists to templates

```delphi
// Scalar values
ViewData['title']    := 'My Page';
ViewData['count']    := 42;

// Object — template accesses properties with dot notation
ViewData['person']   := lPerson;

// List — template iterates with {{for item in list}}
ViewData['people']   := lPeople;               // TObjectList<T>
```

**`ViewData` does NOT own what you put in it.** `TMVCViewDataObject` is a
`TObjectDictionary<string, TValue>` constructed with **no ownership flags**
(`MVCFramework.Commons.pas`) — nothing is freed for you. Free objects yourself, after the render:

```delphi
function TWebController.Customers: String;
var
  lCustomers: TObjectList<TCustomer>;
begin
  lCustomers := GetCustomers;
  try
    ViewData['customers'] := lCustomers;
    Result := RenderView('customers');   // render happens here, inside the try
  finally
    lCustomers.Free;
  end;
end;
```
A `TDataSet` (e.g. a `TFDMemTable`) can be passed as `ViewData` the same way, and iterated with `{{for}}`.

### Accessing session data and current user in a controller

```delphi
// After UseJWTCookieAuthentication, the session is validated automatically.
// Access claims from the JWT token:
var lUsername := Context.LoggedUser.UserName;
var lRoles    := Context.LoggedUser.Roles;  // TList<string>
var lCustom   := Context.LoggedUser.CustomData['key'];
```

### Form posts — validate and re-render with errors

A posted form is **not** validated automatically (only `[MVCFromBody]` is). The framework sample
`samples/webapp_htmx_forms` shows the pattern (`ReadPerson` in `PeopleSampleU.pas`):

1. Copy `Context.Request.ContentFields` (keys are **lowercase** field names) into an object whose
   **properties** carry validator attributes (`MVCFramework.Validators`).
2. `TMVCValidationEngine.Validate(lModel, lErrors)` (`MVCFramework.ValidationEngine`): `lErrors` is nil when
   valid, otherwise a `TDictionary<string, string>` you free, keyed by **property name** (`Email`).
3. Copy the messages into your error map with `LowerCase(Key)` — the forms library (`lib/forms_bootstrap5.tpro`)
   looks them up by the control's `name`.
4. Valid → save, then `RedirectResponse(...)` (Post/Redirect/Get). Invalid → `ViewData['formModel'] :=
   Context.Request.ContentFields` (shows what was typed), `ViewData['formErrors'] := lErrors`, render the same
   view with status 422 (`TMVCHTMLResponse` with `StatusCode := HTTP_STATUS.UnprocessableEntity`).

`MVCRequired` passes on `Integer`/`TDate` (0 is a value): parse those fields yourself and add a message when
the text does not convert. Full code (controller and Minimal API): the `dmvcframework` skill,
`reference/validation.md`, "HTML form post".

---

## 5. TemplatePro Template Syntax

### Output a variable

```html
{{:variable_name}}
{{:person.FirstName}}
{{:person.Address.City}}
```

### Template inheritance

Base layout (`baselayout.html`) defines named blocks:
```html
<!DOCTYPE html>
<html>
<head>
  <title>{{block "title"}}{{:app_name}}{{endblock}}</title>
</head>
<body>
  <main>{{block "body"}}{{endblock}}</main>
  {{block "scripts"}}{{endblock}}
</body>
</html>
```

Child template overrides blocks:
```html
{{extends "../baselayout.html"}}

{{block "title"}}My Page{{endblock}}

{{block "body"}}
  <h1>Hello from {{:app_name}}</h1>
{{endblock}}
```

- `{{extends "path"}}` — path is relative to the current template file.
- `error.html` extends with `{{extends "baselayout.html"}}` (same folder).
- Subfolder templates use `{{extends "../baselayout.html"}}`.

### Conditionals

```html
{{if page_id|eq,"index"}}aria-current="page"{{endif}}

{{if user_logged_in}}
  <p>Welcome, {{:username}}!</p>
{{else}}
  <p><a href="/login">Log in</a></p>
{{endif}}
```

Available comparison operators for `|eq`, `|ne`, `|gt`, `|lt`, `|ge`, `|le`:
```html
{{if count|gt,0}}...{{endif}}
{{if role|eq,"admin"}}...{{endif}}
```

### Loops

```html
{{for person in people}}
  <li>{{:person.FirstName}} {{:person.LastName}}</li>
{{endfor}}

{{# Loop index: it is a pseudo-property OF THE LOOP VARIABLE, and it is 1-based #}}
{{for item in items}}
  <tr>
    <td>{{:item.@@index}}</td>
    <td>{{:item.Name}}</td>
  </tr>
{{endfor}}

{{# ...so it composes with filters #}}
{{if item.@@index|lt,4}}<span class="badge">top</span>{{endif}}
```

### Filters (built-in)

```html
{{:name|uppercase}}
{{:name|lowercase}}
{{:name|capitalize}}
{{:name|rpad,10}}   {{:code|lpad,3,"0"}}
{{:date|datetostr}}   {{:ts|datetimetostr}}   {{:ts|formatdatetime,"dd/mm/yyyy"}}

{{# Filters DMVCFramework adds on top of TemplatePro (MVCFramework.View.Renderers.TemplatePro) #}}
{{:obj|json}}   {{:s|urlencode}}   {{:list|count}}   {{:name|fromquery}}

{{# Comparison filters work on dates too #}}
{{if cust.dob|ge,"2000/01/01"}}...{{endif}}

{{# Output is HTML-escaped by default. Suffix $ to emit raw HTML #}}
{{:myobj.rawhtml$}}

{{# Negation #}}
{{if !person}}<p>Not found</p>{{endif}}
{{:value|default,"N/A"}}
```

### Custom filters (registered in `TemplateProHelpersU.pas`)

```html
{{:value|MyHelper1}}
{{:value|MyHelper2}}
```

### Comments

```html
{{# This is a comment — not rendered in HTML output #}}
```

### Include (partial template)

```html
{{include "partials/card.html"}}
```

---

## 6. Template File Structure

```
bin/templates/
  baselayout.html           ← base layout; all pages extend this
  error.html                ← browser error page (extends baselayout.html)
  home/
    index.html              ← extends "../baselayout.html"
  about/
    index.html              ← extends "../baselayout.html"
  myfeature/
    index.html              ← extends "../baselayout.html"
    _card.html              ← reusable partial (prefix _ by convention)
```

The view path is configured in `.env`:
```ini
dmvc.view_path=templates
dmvc.default.view_file_extension=html
dmvc.view_cache=false   # set to true in production for performance
```

`RenderView('home/index')` resolves to `templates/home/index.html`.

---

## 7. HTMX — the patterns this stack actually uses

HTMX is loaded in `baselayout.html`; child templates need no setup.

**For any attribute, trigger modifier, swap mode, event or extension: invoke the `htmx-skill` and read
the linked htmx.org page.** It indexes every page of the official docs. Do not write HTMX attributes
from memory — inheritance rules and swap/trigger modifiers are easy to get subtly wrong. What follows is
only the handful of patterns that carry a *Delphi-side* implication.

### The one pattern that shapes your controller: page or fragment, one action

The same action serves the full page and the HTMX fragment — the shape of the People table in
`samples/webapp_htmx_forms` (`Controllers.PeoplePagesU`, `people/index.html` + `people/table.html`).
Below with customers. Two templates: `customers/index.html` is the page (it `{{extends
"../baselayout.html"}}` and `{{include "table.html"}}`), `customers/table.html` is the part that changes and
has no `{{extends}}`. You do **not** write a separate fragment endpoint, and you do **not** build HTML strings.

```delphi
function TWebController.Customers: String;
var
  lCustomers: TObjectList<TCustomer>;
begin
  lCustomers := FCustomerService.GetAll;
  try
    ViewData['customers'] := lCustomers;
    // HTMX asks for the table only; a normal request gets the whole page
    if Context.Request.IsHTMX and not Context.Request.HXIsBoosted
       and not Context.Request.HXIsHistoryRestoreRequest then   // uses MVCFramework.HTMX
      Result := RenderView('customers/table')
    else
      Result := RenderView('customers/index');
  finally
    lCustomers.Free;                 // ViewData owns nothing
  end;
end;
```

A plain browser navigation gets the whole document; an `hx-get` gets just the table. **Test `HXIsBoosted`
too:** a link under `hx-boost` is an HTMX request that expects the whole page — checking `IsHTMX` alone
serves it a fragment without the layout. **And `HXIsHistoryRestoreRequest`:** on a history-cache miss (Back
after the cache is full) htmx re-requests the URL with `HX-Request: true` and swaps the answer into `body`, so
it needs the whole page too. Because the same URL answers with a page or a fragment, also send
`Vary: HX-Request`.

### Triggering the request

```html
<!-- click -->
<button hx-get="/web/customers" hx-target="#list" hx-swap="innerHTML">Load</button>

<!-- on load / polling -->
<div hx-get="/web/stats" hx-trigger="load"></div>
<div hx-get="/web/stats" hx-trigger="every 5s"></div>

<!-- form post: response replaces the form -->
<form hx-post="/web/customers" hx-target="#result" hx-swap="outerHTML">…</form>

<!-- destructive action -->
<button hx-delete="/web/customers/1" hx-confirm="Delete this customer?" hx-target="closest tr"
        hx-swap="outerHTML">Delete</button>
```

`hx-put`/`hx-patch`/`hx-delete` reach the matching `[MVCHTTPMethod]` action directly — no method-override
hidden field, unlike a plain HTML form.

### URL in the address bar

```html
<a hx-get="/web/customers" hx-target="#main" hx-push-url="true">Customers</a>
```
Without `hx-push-url`, the fragment loads but the URL does not change and the page is not bookmarkable.

### Loading indicator

`htmx-request` is added to the element for the duration of the request; style it in `style.css`:

```css
.htmx-request { opacity: 0.5; transition: opacity 0.3s; }
```

### Live updates: SSE, not polling

For server-pushed updates use Server-Sent Events. Do not hand-roll the stream in the action — inherit
`TMVCSSEController` (see the `dmvcframework` skill, `reference/sse.md`) and consume it with the htmx SSE
extension (`https://htmx.org/extensions/sse/`).


## 8. HTMX response helpers (from the Delphi side)

`uses MVCFramework.HTMX;` — it adds typed helpers to `Context.Request` / `Context.Response`.
**Use them; do not hand-write `HX-*` header strings** (a typo in a header name fails silently).

```delphi
// full page reload, like window.location — e.g. after login
Context.Response.HXSetRedirect('/web/dashboard');

// HTMX-aware navigation: no full reload, content swapped, history updated
Context.Response.HXSetLocation('/web/products');

// URL bar only
Context.Response.HXSetPushUrl('/web/items/' + ItemID);
Context.Response.HXSetReplaceUrl('/web/items/' + ItemID);

// client-side events (see the dmvcframework-ui skill for showToast — it takes HTML)
Context.Response.HXTriggerClientEvent('refreshCart');
Context.Response.HXTriggerClientEvents(['refreshCart', 'updateBadge']);

// With a payload. It is serialized as a JSON *value*: a string arrives as evt.detail.value.
// Pass an object when you want named properties (evt.detail.FirstName, ...).
Context.Response.HXTriggerClientEvent('showMessage', 'Customer saved');
Context.Response.HXTriggerClientEvent('customerSaved', lCustomer);
// when it fires: etReceived (default), or after swap / after settle — pass the TClientEventType
Context.Response.HXTriggerClientEvent('initDatePicker', etSettled);

// override the swap / target the server chose on the client
Context.Response.HXSetReswap(TSwapOption.soOuterHTML);
Context.Response.HXSetRetarget('#other-element');

// tell the client to refresh the page
Context.Response.HXSetPageRefresh;
```

On the request side: `Context.Request.IsHTMX`, `HXIsBoosted`, `HXGetTarget`, `HXGetPrompt`,
`HXGetTriggeringEventAsJSON`.

To do nothing (leave the DOM as it is), answer 204:

```delphi
Context.Response.StatusCode := HTTP_STATUS.NoContent;
Result := '';
```

---

## 9. Fragments — a template, never a string

A fragment is a **template**, rendered by the same view engine as a full page. Never concatenate HTML in
Delphi, and never call `TTProCompiler.CompileAndRender` yourself — that bypasses the engine, its cache and
its filters, and it forces you to hand-escape every value.

Two ways to serve one, both `RenderView`:

**a) Same action, page or fragment.** Preferred: one URL, bookmarkable, works without JS.

```delphi
function TWebController.Items: String;
var
  lItems: TObjectList<TItem>;
begin
  lItems := FItemService.GetAll;
  try
    ViewData['items'] := lItems;
    if Context.Request.IsHTMX and not Context.Request.HXIsBoosted then
      Result := RenderView('items/table')
    else
      Result := RenderView('items/index');
  finally
    lItems.Free;                     // ViewData owns nothing
  end;
end;
```

**b) A dedicated fragment action**, when the fragment has no page of its own (a row after an edit, a
search-results list). Point it at a partial template and skip the layout:

```delphi
[MVCPath('/fragment/item-list')]
[MVCHTTPMethod([httpGET])]
function TWebController.GetItemList: String;
var
  lItems: TObjectList<TItem>;
begin
  lItems := FItemService.GetAll;
  try
    ViewData['items'] := lItems;
    Result := RenderView('items/_list');   // a template with no {{extends}} → no chrome
  finally
    lItems.Free;
  end;
end;
```

```html
{{# templates/items/_list.html — the fragment, and reusable via {{include}} from the page #}}
<ul id="item-list">
{{for item in items}}
  <li id="item-{{:item.ID}}">
    {{:item.Name}}
    <a hx-delete="/web/items/{{:item.ID}}" hx-target="#item-{{:item.ID}}"
       hx-swap="outerHTML" hx-confirm="Delete?">[x]</a>
  </li>
{{endfor}}
</ul>
```

TemplatePro escapes `{{:item.Name}}` for you — there is no `HTMLEscape` call to forget. The same partial
can be `{{include}}`d by the full page, so page and fragment never drift apart.

---


## 10. Cookie JWT Authentication

### `AuthenticationU.pas` — the three handler methods

```delphi
type
  TAuthentication = class(TInterfacedObject, IMVCAuthenticationHandler)
  protected
    procedure OnRequest(
      const AContext: TWebContext;
      const ControllerQualifiedClassName, ActionName: string;
      var AuthenticationRequired: Boolean);

    procedure OnAuthentication(
      const AContext: TWebContext;
      const UserName, Password: string;
      UserRoles: TList<string>;
      var IsValid: Boolean;
      const SessionData: TSessionData);

    procedure OnAuthorization(
      const AContext: TWebContext;
      UserRoles: TList<string>;
      const ControllerQualifiedClassName, ActionName: string;
      var IsAuthorized: Boolean);
  end;
```

#### `OnRequest` — which routes need auth

```delphi
procedure TAuthentication.OnRequest(
  const AContext: TWebContext;
  const ControllerQualifiedClassName, ActionName: string;
  var AuthenticationRequired: Boolean);
begin
  // Protect everything under TAdminController
  AuthenticationRequired :=
    ControllerQualifiedClassName.Contains('TAdminController');
end;
```

#### `OnAuthentication` — validate credentials

```delphi
procedure TAuthentication.OnAuthentication(
  const AContext: TWebContext;
  const UserName, Password: string;
  UserRoles: TList<string>;
  var IsValid: Boolean;
  const SessionData: TSessionData);
begin
  // validate the credentials against your user store
  IsValid := ValidateFromDB(UserName, Password);
  if IsValid then
  begin
    UserRoles.Add('user');
    if IsAdmin(UserName) then
      UserRoles.Add('admin');
    SessionData.AddOrSetValue('username', UserName);
  end;
end;
```

#### `OnAuthorization` — check roles per action

```delphi
procedure TAuthentication.OnAuthorization(
  const AContext: TWebContext;
  UserRoles: TList<string>;
  const ControllerQualifiedClassName, ActionName: string;
  var IsAuthorized: Boolean);
begin
  if ActionName.StartsWith('Admin') then
    IsAuthorized := UserRoles.Contains('admin')
  else
    IsAuthorized := True;
end;
```

### Login form pattern (browser-facing)

```html
<form hx-post="/login" hx-swap="none"
      hx-on::after-request="if (event.detail.successful) window.location.href = '/web'">
  <input name="jwtusername" type="text" required>
  <input name="jwtpassword" type="password" required>
  <button type="submit">Login</button>
</form>
```

The `/login` route is handled by `UseJWTCookieAuthentication`, which a plain HTML form does not fit:

- **Field names.** Form data is read only as `jwtusername` / `jwtpassword`
  (`TMVCJWTDefaults.USERNAME_HEADER` / `PASSWORD_HEADER`); `username` / `password` are accepted only in a
  JSON body. A form posting `username` gets **401**.
- **No redirect.** On success it sets the HTTP-only cookie and answers `200` with JSON `{"token": "..."}`.
  The page must navigate by itself — hence `hx-swap="none"` and the `after-request` handler above. A plain
  `<form method="POST">` would leave the user looking at the JSON. A failed login answers 401.

### Reading the current user in a controller

```delphi
function TWebController.Profile: String;
begin
  ViewData['username'] := Context.LoggedUser.UserName;
  ViewData['is_admin'] := Context.LoggedUser.Roles.Contains('admin');
  Result := RenderView('profile/index');
end;
```

---

## 11. Custom TemplatePro Filters (`TemplateProHelpersU.pas`)

### Registering filters

The wizard already generates `TemplateProContextConfigure` in `TemplateProHelpersU.pas` and calls it once in
`Boot` (`BootConfigU.pas`). **Add your `AddFilter` calls inside its existing handler — never assign
`TTProConfiguration.OnContextConfiguration` a second time.** The generated handler also sets
`CompiledTemplate.IncludeRootPath`, which keeps a dynamic `{{include}}` built from request data inside the
views folder; a replacement handler silently drops that protection.

```delphi
  TTProConfiguration.OnContextConfiguration := procedure(const CompiledTemplate: ITProCompiledTemplate)
  begin
    CompiledTemplate.IncludeRootPath := lViewPath;   // generated - keep it
    CompiledTemplate.AddFilter('currency', CurrencyFilter);
    CompiledTemplate.AddFilter('timeago', TimeAgoFilter);
    // ... rest of the generated handler
  end;
```

### Writing a filter

```delphi
uses System.Rtti, TemplatePro, TemplatePro.Types;

function CurrencyFilter(const Value: TValue;
  const Parameters: TArray<TFilterParameter>): TValue;
begin
  Result := FormatCurr('€#,##0.00', Value.AsExtended);
end;

function TimeAgoFilter(const Value: TValue;
  const Parameters: TArray<TFilterParameter>): TValue;
var
  lDelta: TTimeSpan;
begin
  lDelta := TTimeSpan.Subtract(Now, Value.AsExtended);
  if lDelta.TotalMinutes < 1 then
    Result := 'just now'
  else if lDelta.TotalHours < 1 then
    Result := Format('%d minutes ago', [Round(lDelta.TotalMinutes)])
  else
    Result := Format('%d hours ago', [Round(lDelta.TotalHours)]);
end;
```

Template usage:
```html
{{:price|currency}}
{{:created_at|timeago}}
```

### Dynamic external data source (`OnGetValue`)

```delphi
CompiledTemplate.OnGetValue :=
  procedure(const DataSource, Members: string;
    var Value: TValue; var Handled: Boolean)
  begin
    if SameText(DataSource, 'config') then
    begin
      Value := GetConfigValue(Members);  // e.g. 'config.siteName'
      Handled := True;
    end;
  end;
```

Template: `{{:config.siteName}}`

---

## 12. DI / Services Pattern (`ServicesU.pas`)

### Registering services

```delphi
procedure RegisterServices(Container: IMVCServiceContainer);
begin
  Container.RegisterType(
    TPeopleService, IPeopleService, TRegistrationType.SingletonPerRequest);
  Container.RegisterType(
    TProductService, IProductService, TRegistrationType.Singleton);
end;
```

Registration types:
- `SingletonPerRequest` — one instance per HTTP request (most common for web)
- `Singleton` — one instance for the lifetime of the process
- `Transient` — new instance on every `Resolve` call

### Resolving in a controller

```delphi
function TWebController.Products: String;
var
  lService: IProductService;
  lProducts: TObjectList<TProduct>;
begin
  lService := Context.ServiceContainerResolver.Resolve(TypeInfo(IProductService)) as IProductService;
  lProducts := lService.GetAll;
  try
    ViewData['products'] := lProducts;
    Result := RenderView('products/index');
  finally
    lProducts.Free;                  // ViewData owns nothing
  end;
end;
```

### Registering — once, in the `.dpr`

`TWebContext` has **no** `ServiceContainer` property (only the read-only `ServiceContainerResolver`).
Registration happens once, before the server starts, and `Build` is mandatory:

```delphi
RegisterServices(DefaultMVCServiceContainer);
DefaultMVCServiceContainer.Build;
```

Prefer injecting the service rather than resolving it by hand — `[MVCInject]` on the controller's
constructor or on an action parameter. See the `dmvcframework` skill, `reference/di-and-repository.md`.

---

## 13. API JSON Sidecar (`Controllers.APIU.pas`)

The web application includes a JSON API controller alongside the web controller.

```delphi
[MVCPath('/api')]
TAPIController = class(TMVCController)
public
  [MVCPath('/server/info')]
  [MVCHTTPMethod([httpGET])]
  function GetServerInfo: IMVCResponse;
end;

function TAPIController.GetServerInfo: IMVCResponse;
begin
  Result := OkResponse(
    StrDict(
      ['application', 'serverTime', 'dmvcVersion'],
      ['MyApp', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Now), DMVCFRAMEWORK_VERSION]
    )
  );
end;
```

`StrDict(keys, values)` returns a **`TMVCStringDictionary`** (string→string), serialized as a flat JSON
object. For a dictionary of *objects*, use `ObjectDict()` — that is the one returning `IMVCObjectDictionary`.

---

## 14. Static Files

Static files are served from `bin/www/` at the `/static` URL prefix.

```
bin/www/
  css/style.css          → /static/css/style.css
  favicon.ico            → /static/favicon.ico
  favicon-32x32.png      → /static/favicon-32x32.png
  site.webmanifest       → /static/site.webmanifest
```

Middleware declaration:
```delphi
AEngine.AddMiddleware(
  TMVCStaticFilesMiddleware.Create('/static', TPath.Combine(AppPath, 'www')));
```

In templates: `<link rel="stylesheet" href="/static/css/style.css">`

---

## 15. Environment Configuration (`bin/.env`)

Full reference — profiles, precedence, `${}`/`$[]` syntax, `RequireKeys`, the `Boot` order:
`dmvcframework` skill, `reference/dotenv.md`.

Key settings for the webapp template:

```ini
# Server
dmvc.server.port=8080

# View engine
dmvc.view_path=templates
dmvc.default.view_file_extension=html
dmvc.view_cache=false         # true in production (caches compiled templates)

# JWT Cookie Auth - the wizard writes a random 384-bit key here; give every environment its own
# (e.g. openssl rand -hex 48) and keep .env out of version control
JWT_SECRET=<generated>
JWT_ISSUER=MyApp
# Set to false only for local development over plain HTTP.
JWT_COOKIE_SECURE=true
SESSION_COOKIE_SECURE=true

# Logging
logger.config.file=loggerpro.json
```

Profile-specific overrides: `.env.test`, `.env.prod` (layered by `UseProfile`).

---

## 16. Scaffolding Workflow — Adding a New Page

### Step 1 — Add actions to the web controller

In `Controllers.HomeU.pas` (or a new controller unit):

```delphi
[MVCPath('/products')]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function Products: String;

[MVCPath('/products/fragment/list')]
[MVCHTTPMethod([httpGET])]
[MVCProduces(TMVCMediaType.TEXT_HTML)]
function GetProductListFragment: String;
```

```delphi
function TWebController.Products: String;
begin
  ViewData['page_id'] := 'products';
  Result := RenderView('products/index');
end;

function TWebController.GetProductListFragment: String;
var
  lProducts: TObjectList<TProduct>;
begin
  lProducts := fProductService.GetAll;
  try
    ViewData['products'] := lProducts;         // ViewData owns nothing — free it below
    Result := RenderView('products/_list');    // partial template, no {{extends}}
  finally
    lProducts.Free;
  end;
end;
```

### Step 2 — Create the template

Create `bin/templates/products/index.html`:

```html
{{extends "../baselayout.html"}}

{{block "title"}}Products{{endblock}}

{{block "body"}}
  <div class="hero">
    <h1>Products</h1>
  </div>

  <div hx-get="/web/products/fragment/list"
       hx-trigger="load"
       hx-swap="innerHTML">
    <p aria-busy="true">Loading products...</p>
  </div>
{{endblock}}
```

### Step 3 — Add nav link in `baselayout.html`

```html
<li>
  <a href="/web/products"
     {{if page_id|eq,"products"}}aria-current="page"{{endif}}>
    Products
  </a>
</li>
```

### Step 4 — Register new controller (if separate unit)

In `EngineConfigU.pas`:
```delphi
AEngine.AddController(TProductsController);
```

---

## 17. Common Pitfalls

| Pitfall | Fix |
|---------|-----|
| Action is `procedure` instead of `function` | All web actions must be `function`: return `String` for HTML, `IMVCResponse` for JSON. |
| Freeing nothing after `ViewData['x'] := lObj` | `ViewData` owns nothing. Free the object yourself in a `finally` after `RenderView`. |
| Hand-building HTML for HTMX fragments | Same action, two templates: `IsHTMX and not HXIsBoosted and not HXIsHistoryRestoreRequest` → `RenderView('x/table')`, otherwise `RenderView('x/index')`. |
| Writing `HX-*` headers by hand | `uses MVCFramework.HTMX` — `Response.HXSetRedirect/HXSetPushUrl/HXSetReswap/HXSetRetarget/HXTriggerClientEvent`, `Request.IsHTMX/HXGetTarget`. |
| `ViewData` not set before `RenderView` | `OnBeforeAction` sets shared data; page-specific data must be set in the action function body before `Result := RenderView(...)`. |
| Template extends wrong relative path | `home/index.html` → `{{extends "../baselayout.html"}}`. `error.html` (same folder as baselayout) → `{{extends "baselayout.html"}}`. |
| `JWT_COOKIE_SECURE=true` with HTTP | Cookie won't be sent by the browser over plain HTTP. Use `false` during local dev, `true` in production with HTTPS. |
| View cache enabled during development | `dmvc.view_cache=true` caches compiled templates — template changes won't appear until restart. Use `false` in dev. |
| `SetExceptionHandler` handles all requests | Check `WebContext.Request.ClientPreferHTML` first and `Exit` if false — otherwise API clients get an HTML error page. |
| Static file not found | File must be under `bin/www/`. The URL prefix is `/static`, not `/www`. Check middleware order — static files should be declared after redirect and auth. |
| Template variable silently empty | TemplatePro doesn't error on missing keys by default. Add a breakpoint after `ViewData[...]` assignments to verify they are set. |
| `{{for item in list}}` doesn't iterate | The list must be a `TObjectList<T>` or array registered in ViewData. Ensure the service returns a non-nil, non-empty list. |
| HTMX response headers ignored | HTMX checks for `HX-*` headers only on responses to HTMX requests (requests with `HX-Request: true` header). Verify the button/div has a `hx-*` trigger attribute. |
| `hx-swap-oob` elements not swapped | Each oob element must have an `id` that matches an existing DOM element. HTMX swaps by ID. |

---

## 18. Key Units Reference

| Unit | Purpose |
|------|---------|
| `MVCFramework.View.Renderers.TemplatePro` | `TMVCTemplateProViewEngine` |
| `TemplatePro` | `TTProCompiler.CompileAndRender`, `TTProConfiguration`, `ITProCompiledTemplate` |
| `TemplatePro.Types` | `TFilterParameter` |
| `MVCFramework.Middleware.JWT` | `UseJWTCookieAuthentication`, `UseJWTMiddleware` |
| `MVCFramework.Middleware.Session` | `UseFileSessionMiddleware` |
| `MVCFramework.Middleware.Redirect` | `TMVCRedirectMiddleware` |
| `MVCFramework.Middleware.StaticFiles` | `TMVCStaticFilesMiddleware` |
| `MVCFramework.Middleware.Compression` | `TMVCCompressionMiddleware` |
| `MVCFramework.JWT` | `TJWT`, `TJWTClaimsSetup`, `TJWTCheckableClaim` |
| `MVCFramework.Commons` | `TMVCMediaType`, `AppPath`, `DMVCFRAMEWORK_VERSION` |
| `MVCFramework.Container` | `IMVCServiceContainer`, `TRegistrationType` |
| `MVCFramework.DotEnv` | `dotEnv`, `dotEnvConfigure`, `NewDotEnv` |
| `LoggerPro.Config` | `TLoggerProConfig.BuilderFromJSONFile` |

---

## 19. Sample Reference Project

In the DelphiMVCFramework repository (https://github.com/danieleteti/delphimvcframework/tree/master/samples):

| Sample | What it shows |
|--------|---------------|
| `samples/htmx_website_with_templatepro/` | TemplatePro + HTMX website: baselayout with blocks, partials, custom filters. Its `ispage` flag predates the wizard's page/table pattern and ignores `hx-boost` — follow the wizard |
| `samples/serversideviews_templatepro/` | The view engine in depth: inheritance, filters, `RenderViews`, CSV/non-HTML views, dataset in `ViewData` |
| `samples/wizard_showcase/web/` | Minimal-API flavour of a web app: `.AsWeb`, sessions, `RenderView` from lambda handlers |

Or generate one with the IDE wizard (preset **Web Application**) — that is the layout this skill assumes.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.