
Última atualização em 2 de setembro de 2022 por Willian Tuttoilmondo
Recapitulando
Nos episódios anteriores, vimos como criar, implementar, disponibilizar e testar APIs RESTful com Delphi e DataSnap. Em cada uma dessas etapas trabalhamos com um aspecto do nosso projeto, como sua concepção, arquitetura, implementação do código, tratamento básico de erros, deploy e testes. Agora chegou a hora de vermos como podemos ampliar nossa API através da implementação de novas classes, assim como implementar a aceitação de um novo método HTTP para a execução de métodos em nossa API.
Como estamos na parte IV de nosso artigo, é interessante estar a par das partes anteriores. Se esta é a primeira leitura da série, recomento que você volte até o início de tudo, onde definimos a arquitetura utilizando padrões de projeto, os quais são a base desse projeto. Se você já nos acompanha há algum tempo, vamos seguir em frente e ver como essa arquitetura pode realmente facilitar nossa vida e proporcionar inúmeras vantagens para nossos projetos futuros.
APIs RESTful com Delphi e DataSnap – Adicionando classes ao projeto
Como temos uma estrutura estabelecida de classes que foram utilizadas em nosso projeto, usamos como base uma Abstract Factory que instancia objetos derivados da classe TOperacao: TSoma, TSubtracao, TMultiplicacao e TDivisao. Como implementação adicional, criaremos duas novas classes: TPotencia e TRaiz. Com elas será possível efetuar os cálculos de ab e b√a. Estes são cálculos aritméticos simples, portanto, sem muitos problemas a serem desenvolvidos em nossa API. Abaixo é possível ver o código destas duas classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | unit Classes.Potencia; interface uses Classes.Operacao; type TPotencia = class(TOperacao) public function Efetuar: Extended; override; end; implementation uses Classes.Factory, System.Math; { TPotencia } function TPotencia.Efetuar: Extended; begin Result := Power(A, B); end; initialization TFactory.Instance.RegisterClass('potencia', TPotencia); end. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | unit Classes.Raiz; interface uses Classes.Operacao; type TRaiz = class(TOperacao) public function Efetuar: Extended; override; end; implementation uses Classes.Factory, System.Math; { TRaiz } function TRaiz.Efetuar: Extended; begin Result := Power(A, 1/B); end; initialization TFactory.Instance.RegisterClass('raiz', TRaiz); end. |
Como vimos, a implementação destas classes é extremamente simples, assim como as demais classes do projeto, mas na classe TRaiz temos um truque matemático. Como não temos um método nativo que calcule b√a, precisamos nos valer de sua equivalência matemática. Como aprendemos lá atrás, ainda na escola, um número x elevado a uma potência 1/y (x1/y) é equivalente a y√x, o que resolve nosso problema. Sendo assim, a linha Result := Power(A, 1/B); nos trará o resultado da raiz b de a.
Criadas e salvas as classes, basta adicioná-las ao projeto – uma vez que elas só serão registradas na Abstract Factory se isso for feito – e recompilá-lo para que estas classes passem a fazer parte da nossa API. Para atualizar nosso módulo no servidor Apache, antes de disponibilizar o novo arquivo em sua estrutura, é preciso parar o servidor. Através da tela inicial do Laragon, se você seguiu os passos do artigo anterior, basta clicar em Parar e aguardar que todos os serviços agregados parem. Assim que isto acontecer, basta substituir o arquivo do módulo pelo novo arquivo compilado e comandar novamente o início dos serviço para ver a mágica acontecer.

