Skip to content
Back to skills

Dmvcframework

ASecurity

Use when writing any server-side feature with DelphiMVCFramework (DMVC) — controllers, routes, REST endpoints, entities, middleware, validation, dependency injection, JWT, SSE, or the server bootstrap. The core skill; the Minimal API, web-app, UI, security and testing skills build on it. Triggers on "DMVCFramework", "DelphiMVCFramework", "create a controller", "add a route", "REST endpoint in Delphi", "TMVCActiveRecord", "ActiveRecord entity", "add middleware", "configure JWT", "MVCRequired",...

  • 20 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added October 4, 2026
developmentrustgosqlreactexpresstestinggitapibackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned October 5, 2026

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

Installs into .claude/skills of the current project.

Are you the author of Dmvcframework?

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

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

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
description: Use when writing any server-side feature with DelphiMVCFramework (DMVC) — controllers, routes, REST endpoints, entities, middleware, validation, dependency injection, JWT, SSE, or the server bootstrap. The core skill; the Minimal API, web-app, UI, security and testing skills build on it. Triggers on "DMVCFramework", "DelphiMVCFramework", "create a controller", "add a route", "REST endpoint in Delphi", "TMVCActiveRecord", "ActiveRecord entity", "add middleware", "configure JWT", "MVCRequired", "add validation", "service container", "MVCInject".
---

# DMVCFramework Development Guide

DMVCFramework is a mature Delphi MVC/REST framework.
Official repo: https://github.com/danieleteti/delphimvcframework

---

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

These skills target **DelphiMVCFramework 3.5.x**. Before writing code, check the project's version
(`DMVCFRAMEWORK_VERSION` in the framework's `sources/dmvcframeworkbuildconsts.inc`, or the version the
project's `AGENTS.md` / `CLAUDE.md` states): if its major.minor differs, tell the user that these skills
describe a different release and that names may have changed. If you
need a signature they do not cover, or you suspect the framework has moved on, **do not invent a name and do
not answer from memory** — a plausible-but-wrong identifier costs the user a compile error and their trust.

Verify, in this order:

1. **Ask the user for the local sources.** They almost certainly have the framework on disk:

   > What is the path to your DelphiMVCFramework checkout? I want to confirm the exact signature in
   > `sources/` rather than guess. If you have the samples too, that helps.

   Then read the unit directly (`sources/MVCFramework*.pas`) and, for usage, the matching sample.

2. **Otherwise, read the official repository** — always current:
   - Sources: https://github.com/danieleteti/delphimvcframework/tree/master/sources
   - Samples: https://github.com/danieleteti/delphimvcframework/tree/master/samples

   Raw file, when you know the unit:
   `https://raw.githubusercontent.com/danieleteti/delphimvcframework/master/sources/<Unit>.pas`

3. **If you cannot verify it, say so.** "I am not certain of this signature — check `MVCFramework.X.pas`" is
   a useful answer. A confidently wrong one is not.

The samples are the best documentation of *usage*: they compile, they run, and they are maintained with the
framework. When a skill and a sample disagree, **the sample wins** — and the skill has a bug worth reporting.

**A signature is half the answer.** `sources/` tells you the arity and the types; a sample tells you the
ownership, who frees what, the order of the calls and the idiom — which is where generated code actually
breaks. Read both before writing a call you have not written before, and prefer the project's own existing
controllers as the third input: they carry the conventions this codebase already follows.

**Record the path instead of asking every session.** Once the user gives you the checkout path and you have
confirmed it exists, offer to write it into the instruction file this agent already reads (`CLAUDE.md` in the
project root, or `AGENTS.md` / `GEMINI.md`). Ask first, then keep it in a block of its own:

```markdown
<!-- delphi-local-sources -->
DelphiMVCFramework checkout: C:\DEV\dmvcframework   (sources/ + samples/)
Delphi RTL/VCL source: C:\Program Files (x86)\Embarcadero\Studio\23.0\source   (12 Athens, CompilerVersion 36.0)
<!-- /delphi-local-sources -->
```

Look for that block **before** asking. If a path in it no longer exists, say so and ask again rather than
falling back to memory. The `delphi` skill documents the same block, and the two share it.

---

## Delphi Language Target

**Minimum version:** Delphi 11 Alexandria.

**Use freely — Delphi 11+ syntax:**
```delphi
var x := SomeFunction();                  // inline variable
for var i := 0 to List.Count - 1 do      // for-var with index
for var item in Collection do             // for-var with iterator
```

**Do NOT use — Delphi 13 Florence only:**
```delphi
NameOf(MyField)                           // NameOf operator — not available yet
var x := if Condition then A else B;      // inline if expression — not available yet
```

For the language itself — what compiles on which release, object lifetime and `Free`/interfaces, strings and
encodings, generics, RTTI, threading — load the **`delphi`** skill. It assumes no framework and is the
foundation this one sits on.

---

**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".

---

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

**Never create a DMVCFramework project from scratch. Never write a `.dpr` bootstrap by hand.**
The IDE wizard already produces a correct, compiling, runnable project; your job is to add features to it.

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

The user is expected to have run the wizard already and to have started the agent **from inside the project
folder**. Look for these files in the working directory (a wizard project has them):

```
*.dpr                 the program: engine creation + server bootstrap
EngineConfigU.pas     controllers + middleware registration   <- where most of your work lands
BootConfigU.pas       dotEnv, LoggerPro, profiler
EntitiesU.pas         entities
ServicesU.pas         DI registrations (if the preset includes DI)
Controllers.*.pas     controllers  (controller-based presets)
RoutesU.pas           lambda routes (Minimal API presets)
```

If they are there: read `*.dpr`, `EngineConfigU.pas` and any existing `Controllers.*.pas` / `RoutesU.pas`
**before writing anything**, and match their conventions.

### 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 the preset that matches what you are building (RESTful API, Web Application, Minimal API, JSON-RPC,
> Real-Time, Full-Stack). Accept the defaults — the server backend is **Indy Direct**. Compile and run it
> once to confirm it works. Then `cd` into the project folder, start me there, and tell me what to add.

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

