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

Perguntas frequentes

O que é a Minimal API no DMVCFramework? A Minimal API é uma forma de criar APIs REST e aplicações web em Delphi definindo rotas como métodos anônimos (function: IMVCResponse) em vez de classes controller. Faz parte do DMVCFramework 3.5 e funciona como o FastAPI em Python ou a Minimal API no ASP.NET Core. As rotas são registradas com MapGet, MapPost, MapPut e MapDelete.

Preciso de uma classe controller para criar uma API REST em Delphi? Não. Com a Minimal API do DMVCFramework você registra rotas diretamente com métodos anônimos, sem atributos [MVCPath] ou [MVCHTTPMethod] e sem classe controller. O modelo clássico de controllers continua disponível: a Minimal API é uma abordagem adicional e opcional, ideal para microsserviços, protótipos e APIs simples.

Como os serviços e os parâmetros de rota são vinculados na Minimal API do DMVCFramework? Os parâmetros são resolvidos por tipo: interfaces a partir do container de injeção de dependência, classes a partir do corpo JSON ou da query string, e tipos escalares (string, Integer, TGUID, TDateTime) a partir do parâmetro de rota se presente, caso contrário da query string. Os nomes dos parâmetros de rota são comparados sem diferenciar maiúsculas, pois Delphi é uma linguagem case-insensitive.

A Minimal API do DMVCFramework pode renderizar aplicações web HTML, não apenas JSON? Sim. Ao chamar .AsWeb em um grupo, ele 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. Rotas do tipo rkWeb são automaticamente excluídas da especificação OpenAPI.

Qual é a diferença entre um EndpointFilter e um HTTPFilter? Um EndpointFilter é vinculado a um grupo de rotas específico com .Use(...) e é disparado apenas quando uma rota corresponde. Um HTTPFilter é registrado no nível do engine e envolve cada requisição antes do roteamento, trabalhando diretamente sobre o transporte HTTP (Ctx.Request/Ctx.Response). Ambos são o equivalente conceitual do middleware clássico do DMVCFramework.

Qual versão do Delphi e do DMVCFramework é necessária para a Minimal API? A Minimal API está disponível no branch master do DMVCFramework a partir da versão 3.5.0-silicon-rc3. Todos os 872 testes de integração passam tanto em Win32 quanto em Win64 contra três backends de servidor: WebBroker, Indy Direct e HTTP.sys.

🎁 O guia completo de Minimal API, de graça

Aquele «guia dedicado com padrões do mundo real, filtros avançados e integração com o container de DI» mencionado logo acima? Os apoiadores AllAccess+ ganham de graça, no dia do lançamento, sem nenhum custo adicional.

E não para por aí. Sua assinatura libera toda a biblioteca de conteúdo premium sobre Delphi: TemplatePro, DotEnv, Job Queues com FirebirdSQL, Prompt Engineering, MVCActiveRecord e muito mais material inédito e normalmente pago, tudo incluído, além de suporte prioritário e acesso antecipado a novos recursos.

Torne-se apoiador AllAccess+ →

Daniele

Comments

comments powered by Disqus