Após realizarmos a atualização do nosso módulo, efetuamos o teste do cálculo da raiz 5 de 32 (5√32) através da rota https://localhost/api/dscalculator/v1/raiz?a=32&b=5, o que nos trouxe um resultado igual a 2, uma vez que 25 é igual a 32, o que pode ser comprovado pela nossa API através da rota https://localhost/api/dscalculator/v1/potencia?a=2&b=5.
Adicionando novos métodos HTTP à API
Segundo a norma RFC 7231, nós temos à nossa disposição outros métodos HTTP além do método GET que podem ser utilizados, sendo os principais: POST, PUT, PATCH e DELETE. Através deles é possível identificar qual operação será realizada em uma API sem a necessidade de descrevê-la em sua rota. Com base nesta mesma norma, podemos entender que utilizaremos:
- GET quando estamos pesquisando itens em nossa API ou executando operações que permitam uma URI que não ultrapassem o limite de 256 caracteres;
- POST quando queremos incluir um item em nossa API ou executando operações complexas onde o limite de 256 caracteres da URI é ultrapassado;
- PUT quando queremos substituir completamente um item em nossa API;
- PATCH quando queremos atualizar algumas informações em um item de nossa API e;
- DELETE quando queremos excluir um ou mais itens de nossa API.
Sendo assim vamos, apenas a título de entendimento, adicionar o método HTTP POST à nossa API para entendermos como trabalharemos com outros métodos além de GET. Para podermos começar, vamos implementar um método restrito ao método WebModule1DefaultHandlerAction chamado DoPOSTOperation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | procedure DoPOSTOperation; begin try // Criando o objeto JSON que tratará a requisição with TJSONObject.ParseJSONValue(Request.Content) as TJSONObject do begin // Verificando se ele possui os nós a e b if not (HasProperty('a') and HasProperty('b')) then begin SetErrorResponse('Parâmetro a ou b não encontrado.'); Free; Exit; end; // Verificando se os nós são valores numéricos if not ((GetValue('a').IsInteger or GetValue('a').IsFloat) and (GetValue('b').IsInteger or GetValue('b').IsFloat)) then begin SetErrorResponse('Parâmetros a e b devem conter valores numéricos.'); Free; Exit; end; // Atribuindo valores às propriedades do objeto objOperacao.A := GetValue('a').AsFloat; objOperacao.B := GetValue('b').AsFloat; // Liberando o objeto JSON da memória Free; end; except SetErrorResponse('O corpo da requisição não possui um objeto JSON válido.'); Exit; end; // Criando o resultado with TJSONObject.Create do try try AddFloatPair('result', objOperacao.Efetuar); Response.Content := Minify; Response.StatusCode := 200; except on E: Exception do begin case E is EZeroDivide of True : raise Exception.Create('O valor de b não pode ser 0 em uma divisão.'); False: raise; end; end; end; finally Free; end; end; |
Com este método implementado, podemos realizar uma rápida alteração no método WebModule1DefaultHandlerAction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | try try // Verificando o método da requisição case Request.MethodType of // Executando operações com método GET mtGet : DoOperation; // Executando operações com método POST mtPost: DoPOSTOperation; else // Enviando resposta caso outro método seja solicitado SetErrorResponse('Método não suportado.', 405 {Method not allowed}); end; except // Em caso de erro... on E: Exception do begin SetErrorResponse(E.Message, 500); end; end; finally objOperacao.DisposeOf; end; |
Sendo assim, nossa unit WebModule.Container fica com o seguinte código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | unit WebModule.Container; interface uses System.Classes, Web.HTTPApp, IPPeerServer, Datasnap.DSCommonServer, Datasnap.DSHTTP, Datasnap.DSHTTPWebBroker, Datasnap.DSServer; type TdmDSCalculator = class(TWebModule) hwdDSCalculator: TDSHTTPWebDispatcher; dssDSCalculator: TDSServer; procedure WebModule1DefaultHandlerAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); private { Private declarations } public { Public declarations } end; var WebModuleClass: TComponentClass = TdmDSCalculator; implementation {%CLASSGROUP 'System.Classes.TPersistent'} {$R *.dfm} uses System.SysUtils, Web.WebReq, System.JSON, Helpers.JSONObject, Helpers.JSONValue, Helpers.Str, Helpers.Integer, Classes.Factory, Classes.Operacao; procedure TdmDSCalculator.WebModule1DefaultHandlerAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var strPathInfo: string; bufPathInfo: TArray<string>; strClass : string; objOperacao: TOperacao; procedure SetErrorResponse(const AMessage: string; const AStatusCode: Integer = 400 {Bad Request}); begin with TJSONObject.Create do begin AddBooleanPair('sucesso', False); AddStringPair('mensagem', AMessage); Response.Content := Minify; Response.StatusCode := AStatusCode; Free; end; end; function GetQueryField(const AField: string): string; var strField: string; bufField: TArray<string>; i : Integer; begin Result.Clear; for i := 0 to Request.QueryFields.Count.Pred do begin strField := Request.QueryFields[i]; if strField.Pos(AField + '=') <> 1 then begin Continue; end; bufField := strField.Split('='); Result := bufField[1]; Break; end; end; procedure DoOperation; var strA: string; strB: string; begin // Verifica se os parÂmetros foram passados corretamente if Request.QueryFields.Text.IsEmpty then begin SetErrorResponse('Nenhum parâmetro foi atribuído na rota.'); Exit; end; // Lendo os campos na Query String da rota strA := GetQueryField('a'); strB := GetQueryField('b'); // Verificando se os campos estão vazios ou não foram encontrados if strA.IsEmpty or strB.IsEmpty then begin SetErrorResponse('Parâmetro a ou b não encontrado.'); Exit; end; // Verificando se os campos são valores numéricos if not ((strA.IsNumbersOnly or strA.IsFloat) and (strB.IsNumbersOnly or strB.IsFloat)) then begin SetErrorResponse('Parâmetros a e b devem conter valores numéricos.'); Exit; end; // Atribuindo valores às propriedades do objeto objOperacao.A := strA.ToFloat; objOperacao.B := strB.ToFloat; // Criando o resultado with TJSONObject.Create do try try AddFloatPair('result', objOperacao.Efetuar); Response.Content := Minify; Response.StatusCode := 200; except on E: Exception do begin case E is EZeroDivide of True : raise Exception.Create('O valor de b não pode ser 0 em uma divisão.'); False: raise; end; end; end; finally Free; end; end; procedure DoPOSTOperation; begin try // Criando o objeto JSON que tratará a requisição with TJSONObject.ParseJSONValue(Request.Content) as TJSONObject do begin // Verificando se ele possui os nós a e b if not (HasProperty('a') and HasProperty('b')) then begin SetErrorResponse('Parâmetro a ou b não encontrado.'); Free; Exit; end; // Verificando se os nós são valores numéricos if not ((GetValue('a').IsInteger or GetValue('a').IsFloat) and (GetValue('b').IsInteger or GetValue('b').IsFloat)) then begin SetErrorResponse('Parâmetros a e b devem conter valores numéricos.'); Free; Exit; end; // Atribuindo valores às propriedades do objeto objOperacao.A := GetValue('a').AsFloat; objOperacao.B := GetValue('b').AsFloat; // Liberando o objeto JSON da memória Free; end; except SetErrorResponse('O corpo da requisição não possui um objeto JSON válido.'); Exit; end; // Criando o resultado with TJSONObject.Create do try try AddFloatPair('result', objOperacao.Efetuar); Response.Content := Minify; Response.StatusCode := 200; except on E: Exception do begin case E is EZeroDivide of True : raise Exception.Create('O valor de b não pode ser 0 em uma divisão.'); False: raise; end; end; end; finally Free; end; end; begin // Recebendo o PathInfo constante na Request strPathInfo := Request.InternalPathInfo; // Eliminando o primeiro caracter / do PathInfo Delete(strPathInfo, 1, 1); // Dividindo o PathInfo em partes para idenificar a classe solicitada bufPathInfo := strPathInfo.Split('/'); // Atribuindo o Content-Type da resposta como padrão para todas elas Response.ContentType := 'application/json'; // Verificando se o comprimento do caminho está correto // Se não estiver, emita uma mensagem de erro e saia if not bufPathInfo.Count.Equals(3) then begin SetErrorResponse('Caminho para a solicitação inválido.'); Exit; end; // Verificando se a versão da API é a mesma da constante na rota // Se não for, emita uma mensagem de erro e saia if not bufPathInfo[1].Equals('v1') then begin SetErrorResponse('Versão inválida da API.'); Exit; end; strClass := bufPathInfo[2]; // Verificando se a classe chamada na rota está registrada na factory // Se não estiver, emita uma mensagem de erro e saia if not TFactory.Instance.Operacoes.Keys.ToArray.Contains(strClass) then begin SetErrorResponse('Classe %s não encontrada.'.Format([strClass])); Exit; end; // Instanciando um objeto TOperação com base na solicitação objOperacao := TFactory.Instance.GetOperacao(strClass); try try // Verificando o método da requisição case Request.MethodType of // Executando operações com método GET mtGet : DoOperation; // Executando operações com método POST mtPost: DoPOSTOperation; else // Enviando resposta caso outro método seja solicitado SetErrorResponse('Método não suportado.', 405 {Method not allowed}); end; except // Em caso de erro... on E: Exception do begin SetErrorResponse(E.Message, 500); end; end; finally objOperacao.DisposeOf; end; end; initialization finalization Web.WebReq.FreeWebModules; end. |
Testando a nova implementação
Com a conclusão desta etapa, podemos testar seu funcionamento. Como sugeri no artigo anterior, para o teste dos outros métodos que utilizaremos além do método GET, precisaremos usar o Postman, o qual pode ser obtido aqui. Como os demais testes que demonstrei aqui foram realizados com ele, o próximo também será. Como implementamos o método POST em nossa API, neste teste, a rota fica um pouco diferente.