### Step 2b — Detect the host, and leave it alone

**For a NEW project the default is Indy Direct** — a self-contained executable, no WebModule, no WebBroker.
Never introduce a `TWebModule` into a new standalone server.

**But an existing project may legitimately be WebBroker-hosted** — an ISAPI DLL, an Apache module, or an app
that predates Indy Direct. Read the `.dpr` and tell them apart:

| What you find in the project | Host | What you do |
|------------------------------|------|-------------|
| `TMVCServerFactory.CreateIndyDirect` | Indy Direct | The default. Carry on. |
| `TMVCServerFactory.CreateHttpSys` | HTTP.sys | Carry on. |
| `TWebModule` + `TMVCEngine.CreateForWebBroker` / `TMVCEngine.Create(Self, …)`, or a `.dpr` with `TISAPIApplication` / `Web.ApacheApp` | WebBroker (ISAPI / Apache / WebBroker app) | **Legitimate. Keep it.** |

**Everything in this skill except hosting is identical across all three hosts** — controllers, actions,
routing attributes, `IMVCResponse`, entities, ActiveRecord, validation, DI, middleware, JWT, serialization.
A controller does not know what is listening on the socket.

So on a WebBroker project: add controllers and configure the engine exactly as documented, inside the
existing `ConfigureEngine` / `WebModuleCreate`. **Do not migrate it to Indy Direct, do not rewrite the
`.dpr`, do not warn the user that they "should" switch** — an ISAPI or Apache deployment is a deliberate
requirement, not a mistake. Only touch the host if the user explicitly asks.

Details of all hosts: `reference/servers.md`.

### Step 3 — Work within the wizard structure

The wizard generates a specific layout. Always follow it:

| Task | Where to act |
|------|-------------|
| Add a new controller | Create `Controllers.<Resource>U.pas`; add `uses` + `AEngine.AddController(...)` in `EngineConfigU.pas` |
| Add an entity | Extend `EntitiesU.pas` or create `Entities.<Domain>U.pas` beside it |
| Add middleware | Add `AEngine.AddMiddleware(...)` inside `ConfigureEngine` in `EngineConfigU.pas` |
| Change engine config | Edit the `TMVCEngine.Create` config lambda in the `.dpr` |
| Change the port / settings | Edit `bin/.env` — the wizard reads it via `dotEnv`; do not hard-code |

### Wizard-generated file overview

```
MyProject/
├── MyProject.dpr              # Boot, RegisterServices, then RunServer — Indy Direct, no WebModule
├── BootConfigU.pas            # dotEnv + LoggerPro + profiler setup (do not modify)
├── EngineConfigU.pas          # ConfigureEngine() — add controllers/middleware HERE
├── EntitiesU.pas              # Starter entity — extend as needed
├── ServicesU.pas              # DI registrations (presets with a service container)
├── Controllers.HomeU.pas      # Home controller — keep as reference
├── Controllers.PeopleU.pas    # CRUD example — follow this pattern
└── bin/.env                   # port, connection string, secrets — edit here, not in code
```

The `.dpr` already contains the correct Indy Direct bootstrap. Leave it alone unless the task is
specifically about hosting.

