Become a member!

Delphi Minimal API: APIs REST simples y rápidas con DMVCFramework

🌐
Este artículo también está disponible en otros idiomas:
🇬🇧 English  •  🇮🇹 Italiano  •  🇩🇪 Deutsch  •  🇧🇷 Português

APIs REST y aplicaciones web simples y rápidas en Delphi — rutas con métodos anónimos, sin clase controlador, con DMVCFramework, el framework Delphi open-source más popular.

Una nueva forma de escribir APIs en Delphi con DMVCFramework

DMVCFramework nació para resolver problemas “grandes” y complejos: aplicaciones de servidor de larga duración, con muchos endpoints, reglas de negocio elaboradas y equipos que trabajan en ellas durante años. Precisamente por eso el modelo controlador-acción siempre ha sido el corazón del framework. Es robusto, comprobable, escalable, e impone una estructura clara a medida que el proyecto crece: es esta solidez la que ha convertido a DMVCFramework en el proyecto open source más extendido para crear soluciones web en Delphi, y así seguirá siendo.

Pero en los últimos años he observado cómo se suma un nuevo enfoque al ecosistema: FastAPI en Python, Minimal API en ASP.NET Core, Hono en TypeScript. Todos ofrecen una alternativa más directa para ciertos escenarios: microservicios, prototipos, APIs sencillas.

Quería ofrecer esa misma elección en DMVCFramework. Y funciona.

El proyecto cuenta ahora con más de 50 colaboradores: una comunidad en crecimiento que hace posible trabajar en funcionalidades ambiciosas como esta.

Y no es solo una cuestión de código: la Minimal API ya está integrada en el asistente del IDE, con dos nuevas plantillas en el Repositorio de Objetos de Delphi (las veremos en detalle más adelante).

Los presets Minimal API RESTful y Minimal API WebApp en el Repositorio de Objetos de Delphi

Hola, Mundo

Antes que nada: ¿qué tan minimal es?

AEngine.Root
  .MapGet('/hello',
    function: IMVCResponse
    begin
      Result := Ok('Hello, World!');
    end);

Tres líneas. Sin clase, sin atributo, sin archivo separado.

¿Qué devuelve? El método anónimo tiene como tipo de retorno IMVCResponse, el objeto que describe toda la respuesta HTTP (estado, cabeceras, cuerpo). Ok(...) es una factory que crea uno con estado 200 OK, y el cuerpo es siempre un objeto JSON: si le pasas una cadena, esta se encapsula en el campo message. La respuesta que llega al cliente es, por tanto:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{"message":"Hello, World!"}

Si en cambio le pasas a Ok(...) un objeto o una lista, estos acaban en el campo data del mismo objeto JSON ({"data": ...}): es el mismo mecanismo del modelo de controlador, aquí expuesto mediante una función en lugar de un método.


Cómo se ve en un proyecto real

Antes de ver el código, una nota sobre los filtros. Existen de dos tipos: los EndpointFilter, que se asocian a un grupo de rutas específico mediante .Use(...), y los HTTPFilter, registrados a nivel de motor, que se aplican a cada petición con independencia del enrutamiento, trabajando directamente sobre el transporte HTTP (Ctx.Request/Ctx.Response). Ambos son el equivalente conceptual del middleware clásico de DMVCFramework, y los veremos en detalle más adelante. En el ejemplo de abajo usamos los primeros: cada ruta del grupo los atraviesa en orden antes de llegar al handler. Por ahora, basta saber que .Use() significa “aplicar este comportamiento a todo lo que sigue”.

A continuación, una API REST completa (autenticación JWT, registro de peticiones, endpoints CRUD) en un único archivo, sin una sola clase controlador:

procedure RegisterRoutes(AEngine: TMVCEngine);
begin
  // Health check público, sin filtros
  AEngine.Root
    .MapGet('/health',
      function: IMVCResponse
      begin
        Result := Ok('OK');
      end);

  // Grupo /api/v1: autenticación y registro aplicados a todas las rutas
  AEngine.Prefix('/api/v1')
    .Use(RequestLoggingFilter())
    .Use(BearerAuthFilter())

    // Por convención, toda interfaz (como IPeopleService) se resuelve desde el contenedor 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;

Eso es todo. Sin [MVCPath], sin [MVCHTTPMethod], sin clase controlador que declarar.


Aplicaciones grandes: la estructura no desaparece, la eliges tú

La pregunta legítima es: "¿y cuando tengo 80 rutas?"

Ningún framework de routing mínimo escala bien si todo va en un único RegisterRoutes. La diferencia es que con DMVC Minimal API la estructura no la impone la sintaxis, la impones tú, de la manera que tenga más sentido para tu dominio.

