Delphi Minimal API: REST API semplici e veloci con DMVCFramework
🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇧🇷 Português
REST API e web app semplici e veloci in Delphi — route con metodi anonimi, nessuna classe controller, con DMVCFramework, il framework Delphi open-source più popolare.
Un Nuovo Modo di Scrivere API in Delphi con DMVCFramework
DMVCFramework è nato per risolvere problemi “grandi” e complessi: applicazioni server di lunga durata, con molti endpoint, regole di business articolate e team che ci lavorano per anni. Proprio per questo il modello controller-action è sempre stato il cuore del framework. È robusto, testabile, scalabile, impone una struttura chiara man mano che il progetto cresce: è questa solidità ad aver reso DMVCFramework il progetto open source più diffuso per realizzare soluzioni web in Delphi, e rimarrà tale.
Ma negli ultimi anni ho osservato l’affiancarsi di un nuovo approccio nell’ecosistema: FastAPI in Python, Minimal API in ASP.NET Core, Hono in TypeScript. Tutti offrono un’alternativa più diretta per certi scenari: microservizi, prototipi, API semplici.
Ho voluto portare la stessa scelta in DMVCFramework. E funziona.
Il progetto conta ora più di 50 contributori: una comunità in crescita che rende possibile lavorare su funzionalità ambiziose come questa.
E non è solo una questione di codice: le Minimal API sono già integrate nel wizard dell’IDE, con due nuovi template nel Repository Oggetti di Delphi (li vediamo in dettaglio più avanti).
Hello, World
Prima di tutto: quanto è minimale?
AEngine.Root
.MapGet('/hello',
function: IMVCResponse
begin
Result := Ok('Hello, World!');
end);
Tre righe. Nessuna classe, nessun attributo, nessun file separato.
Cosa restituisce? Il metodo anonimo ha tipo di ritorno IMVCResponse, l’oggetto che descrive
l’intera risposta HTTP (status, header, corpo). Ok(...) è una factory che ne crea uno con
status 200 OK, e il corpo è sempre un oggetto JSON: passandole una stringa, questa viene
incapsulata nel campo message. La risposta che arriva al client è quindi:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{"message":"Hello, World!"}
Passando invece a Ok(...) un oggetto o una lista, questi finiscono nel campo data dello stesso
oggetto JSON ({"data": ...}): è lo stesso meccanismo del modello a controller, qui esposto da una
funzione invece che da un metodo.
Come appare in un progetto reale
Prima di guardare il codice, una parola sui filtri. Ne esistono di due tipi: gli
EndpointFilter, che si agganciano a uno specifico gruppo di route tramite .Use(...), e gli
HTTPFilter, registrati a livello di engine, che si applicano a ogni richiesta
indipendentemente dal routing, lavorando direttamente sul trasporto HTTP (Ctx.Request/Ctx.Response). Sono entrambi
l’equivalente concettuale del middleware classico di DMVCFramework, e li tratteremo in dettaglio
più avanti. Nell’esempio qui sotto usiamo i primi: ogni route del gruppo li attraversa in ordine
prima di raggiungere il gestore. Per ora basta sapere che .Use() significa
“applica questo comportamento a tutto ciò che segue”.
Ecco una REST API completa (autenticazione JWT, logging, endpoint CRUD) in un singolo file, senza nemmeno una classe controller:
procedure RegisterRoutes(AEngine: TMVCEngine);
begin
// Health check pubblico, nessun filtro
AEngine.Root
.MapGet('/health',
function: IMVCResponse
begin
Result := Ok('OK');
end);
// Gruppo /api/v1: autenticazione e logging applicati a tutte le route
AEngine.Prefix('/api/v1')
.Use(RequestLoggingFilter())
.Use(BearerAuthFilter())
// Per convenzione, ogni interfaccia (come IPeopleService) viene risolta dal container DI
.MapGet<IPeopleService>('/people',
function(Svc: IPeopleService): IMVCResponse
begin
Result := Ok(Svc.GetAll);
end)
.MapGet<string, IPeopleService>('/people/($id)',
function(ID: string; Svc: IPeopleService): IMVCResponse
begin
Result := Ok(Svc.GetByID(ID));
end)
.MapPost<TPerson, IPeopleService>('/people',
function(Person: TPerson; Svc: IPeopleService): IMVCResponse
begin
Svc.Create(Person);
Result := Created('/api/v1/people/' + Person.ID.ToString, Person);
end);
end;
Tutto qui. Nessun [MVCPath], nessun [MVCHTTPMethod], nessuna classe controller da dichiarare.
Applicazioni grandi: la struttura non scompare, la scegli tu
La domanda legittima è: “e quando ho 80 route?”
Nessun framework di routing minimale scala bene se tutto finisce in un unico RegisterRoutes.
La differenza è che con DMVC Minimal API la struttura non è imposta dalla sintassi,
la imponi tu, nel modo che ha più senso per il tuo dominio.
Il pattern naturale è suddividere le route per area funzionale, ognuna nella propria procedura (o unit), passando il gruppo come parametro:
// PeopleRoutesU.pas
procedure RegisterPeopleRoutes(AGroup: TMVCRouteGroup<TObject>);
begin
AGroup
.MapGet<IPeopleService>('/people', ...)
.MapGet<string, IPeopleService>('/people/($id)', ...)
.MapPost<TPerson, IPeopleService>('/people', ...)
.MapPut<string, TPerson, IPeopleService>('/people/($id)', ...)
.MapDelete<string, IPeopleService>('/people/($id)', ...);
end;
// OrdersRoutesU.pas
procedure RegisterOrdersRoutes(AGroup: TMVCRouteGroup<TObject>);
begin
AGroup
.MapGet<IOrderService>('/orders', ...)
.MapPost<TOrder, IOrderService>('/orders', ...);
end;
// RoutesU.pas: punto di assemblaggio unico
procedure RegisterRoutes(AEngine: TMVCEngine);
var
lAPI: TMVCRouteGroup<TObject>;
begin
lAPI := AEngine.Prefix('/api/v1')
.Use(RequestLoggingFilter())
.Use(BearerAuthFilter());
RegisterPeopleRoutes(lAPI);
RegisterOrdersRoutes(lAPI);
end;
Il risultato è una struttura ad albero esplicita: i filtri condivisi vivono in un unico posto, ogni area funzionale vive nel proprio file, e il punto di assemblaggio è una lista di chiamate leggibile in dieci secondi.
Con il modello classico a controller, la struttura esiste ma è imposta dalla sintassi: ogni risorsa richiede una classe dedicata. Con le Minimal API è opzionale: puoi iniziare con tutto in un file e fare refactoring quando ha senso, senza riscrivere nulla.
Come funziona il binding degli argomenti
Ogni parametro del metodo anonimo viene risolto per tipo, automaticamente:
| Tipo del parametro | Sorgente |
|---|---|
TWebContext |
Contesto HTTP della richiesta corrente |
| Interfaccia nel container DI | Servizio iniettato |
| Classe | Corpo JSON (POST/PUT/PATCH) o query string (GET/DELETE) |
Record con attributi [MVCFromBody] / [MVCFromQueryString] / [MVCFromContentField] |
Binding misto, campo per campo |
string, Integer, TGUID, TDateTime, … |
Parametro di route se presente, altrimenti dalla query string |
Non è necessario dichiarare da dove proviene ciascun parametro: il framework lo deduce dal tipo e dal contesto.
Filtri: due livelli, una pipeline
Una delle cose di cui sono più soddisfatto in questa implementazione è il modello a due livelli di filtri.
EndpointFilter: per gruppo, si attiva solo quando una route corrisponde:
AEngine.Prefix('/admin')
.Use(
function(Ctx: TWebContext; Next: TMVCEndpointFilterNext): IMVCResponse
begin
if not IsAdmin(Ctx) then
Result := Status(403, 'Forbidden')
else
Result := Next(); // prosegue verso il filtro successivo o il gestore
end);
HTTPFilter: si registra a livello di engine e avvolge l’intera gestione di ogni richiesta,
prima ancora che avvenga il routing, lavorando direttamente sul trasporto HTTP
(Ctx.Request/Ctx.Response). Ideale per rate limiting, file statici, security header, compressione:
AEngine.UseHTTPFilter(
procedure(Ctx: TWebContext; Next: TMVCHTTPFilterNext)
begin
Ctx.Response.SetCustomHeader('X-Frame-Options', 'DENY');
Next(); // lascia continuare la pipeline
end);
Entrambi i tipi si compongono nell’ordine di registrazione. L’ordine è prevedibile, esplicito e leggibile in un unico punto.
Non solo API: anche le web app
Le Minimal API non sono solo JSON. Con .AsWeb un gruppo diventa una raccolta di route
server-rendered che restituiscono HTML tramite TemplatePro. RenderView e ViewData funzionano
come ci si aspetta, e la sessione è semplicemente un filtro:
AEngine.Root.AsWeb
.Use(MemorySession(10))
.MapGet('/',
function: IMVCResponse
begin
ViewData['title'] := 'Home';
Result := RenderView('pages/home');
end);
Le route di tipo rkWeb sono escluse automaticamente dalla spec OpenAPI: il framework
sa distinguere gli endpoint API dalle pagine HTML.
Supporto nel Wizard dell’IDE
Il wizard non poteva mancare. Troverai due nuovi preset nel Repository Oggetti di Delphi:
- Minimal API RESTful: API JSON pura, filtri per gruppo, container DI
- Minimal API Web App: route
.AsWeb, sessioni, login/logout, pronto per HTMX
Un progetto funzionante, zero configurazione, pronto in 30 secondi.
Stato
La funzionalità è disponibile sul branch master a partire dalla versione 3.5.0-silicon-rc3.
Tutti i 872 test di integrazione passano sia su Win32 che su Win64, contro tre distinti backend server
(WebBroker, Indy Direct, HTTP.sys): 5.232 esecuzioni totali su tutte le combinazioni di piattaforma e server.
In arrivo:
- Benchmark API classica vs Minimal API (anticipazione: i risultati mi hanno sorpreso)
- Integrazione OpenAPI 3.x
- Una guida dedicata con pattern reali, filtri avanzati e integrazione del container DI
Daniele
Comments
comments powered by Disqus