### EngineConfigU.pas — where to add controllers

```delphi
procedure ConfigureEngine(AEngine: TMVCEngine);
begin
  // Controllers
  AEngine.AddController(THomeController);
  AEngine.AddController(TPeopleController);
  AEngine.AddController(TProductsController);  // ← add new controllers here
  // Controllers - END

  // Middleware
  AEngine.AddMiddleware(TMVCCORSMiddleware.Create);  // ← add middleware here
  // Middleware - END
end;
```

---

## Reference files — read the one you need

This SKILL.md is the working core. Detailed material lives in `reference/`; open a file only when the
task needs it.

| File | Read it when the task involves |
|------|-------------------------------|
| `reference/servers.md` | Bootstrapping the engine: Indy Direct (default), HTTP.sys, WebBroker/ISAPI/Apache, `IMVCServer`, FireDAC connection setup |
| `reference/dotenv.md` | Configuration: the `Boot`/`dotEnvConfigure` pattern, profiles, precedence, `.env` syntax and expressions, the keys wizard projects use, secrets |
| `reference/activerecord.md` | Entities and the ORM: `TMVCActiveRecord`, mapping attributes, CRUD, RQL, named queries, hooks, soft delete, audit columns, optimistic locking, master-detail, the connection middleware, and the automatic REST CRUD controller |
| `reference/di-and-repository.md` | The service container (`DefaultMVCServiceContainer`, `[MVCInject]`) and `IMVCRepository<T>` |
| `reference/validation.md` | Every validator attribute with its parameters, when validation fires (controller, Minimal API, records, forms, ActiveRecord), error keys, the 422 body, manual validation, HTML form re-render with errors, custom validators, cross-field rules |
| `reference/sse.md` | Server-Sent Events: `TMVCSSEController`, `SSEBroker` |

Related skills: **`dmvcframework-minimal-api`** (lambda routes, no controller class) ·
**`dmvcframework-webapp`** (TemplatePro views + HTMX) · **`dmvcframework-testing`** (DUnitX integration tests).

---

## Core Principles

- **Attribute-driven routing** — routes live on controller methods via attributes, no config files
- **Functional actions** — controller actions are **always `function`**, never `procedure + Render(...)`. Return the data or an `IMVCResponse`; the framework serializes it automatically. **Exception:** SSE/streaming actions that write directly to the socket may use `procedure`.
- **Result ownership** — the framework frees the object you return. `ToFree<T>` is only for *extra* objects allocated in the action that you do NOT return (never `Result := ToFree(...)` → double free)
- **ActiveRecord ORM** — entities inherit `TMVCActiveRecord`, fields mapped with attributes
- **Middleware chain** — cross-cutting concerns (CORS, JWT, auth, compression) added in `ConfigureEngine`
- **Auto-serialization** — returning a `TObject` or `TObjectList<T>` from an action produces JSON automatically

---

## Controller Scaffolding — Functional Actions

**RULE: every controller action is a `function`. Never write `procedure SomeAction; begin Render(...); end;`.**
Return data objects or `IMVCResponse`. The framework detects the return type via RTTI and serializes automatically.
The framework frees the returned object after serializing it. `ToFree<T>(obj)` is only for extra
objects you allocate and do *not* return (see *Memory Management in Functional Actions*) — never wrap the result in it.

**File: `Controllers.MyResource.pas`**

