Become a member!

Delphi Minimal API: APIs REST simples e rápidas com DMVCFramework

🌐
Este artigo também está disponível em outros idiomas:
🇬🇧 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).

Os presets Minimal API RESTful e Minimal API WebApp no Object Repository do Delphi

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