El patrón natural es dividir las rutas por área funcional, cada una en su propio procedimiento (o unidad), pasando el grupo como parámetro:

// 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 de ensamblaje único
procedure RegisterRoutes(AEngine: TMVCEngine);
var
  lAPI: TMVCRouteGroup<TObject>;
begin
  lAPI := AEngine.Prefix('/api/v1')
    .Use(RequestLoggingFilter())
    .Use(BearerAuthFilter());

  RegisterPeopleRoutes(lAPI);
  RegisterOrdersRoutes(lAPI);
end;

El resultado es una estructura de árbol explícita: los filtros compartidos están en un solo lugar, cada área funcional tiene su propio archivo, y el punto de ensamblaje es una lista de llamadas que se lee en diez segundos.

Con el modelo de controlador clásico, la estructura existe pero está impuesta por la sintaxis: cada recurso requiere una clase dedicada. Con Minimal API es opcional: puedes empezar con todo en un archivo y refactorizar cuando tenga sentido, sin reescribir nada.


Cómo funciona la vinculación de argumentos

Cada parámetro del método anónimo se resuelve por tipo, de forma automática:

Tipo de parámetro Origen
TWebContext Contexto HTTP de la petición actual
Interfaz en el contenedor DI Servicio inyectado
Clase Cuerpo JSON (POST/PUT/PATCH) o cadena de consulta (GET/DELETE)
Record con atributos [MVCFromBody] / [MVCFromQueryString] / [MVCFromContentField] Vinculación mixta, campo a campo
string, Integer, TGUID, TDateTime, … Parámetro de ruta si existe, en caso contrario cadena de consulta

No es necesario declarar de dónde viene cada parámetro: el framework lo deduce a partir del tipo y el contexto.


Filtros: dos niveles, una única cadena

Una de las cosas con las que estoy más satisfecho en esta implementación es el modelo de filtros en dos niveles.

EndpointFilter: por grupo, se activa solo cuando coincide una ruta:

AEngine.Prefix('/admin')
  .Use(
    function(Ctx: TWebContext; Next: TMVCEndpointFilterNext): IMVCResponse
    begin
      if not IsAdmin(Ctx) then
        Result := Status(403, 'Forbidden')
      else
        Result := Next();  // continúa hacia el siguiente filtro o el handler
    end);

HTTPFilter: se registra a nivel de motor y envuelve toda la gestión de cada petición, antes incluso de que se produzca el enrutamiento, trabajando directamente sobre el transporte HTTP (Ctx.Request/Ctx.Response). Ideal para limitación de tasa, archivos estáticos, cabeceras de seguridad, compresión:

AEngine.UseHTTPFilter(
  procedure(Ctx: TWebContext; Next: TMVCHTTPFilterNext)
  begin
    Ctx.Response.SetCustomHeader('X-Frame-Options', 'DENY');
    Next();  // deja que la cadena continúe
  end);

Ambos tipos se componen en orden de registro. El orden es predecible, explícito y legible en un solo lugar.


No solo APIs: también aplicaciones web

La Minimal API no es solo JSON. Con .AsWeb, un grupo se convierte en una colección de rutas renderizadas en el servidor que devuelven HTML mediante TemplatePro. RenderView y ViewData funcionan como se espera, y la sesión es simplemente un filtro:

AEngine.Root.AsWeb
  .Use(MemorySession(10))
  .MapGet('/',
    function: IMVCResponse
    begin
      ViewData['title'] := 'Home';
      Result := RenderView('pages/home');
    end);

Las rutas de tipo rkWeb se excluyen automáticamente de la especificación OpenAPI: el framework sabe distinguir los endpoints de API de las páginas HTML.


Soporte en el asistente del IDE

El asistente no se ha quedado atrás. Encontrarás dos nuevos presets en el Repositorio de Objetos de Delphi:

  • Minimal API RESTful: API JSON pura, filtros por grupo, contenedor DI
  • Minimal API Web App: rutas .AsWeb, sesiones, login/logout, listo para HTMX

Un proyecto funcional, sin configuración, listo en 30 segundos.


Estado

La funcionalidad está disponible en la rama master a partir de la versión 3.5.0-silicon-rc3. Los 872 tests de integración pasan tanto en Win32 como en Win64, contra tres backends de servidor distintos (WebBroker, Indy Direct, HTTP.sys): 5.232 ejecuciones totales entre todas las combinaciones de plataforma y servidor.

Próximamente:

  • Comparativa de rendimiento entre la API clásica y la Minimal API (adelanto: los resultados me sorprendieron)
  • Integración con OpenAPI 3.x
  • Una guía dedicada con patrones del mundo real, filtros avanzados e integración con el contenedor DI

Daniele

Comments

comments powered by Disqus