```delphi
unit Controllers.MyResource;

interface

uses
  MVCFramework,
  MVCFramework.Commons,
  MVCFramework.Serializer.Commons,
  System.Generics.Collections,
  Entities.MyResource;

type
  [MVCDoc('CRUD API for MyResource')]
  [MVCPath('/myresources')]
  TMyResourceController = class(TMVCController)
  public
    [MVCDoc('Returns all MyResource records')]
    [MVCPath]
    [MVCHTTPMethod([httpGET])]
    function GetAll: TObjectList<TMyResource>;

    [MVCDoc('Returns MyResource by ID')]
    [MVCPath('/($id)')]
    [MVCHTTPMethod([httpGET])]
    function GetByID(id: Integer): TMyResource;

    [MVCDoc('Creates a new MyResource; returns 201 + Location')]
    [MVCPath]
    [MVCHTTPMethod([httpPOST])]
    function Create(const [MVCFromBody] Resource: TMyResource): IMVCResponse;

    [MVCDoc('Fully replaces a MyResource; returns 200 + updated record')]
    [MVCPath('/($id)')]
    [MVCHTTPMethod([httpPUT])]
    function Update(const id: Integer; const [MVCFromBody] Resource: TMyResource): IMVCResponse;

    [MVCDoc('Partially updates a MyResource; returns 204')]
    [MVCPath('/($id)')]
    [MVCHTTPMethod([httpPATCH])]
    function PartialUpdate(const id: Integer; const [MVCFromBody] Resource: TMyResource): IMVCResponse;

    [MVCDoc('Deletes a MyResource; returns 204')]
    [MVCPath('/($id)')]
    [MVCHTTPMethod([httpDELETE])]
    function Delete(id: Integer): IMVCResponse;
  end;

implementation

uses
  MVCFramework.ActiveRecord,
  System.SysUtils;

function TMyResourceController.GetAll: TObjectList<TMyResource>;
begin
  Result := TMVCActiveRecord.All<TMyResource>;      // framework frees it
end;

function TMyResourceController.GetByID(id: Integer): TMyResource;
begin
  Result := TMVCActiveRecord.GetByPk<TMyResource>(id);
end;

function TMyResourceController.Create(const [MVCFromBody] Resource: TMyResource): IMVCResponse;
begin
  Resource.Insert;
  Result := CreatedResponse('/myresources/' + Resource.ID.Value.ToString);  // ID: NullableInt64
end;

function TMyResourceController.Update(const id: Integer;
  const [MVCFromBody] Resource: TMyResource): IMVCResponse;
var
  lExisting: TMyResource;
begin
  lExisting := TMVCActiveRecord.GetByPk<TMyResource>(id);
  try
    // TMVCActiveRecord.Assign does nothing: copy the fields (or override Assign in the entity)
    lExisting.Name := Resource.Name;
    // copy the other fields...
    lExisting.Update;
  except
    lExisting.Free;
    raise;
  end;
  Result := OKResponse(lExisting);                  // response owns and frees it
end;

function TMyResourceController.PartialUpdate(const id: Integer;
  const [MVCFromBody] Resource: TMyResource): IMVCResponse;
var
  lExisting: TMyResource;
begin
  lExisting := ToFree(TMVCActiveRecord.GetByPk<TMyResource>(id));  // not returned → ToFree
  if Resource.Name <> '' then lExisting.Name := Resource.Name;
  // apply other optional fields...
  lExisting.Update;
  Result := NoContentResponse;
end;

function TMyResourceController.Delete(id: Integer): IMVCResponse;
begin
  begin
    var Ctx := TMVCActiveRecord.UseTransactionContext;   // commits on exit, rolls back on exception
    ToFree(TMVCActiveRecord.GetByPk<TMyResource>(id)).Delete;
  end;
  Result := NoContentResponse;
end;

end.
```

### When `procedure` is allowed

`procedure` (no return value) is acceptable only when the action does not produce a serialized body:
- **Redirects** in web controllers — `Redirect('/people');`
- **SSE** — but do not hand-roll it: inherit `TMVCSSEController` (see `reference/sse.md`).

Everything else **must** be a `function`.

---

## Route Attribute Reference

| Attribute | Purpose | Example |
|-----------|---------|---------|
| `[MVCPath]` | Map to controller base path | `[MVCPath]` → `/myresources` |
| `[MVCPath('/($id)')]` | URL parameter | `/myresources/42` |
| `[MVCPath('/search')]` | Sub-path | `/myresources/search` |
| `[MVCHTTPMethod([httpGET])]` | Allowed HTTP methods | `httpGET, httpPOST, httpPUT, httpDELETE, httpPATCH` |
| `[MVCProduces(TMVCMediaType.APPLICATION_JSON)]` | Response content type | optional — JSON is default |
| `[MVCConsumes(TMVCMediaType.APPLICATION_JSON)]` | Expected request type | optional |
| `[MVCDoc('description')]` | Swagger/OpenAPI doc string | any string |
| `[MVCRequiresAuthentication]` | Requires authenticated user | on method or class |
| `[MVCRequiresRole('admin')]` | Requires a role. **Not in `MVCFramework`** — it is `MVCRequiresRoleAttribute` in `MVCFramework.Middleware.Authentication.RoleBasedAuthHandler`, and it is enforced **only** by that handler. Add the unit and the handler, or it silently does nothing | on method or class |

---

## Parameter Injection Attributes

```delphi
// URL segment: /orders/($id) → mapped to 'id' parameter
function GetOrder(id: Integer): TOrder;

// Query string: /orders?status=open
function Search(const [MVCFromQueryString('status', '')] Status: string): TObjectList<TOrder>;

// Request body (auto-deserialized from JSON)
function Create(const [MVCFromBody] Order: TOrder): IMVCResponse;

// Collection from body
function BulkCreate(const [MVCFromBody] Orders: TObjectList<TOrder>): IMVCResponse;

// HTTP header
function WithHeader(const [MVCFromHeader('X-Tenant-ID')] TenantID: string): IMVCResponse;

// Cookie
function WithCookie(const [MVCFromCookie('session')] SessionID: string): IMVCResponse;
```

