Delphi Minimal API: APIs REST simples e rápidas com DMVCFramework
🇬🇧 English • 🇮🇹 Italiano • 🇪🇸 Español • 🇩🇪 Deutsch
APIs REST e aplicações web simples e rápidas em Delphi — rotas com métodos anônimos, sem classe controller, com DMVCFramework, o framework Delphi open-source mais popular.
Uma Nova Forma de Escrever APIs em Delphi com DMVCFramework
O DMVCFramework nasceu para resolver problemas “grandes” e complexos: aplicações de servidor de longa duração, com muitos endpoints, regras de negócio elaboradas e equipes que trabalham nelas por anos. É justamente por isso que o modelo controller-action sempre foi o coração do framework. Ele é robusto, testável, escalável e impõe uma estrutura clara à medida que o projeto cresce: é essa solidez que tornou o DMVCFramework o projeto open source mais difundido para criar soluções web em Delphi, e continuará sendo.
Mas, nos últimos anos, observei uma nova abordagem se somar ao ecossistema: FastAPI em Python, Minimal API no ASP.NET Core, Hono em TypeScript. Todos oferecem uma alternativa mais direta para certos cenários: microsserviços, protótipos, APIs simples.
Eu queria trazer a mesma escolha para o DMVCFramework. E funcionou.
O projeto agora tem mais de 50 contribuidores: uma comunidade crescente que torna possível trabalhar em funcionalidades ambiciosas como esta.
E não é apenas uma questão de código: a Minimal API já está integrada ao wizard da IDE, com dois novos templates no Object Repository do Delphi (veremos em detalhes mais adiante).
Olá, Mundo
Antes de tudo: quão mínima é?
AEngine.Root
.MapGet('/hello',
function: IMVCResponse
begin
Result := Ok('Hello, World!');
end);
Três linhas. Sem classe, sem atributo, sem arquivo separado.
O que ela retorna? O método anônimo tem tipo de retorno IMVCResponse, o objeto que descreve toda a resposta
HTTP (status, cabeçalhos, corpo). Ok(...) é uma factory que cria um com status 200 OK, e o corpo é
sempre um objeto JSON: ao passar uma string, ela é encapsulada no campo message. A resposta que
chega ao cliente é, portanto:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{"message":"Hello, World!"}
Já passando para Ok(...) um objeto ou uma lista, estes vão para o campo data do mesmo objeto JSON
({"data": ...}): é o mesmo mecanismo do modelo de controllers, aqui exposto por uma função em vez de
um método.
Como fica em um projeto real
Antes de ver o código, uma palavra sobre os filtros. Existem dois tipos: os EndpointFilter, que
se vinculam a um grupo de rotas específico via .Use(...), e os HTTPFilter, registrados no nível do
engine, que se aplicam a toda requisição independentemente do roteamento, trabalhando diretamente sobre o
transporte HTTP (Ctx.Request/Ctx.Response). Ambos são o equivalente conceitual do middleware clássico
do DMVCFramework, e vamos abordá-los em detalhes mais adiante. No exemplo abaixo usamos os primeiros: toda
rota do grupo passa por eles em ordem antes de chegar ao handler. Por ora, saiba apenas que .Use()
significa “aplicar este comportamento a tudo que vier a seguir”.
Aqui está uma API REST completa (autenticação JWT, logging, endpoints CRUD) em um único arquivo, sem nenhuma classe controller:
procedure RegisterRoutes(AEngine: TMVCEngine);
begin
// Health check público, sem filtros
AEngine.Root
.MapGet('/health',
function: IMVCResponse
begin
Result := Ok('OK');
end);
// Grupo /api/v1: autenticação e logging aplicados a todas as rotas
AEngine.Prefix('/api/v1')
.Use(RequestLoggingFilter())
.Use(BearerAuthFilter())
// Por convenção, toda interface (como IPeopleService) é resolvida pelo container de 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;
É isso. Sem [MVCPath], sem [MVCHTTPMethod], sem classe controller para declarar.
Aplicações grandes: a estrutura não desaparece, você a escolhe
A pergunta justa é: “e quando eu tiver 80 rotas?”
Nenhum framework de roteamento mínimo escala bem se tudo for colocado em um único RegisterRoutes.
A diferença é que, com a Minimal API do DMVC, a estrutura não é imposta pela sintaxe,
ela é imposta por você, da forma que fizer mais sentido para o seu domínio.
O padrão natural é dividir as rotas por área funcional, cada uma em seu próprio procedimento (ou unit), passando o 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: ponto único de montagem
procedure RegisterRoutes(AEngine: TMVCEngine);
var
lAPI: TMVCRouteGroup<TObject>;
begin
lAPI := AEngine.Prefix('/api/v1')
.Use(RequestLoggingFilter())
.Use(BearerAuthFilter());
RegisterPeopleRoutes(lAPI);
RegisterOrdersRoutes(lAPI);
end;
O resultado é uma estrutura em árvore explícita: filtros compartilhados ficam em um lugar, cada área funcional vive no seu próprio arquivo, e o ponto de montagem é uma lista de chamadas legível em dez segundos.
Com o modelo clássico de controllers, a estrutura existe mas é imposta pela sintaxe: cada recurso requer uma classe dedicada. Com a Minimal API ela é opcional: você pode começar com tudo em um arquivo e refatorar quando fizer sentido, sem reescrever nada.
Como funciona o binding de argumentos
Cada parâmetro do método anônimo é resolvido por tipo, automaticamente:
| Tipo do parâmetro | Origem |
|---|---|
TWebContext |
Contexto HTTP da requisição atual |
| Interface no container de DI | Serviço injetado |
| Classe | Corpo JSON (POST/PUT/PATCH) ou query string (GET/DELETE) |
Record com atributos [MVCFromBody] / [MVCFromQueryString] / [MVCFromContentField] |
Binding misto, campo a campo |
string, Integer, TGUID, TDateTime, … |
Parâmetro de rota se presente, caso contrário query string |
Não é necessário declarar a origem de cada parâmetro: o framework a infere a partir do tipo e do contexto.
Filtros: dois níveis, um pipeline
Uma das coisas de que mais me orgulho nesta implementação é o modelo de filtros em dois níveis.
EndpointFilter: por grupo, disparado apenas quando uma rota corresponde:
AEngine.Prefix('/admin')
.Use(
function(Ctx: TWebContext; Next: TMVCEndpointFilterNext): IMVCResponse
begin
if not IsAdmin(Ctx) then
Result := Status(403, 'Forbidden')
else
Result := Next(); // continua para o próximo filtro ou para o handler
end);
HTTPFilter: é registrado no nível do engine e envolve todo o tratamento de cada requisição, antes
mesmo de o roteamento acontecer, trabalhando diretamente sobre o transporte HTTP
(Ctx.Request/Ctx.Response). Ideal para rate limiting, arquivos estáticos, cabeçalhos de segurança,
compressão:
AEngine.UseHTTPFilter(
procedure(Ctx: TWebContext; Next: TMVCHTTPFilterNext)
begin
Ctx.Response.SetCustomHeader('X-Frame-Options', 'DENY');
Next(); // deixa o pipeline continuar
end);
Ambos os tipos se compõem na ordem de registro. A ordem é previsível, explícita e legível em um único lugar.
Não apenas APIs: também aplicações web
A Minimal API não é exclusiva para JSON. Com .AsWeb, um grupo se torna uma coleção de
rotas renderizadas no servidor que retornam HTML via TemplatePro. RenderView e ViewData
funcionam como esperado, e a sessão é apenas um filtro:
AEngine.Root.AsWeb
.Use(MemorySession(10))
.MapGet('/',
function: IMVCResponse
begin
ViewData['title'] := 'Home';
Result := RenderView('pages/home');
end);
Rotas do tipo rkWeb são automaticamente excluídas da especificação OpenAPI: o framework
sabe distinguir endpoints de API de páginas HTML.
Suporte no Wizard do IDE
O wizard não ficou de fora. Você encontrará dois novos presets no Object Repository do Delphi:
- Minimal API RESTful: API JSON pura, filtros por grupo, container de DI
- Minimal API Web App: rotas
.AsWeb, sessões, login/logout, pronto para HTMX
Um projeto funcional, zero configuração, pronto em 30 segundos.
Status
A funcionalidade está disponível no branch master a partir da versão 3.5.0-silicon-rc3.
Todos os 872 testes de integração passam tanto em Win32 quanto em Win64, em três backends de servidor distintos
(WebBroker, Indy Direct, HTTP.sys): 5.232 execuções totais entre todas as combinações de plataforma e servidor.
Em breve:
- Benchmark API Clássica vs Minimal API (spoiler: os resultados me surpreenderam)
- Integração com OpenAPI 3.x
- Um guia dedicado com padrões do mundo real, filtros avançados e integração com o container de DI
Daniele
Comments
comments powered by Disqus