Become a member!

Delphi Minimal API: REST API semplici e veloci con DMVCFramework

🌐
Questo articolo è disponibile anche in altre lingue:
🇬🇧 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).

I preset Minimal API RESTful e Minimal API WebApp nel Repository Oggetti di Delphi

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