Agora, para a utilização pelo método POST, nossa rota é mais curta, terminando no nome da classe (https://localhost/api/dscalculator/v1/raiz). Contudo, para que esta rota seja válida, precisamos indicar que o método utilizado é o POST, como pode ser visto na imagem, e precisamos adicionar um corpo à requisição, o qual indicamos na aba Body, selecionando a opção raw e indicando o Content-Type como JSON. Além disso, o objeto JSON nesta requisição deve obedecer certos padrões que são requisitos de nossa API. Para isso, ele deve:
- conter os nós a e b e;
- os nós a e b devem conter valores numéricos.
Ao enviarmos o objeto JSON à API, esta faz validações básicas, como a consistência do objeto em si, a existência dos nós requeridos e se o valor destes é numérico. Assim como fizemos nos primeiros testes, vou colocar no objeto JSON um nó com valor não-numérico e enviá-lo à API. Se tudo correr bem, ela tratará o erro e nos devolverá uma mensagem amigável como retorno.

Próximos passos
Nesta etapa encerramos o desenvolvimento de APIs RESTful com Delphi e DataSnap. Este é um trabalho inacabado, uma vez que, propositalmente, alguns pontos do código, apesar de funcionais, podem ser melhorados em aspectos de boas práticas e princípios de Clean Code. Futuramente discutiremos estes assuntos, trazendo uma abordagem mais profunda destas melhorias utilizando o código deste projeto como base.
Espero que vocês tenham gostado do resultado final e que possam, de hoje em diante, repensar a forma como se desenvolvem novas aplicações. Um abraço e até o próximo artigo!
Ah, e o código fonte deste projeto pode ser baixado, na íntegra, aqui!
Muito bom o artigo. Tem chance de dar continuada com um módulo com retorno de dados de uma base de saída?
Opa!
Obrigado pelo retorno!
Está planejada uma sequência de artigos abordando bases de dados, uso de interfaces, melhoria de desempenho com Redis e deploy de aplicações com Docker (servidor de aplicações, base de dados e Redis), mas o que falta é tempo para desenvolver os artigos.
Mas fique ligado. Logo saem as novidades.
Abraço.
Opa. Dá uma olhada no último artigo. Acho que vai te interessar…