---

## IMVCResponse Factory Methods

Use these to return responses with specific HTTP status codes from functional actions.
All methods are inherited from `TMVCRenderer` (available in any controller).

```delphi
// 200 OK
Result := OKResponse(AnObject);           // body = serialized object
Result := OKResponse(ObjectDict()         // body = {"data": [...]}
  .Add('data', AList));
Result := OKResponse('message text');     // body = plain message
Result := OKResponse;                     // 200 with empty body

// 201 Created
Result := CreatedResponse('/resources/' + id.ToString);               // no body
Result := CreatedResponse('/resources/' + id.ToString, AnObject);     // with body
Result := CreatedResponse('/resources/' + id.ToString, 'Created OK'); // with message

// 202 Accepted
Result := AcceptedResponse('/jobs/' + jobId);

// 204 No Content
Result := NoContentResponse;

// 3xx Redirect
Result := RedirectResponse('/new-url');
Result := RedirectResponse('/new-url', {Permanent=}True);

// 304 Not Modified
Result := NotModifiedResponse;

// 400 Bad Request
Result := BadRequestResponse('Validation failed');
Result := BadRequestResponse(ErrorObject);

// 401 Unauthorized
Result := UnauthorizedResponse;
Result := UnauthorizedResponse('Token expired');

// 403 Forbidden
Result := ForbiddenResponse;
Result := ForbiddenResponse('Insufficient permissions');

// 404 Not Found
Result := NotFoundResponse;
Result := NotFoundResponse('Resource not found');

// 409 Conflict
Result := ConflictResponse;

// 422 Unprocessable Content
Result := UnprocessableContentResponse('Field email is invalid');
Result := UnprocessableContentResponse(ErrorObject);

// 500 Internal Server Error
Result := InternalServerErrorResponse;
Result := InternalServerErrorResponse('Unexpected failure');

// Custom status code
Result := StatusResponse(418, 'I am a teapot');

// Builder pattern for full control
Result := MVCResponseBuilder
  .StatusCode(202)
  .Header('X-Job-ID', jobId)
  .Body(AnObject)
  .Build;
```

**Error exceptions** (alternative — framework catches and serializes automatically):

```delphi
raise EMVCException.Create(HTTP_STATUS.NotFound, 'Resource not found');
raise EMVCException.Create(HTTP_STATUS.BadRequest, 'Invalid input');
raise EMVCException.Create(HTTP_STATUS.Conflict, 'Already exists');
raise EMVCException.CreateFmt(HTTP_STATUS.UnprocessableEntity, 'Field %s invalid', ['email']);
```

---

## Memory Management in Functional Actions

**The framework already frees the object you return.** `MVCFramework.pas` renders the result and then
calls `lResponseObject.Free` on it. You do not need — and must not add — any ownership call for it.

`ToFree<T>(obj)` exists for the **other** objects: intermediates you allocate inside the action and do
**not** return. It registers them in an owning free-list emptied after the action, so you can skip `try/finally`.

**Never `Result := ToFree(...)`** — the object would be freed by the free-list *and* as the response
object: **double free**.

```delphi
// CORRECT — just return it. No ToFree.
function GetAll: TObjectList<TMyResource>;
begin
  Result := TMVCActiveRecord.All<TMyResource>;
end;

function GetByID(id: Integer): TMyResource;
begin
  Result := TMVCActiveRecord.GetByPk<TMyResource>(id);
end;

// CORRECT — ToFree for an intermediate that is NOT the result
function GetSingleDataSet: TDataSet;
begin
  var lDM := ToFree<TdmMain>(TdmMain.Create(nil));  // the datamodule is not returned
  lDM.dsPeople.Open;
  Result := lDM.dsPeople;                            // the dataset it owns is
end;

// [MVCFromBody] parameter: framework owns it; safe to return it directly
function Create(const [MVCFromBody] Resource: TMyResource): IMVCResponse;
begin
  Resource.Insert;
  Result := CreatedResponse('/myresources/' + Resource.ID.Value.ToString);  // ID: NullableInt64
end;
```

**Rules:**
- The **result** is freed by the framework — never `ToFree` it
- `ToFree` only for objects allocated inside the action that you do **not** return
- **Do not** `ToFree` a `[MVCFromBody]` parameter — the framework owns it (and detects the alias if you return it, freeing it once)
- Nothing to free besides the result? Then you do not need `ToFree` at all — most actions don't

