Delphi Minimal API: APIs REST simples y rápidas con DMVCFramework
🇬🇧 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).
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