---

## Request Context

```delphi
// Available inside any action via the inherited Context property:
Context.Request.HTTPMethod                    // TMVCHTTPMethodType
Context.Request.Body                          // raw body string
Context.Request.BodyAs<TMyClass>              // deserialize body to typed object
Context.Request.QueryStringParam('name')      // query string value
Context.Request.Headers['Authorization']      // request header
Context.Request.Cookie('session')             // cookie value
Context.Request.PathInfo                      // URL path
Context.Request.ClientIP                      // remote IP

Context.Response.StatusCode := 200;
Context.Response.SetCustomHeader('X-Header', 'value');

Context.Session['key'] := 'value';            // session read/write
Context.LoggedUser.UserName                   // authenticated user (requires auth middleware)
Context.Data['mykey'] := 'a string';          // per-request storage; TMVCStringDictionary — STRINGS ONLY, no objects
```

---

## Lifecycle Hooks

Override in your controller:

```delphi
procedure OnBeforeAction(AContext: TWebContext; const AActionName: string;
  var AHandled: Boolean); override;
procedure OnAfterAction(AContext: TWebContext; const AActionName: string); override;
procedure OnException(const AContext: TWebContext; const AException: Exception;
  var AHandled: Boolean); override;
procedure MVCControllerAfterCreate; override;
procedure MVCControllerBeforeDestroy; override;
```

`AHandled := True` in `OnBeforeAction` short-circuits the action (auth guards, pre-flight checks, caching).

ActiveRecord lifecycle (override in entity classes):

```delphi
procedure OnBeforeInsertOrUpdate; override;  // computed fields (runs AFTER the validators)
procedure OnAfterLoad; override;             // load related records
procedure OnAfterInsertOrUpdate; override;   // cascade saves
procedure OnBeforeDelete; override;          // cascade deletes
procedure OnValidation(const Action: TMVCEntityAction); override; // validation
```

---

## Middleware Configuration

Add inside `ConfigureEngine` (in a WebBroker project, that is still where it goes — it is called from
`WebModuleCreate`). Order matters:

```delphi
// FireDAC connection pool for ActiveRecord (required when using ORM)
FEngine.AddMiddleware(TMVCActiveRecordMiddleware.Create('MyConnDef'));

// CORS — must come BEFORE JWT so OPTIONS preflight bypasses auth
FEngine.AddMiddleware(TMVCCORSMiddleware.Create);

// Gzip/deflate compression (threshold in bytes)
FEngine.AddMiddleware(TMVCCompressionMiddleware.Create(256));

// JWT authentication — AConfigClaims is MANDATORY (2nd argument)
FEngine.AddMiddleware(TMVCJWTAuthenticationMiddleware.Create(
  TMyAuthHandler.Create,
  procedure(const JWT: TJWT)                    // TJWTClaimsSetup
  begin
    JWT.Claims.Issuer := 'MyApp';
    JWT.Claims.ExpirationTime := Now + OneHour;
    JWT.Claims.NotBefore := Now - OneMinute * 5;
    JWT.Claims.IssuedAt := Now;
  end,
  dotEnv.Env('JWT_SECRET', ''),                 // never hard-code the secret
  '/auth/login',                                // endpoint that issues tokens
  [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore, TJWTCheckableClaim.IssuedAt],
  300                                           // leeway, seconds
));

// Basic authentication
FEngine.AddMiddleware(TMVCBasicAuthenticationMiddleware.Create(TMyAuthHandler.Create));

// Serve static files
FEngine.AddMiddleware(TMVCStaticFilesMiddleware.Create('/app', '.\www'));

// ETag caching
FEngine.AddMiddleware(TMVCETagMiddleware.Create);
```

---

## JWT Authentication Pattern

**Auth handler:**

```delphi
type
  TMyAuthHandler = class(TInterfacedObject, IMVCAuthenticationHandler)
  public
    procedure OnRequest(const AContext: TWebContext;
      const AControllerQualifiedClassName, AActionName: string;
      var AAuthenticationRequired: Boolean);
    procedure OnAuthentication(const AContext: TWebContext;
      const AUserName, APassword: string;
      AUserRoles: TList<string>; var AIsValid: Boolean;
      const ASessionData: TDictionary<string, string>);
    procedure OnAuthorization(const AContext: TWebContext; AUserRoles: TList<string>;
      const AControllerQualifiedClassName, AActionName: string;
      var AIsAuthorized: Boolean);
  end;

procedure TMyAuthHandler.OnRequest(const AContext: TWebContext;
  const AControllerQualifiedClassName, AActionName: string;
  var AAuthenticationRequired: Boolean);
begin
  AAuthenticationRequired := True; // set False for specific public actions
end;

procedure TMyAuthHandler.OnAuthentication(const AContext: TWebContext;
  const AUserName, APassword: string;
  AUserRoles: TList<string>; var AIsValid: Boolean;
  const ASessionData: TDictionary<string, string>);
begin
  AIsValid := (AUserName = 'admin') and (APassword = 'secret');
  if AIsValid then
    AUserRoles.Add('admin');
end;

procedure TMyAuthHandler.OnAuthorization(const AContext: TWebContext;
  AUserRoles: TList<string>;
  const AControllerQualifiedClassName, AActionName: string;
  var AIsAuthorized: Boolean);
begin
  AIsAuthorized := AUserRoles.Contains('admin');
end;
```

**Attribute-level auth:**

```delphi
[MVCRequiresAuthentication]     // any authenticated user — MVCFramework

// [MVCRequiresRole] is NOT part of MVCFramework. It comes from
// MVCFramework.Middleware.Authentication.RoleBasedAuthHandler and is only enforced
// when that handler is installed. Without it the attribute compiles and does nothing.
[MVCRequiresRole('admin')]
```

---

## HTTP_STATUS Constants

```delphi
HTTP_STATUS.OK                  // 200
HTTP_STATUS.Created             // 201
HTTP_STATUS.Accepted            // 202
HTTP_STATUS.NoContent           // 204
HTTP_STATUS.BadRequest          // 400
HTTP_STATUS.Unauthorized        // 401
HTTP_STATUS.Forbidden           // 403
HTTP_STATUS.NotFound            // 404
HTTP_STATUS.MethodNotAllowed    // 405
HTTP_STATUS.Conflict            // 409
HTTP_STATUS.UnprocessableEntity // 422
HTTP_STATUS.InternalServerError // 500
```

---

## Scaffolding Workflow

When the user asks to scaffold a resource (e.g., "create a Products controller"):

### Step 1 — Entity (if DB-backed)
Generate `Entities.Products.pas` with `TProduct` inheriting `TMVCActiveRecord`.
- Use `[MVCNameCase(ncCamelCase)]`, `[MVCTable('...')]`
- Map all fields with `[MVCTableField(...)]` — always `foPrimaryKey, foAutoGenerated` on the PK

### Step 2 — DTO (if needed for validation / PATCH / complex input)
Generate `TProductIn` with validator attributes on its **properties** (`TMVCValidatable` only if it needs `OnValidate`).
Use plain `string`/`Integer` for fields with format/range rules: on a `NullableXxx` property only `MVCRequired` checks anything (`reference/validation.md`).

### Step 3 — Controller
Generate `Controllers.Products.pas` with `TProductsController`.
- **All actions are `function`** returning either a data type or `IMVCResponse`
- Include full CRUD: `GetAll`, `GetByID`, `Create`, `Update`, `PartialUpdate`, `Delete`
- Add `[MVCDoc(...)]` on every action
- Wrap mutations in transactions (`UseTransactionContext`); return the object, don't `ToFree` it

### Step 4 — Register the controller
In `EngineConfigU.pas`: add `uses Controllers.Products;` and
`AEngine.AddController(TProductsController);` inside `ConfigureEngine`.

### Step 5 — Verify naming
- Controller class: `T[Resource]Controller` (plural resource name)
- Entity class: `T[Entity]` (singular)
- Unit names: `Controllers.[Resource]`, `Entities.[Entity]`

---

## Common Pitfalls

- **Never `procedure + Render(...)`** — use `function` returning data or `IMVCResponse` factory methods
- **Double free** — the framework frees the returned object. `Result := ToFree(x)` and `OKResponse(ToFree(x))` free it twice. `ToFree` is for objects you do NOT return
- **`[MVCOwned]`** — deserialization hint only; the entity still creates and frees the list (`reference/activerecord.md`, Master-Detail)
- **Nullable fields** — `NullableInt64`, `NullableString`, etc. from `MVCFramework.Nullables`; always check `.HasValue` before `.Value`
- **Transactions** — `TMVCActiveRecordMiddleware` opens a connection per request but does NOT auto-commit; wrap multi-step mutations in `StartTransaction/Commit/Rollback`
- **CORS order** — add `TMVCCORSMiddleware` BEFORE JWT/Basic so `OPTIONS` preflight is served without auth
- **Route conflicts** — two actions on the same path must differ only in `[MVCHTTPMethod]`
- **SSE** — never hand-roll the stream; inherit `TMVCSSEController` and override its hooks (`reference/sse.md`)
- **Validation trigger** — any class with ≥1 validator attribute is auto-validated on `[MVCFromBody]`; `TMVCValidatable` is needed only for `OnValidate`. Suppress with `[MVCFromBody(bvDoNotValidate)]`
- **Validators read properties, not fields** (except ActiveRecord at save time); every validator but `MVCRequired` passes an empty value; `MVCRequired` always passes on `Integer`/`TDate`. Error keys are Delphi property names (`Email`, `Items[0].Name`). Minimal API 422 is ProblemDetails with an `errors` field → message object (after 3.5.0-rc7; before it, no per-field messages) — details in `reference/validation.md`
- **`OnValidate` error keys are stored verbatim** — nothing converts them to the serialized name. Whatever string you pass to `AErrors.Add` is what the client sees; be consistent (the samples use the Delphi property name, e.g. `'EndDate'`)
- **Soft delete — use `NullableTDateTime`** — the `[MVCSoftDeleted]` filter relies on `IS NULL`; a plain `TDateTime` defaults to `1899-12-30`, which would never be NULL
- **`[MVCChangeTracking]` overhead** — allocates a CRC32 snapshot per tracked field; only apply to entities that are frequently partially updated
- **`UseTransactionContext` scope** — declare with `var Ctx :=` at the top of the block; do NOT assign it to another variable (raises `EMVCActiveRecordTransactionContext`)
- **Partition filter and `DeleteAll`** — `DeleteAll(TPartitionedClass)` respects the partition and only deletes rows matching the partition value

---

## Key Units Reference

| Unit | Purpose |
|------|---------|
| `MVCFramework` | Core: `TMVCController`, `TMVCEngine`, attributes, `IMVCResponse` |
| `MVCFramework.Commons` | `TMVCMediaType`, `TMVCHTTPMethodType`, `HTTP_STATUS` |
| `MVCFramework.ActiveRecord` | `TMVCActiveRecord` and its class-level CRUD; also `ActiveRecordConnectionsRegistry`, `TMVCActiveRecordBackEnd`, `loIgnoreNotExistentFields`, `TMVCActiveRecordList` |
| `MVCFramework.Nullables` | `NullableInt32/64/String/Boolean/TDate/TDateTime/Currency` |
| `MVCFramework.Serializer.Commons` | `[MVCNameCase]`, `[MVCNameAs]`, `[MVCDoNotSerialize]`, etc. |
| `MVCFramework.Validation` | `TMVCValidatable`, `EMVCValidationException`, `EMVCStorageValidationException` |
| `MVCFramework.Validators` | All validator attributes (`MVCRequired`, `MVCEmail`, `MVCRange`, …) |
| `MVCFramework.ValidationEngine` | `TMVCValidationEngine` for manual validation |
| `MVCFramework.Middleware.CORS` | `TMVCCORSMiddleware` |
| `MVCFramework.Middleware.Compression` | `TMVCCompressionMiddleware` |
| `MVCFramework.Middleware.ActiveRecord` | `TMVCActiveRecordMiddleware` |
| `MVCFramework.Middleware.JWT` | `TMVCJWTAuthenticationMiddleware` |
| `MVCFramework.DataSet.Utils` | Dataset → JSON helpers |
| `MVCFramework.Container` | `IMVCServiceContainer`, DI registration |
| `MVCFramework.SQLGenerators` | SQL generators (needed for some SQL customization) |

---

## Sample Reference Projects

All samples are in the official repository at https://github.com/danieleteti/delphimvcframework

| Folder | Use case |
|--------|----------|
| `samples/basicdemo_server/` | Minimal setup, hello world |
| `samples/master_details/` | Full CRUD with master-detail relations |
| `samples/routing/` | All routing patterns and parameter types |
| `samples/jsonwebtokenplain/` | JWT authentication |
| `samples/activerecord_restful_crud/` | ActiveRecord + auto-CRUD with `[MVCEntityActions]` |
| `samples/validation_showcase/` | All validator attributes, cross-field, `OnValidate` |
| `samples/validation_vs_storage_demo/` | DTO vs ActiveRecord two-layer validation pattern |
| `samples/activerecord_showcase/` | Full ActiveRecord feature showcase (all attributes, named queries, soft-delete, change tracking, audit, partitioning) |

---

Files in this skill

  • SKILL.md33.8 KB
  • reference/activerecord.md23.9 KB
  • reference/di-and-repository.md4.2 KB
  • reference/dotenv.md7.5 KB
  • reference/servers.md5.9 KB
  • reference/sse.md1.7 KB
  • reference/validation.md9.9 KB
  • scripts/build.bat5.1 KB
  • scripts/run.bat1.5 KB

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.