
Última atualização em 10 de outubro de 2024 por Willian Tuttoilmondo
APIs RESTful e Delphi – Um casamento perfeito
Quem acompanha meus artigos há algum tempo já deve ter lido sobre vários assuntos. Já falei sobre multi-tenacidade e chaves primárias, padrões de projetos, class helpers, APIs RESTful, verticalização de dados, registro de logs em modo thread safe, interfaces e HATEOAS, de maneira bem didática, com um único propósito: chegarmos até aqui, onde veremos como aplicar todos estes conhecimentos para construir APIs RESTful dinâmicas e funcionais, onde a codificação será a menor possível e o resultado será uma API RESTful CRUD plenamente funcional.
Como já disse na série de artigos sobre APIs RESTful com Delphi e Datasnap – as partes I, II, III e IV podem ser lidas individualmente -, e que servirão de base para este projeto, uma versão Enterprise ou Architect do Embarcadero Delphi é necessária. Até o momento do fechamento deste artigo, a versão disponibilizada é a Athens 12.2. Se já temos tudo o que precisamos, podemos começar. Mas, antes, é sempre bom deixar as coisas bem claras.
Delphi é uma linguagem, ao contrário do imaginário popular, que implementa fortemente o paradigma de desenvolvimento orientado a objetos. O que comumente vemos são projetos RAD, muitos desenvolvidos há 20 ou 30 anos, que nunca passaram por um processo de modernização e foram sendo “evoluídos” utilizando as piores práticas possíveis. O mais comum é que eles sejam desenvolvidos em uma camada muito superficial da orientação a objetos, onde os desenvolvedores dropavam componentes visuais e não visuais em um formulário ou datamodule e usavam os eventos destes componentes para desenvolver as regras de negócio e persistência da aplicação.
Esse cenário gerou aplicações onde as regras de negócio se fragmentam e onde o acoplamento entre componentes acaba sendo o fator mais custoso durante sua manutenção. Com isso, empresas com produtos desenvolvidos desta forma no mercado têm pouca margem de manobra para mudanças mais profundas, o que acaba sucateando o próprio produto. A opção, muitas vezes, é reconstruir tudo, e geralmente com outra tecnologia, desperdiçando todo o potencial da equipe e todo o capital intelectual acumulado.
Mesmo que o Delphi tenha nascido da orientação a objetos, sua finada criadora insistia em divulgar amplamente o conceito RAD, o que trouxe popularidade entre as décadas de 1990 e 2000,mas que hoje tem sido motivo de piada no meio do desenvolvimento. Tanto é que há muita gente que acredita, ainda, que não existe orientação a objetos em Delphi e que, por isso, é uma tecnologia morta e ultrapassada. Neste ponto, estou aqui para desmentir esta afirmação, afinal, com quase 30 anos de experiência, acompanhei a evolução do Delphi e evoluí com ele, ao contrário de muitos outros desenvolvedores, ainda presos ao modelo RAD, aos eventos e a todas as práticas que, há anos, o mercado luta para extirpar.
“Delphi é uma ferramenta poderosa, versátil e completa, capaz de construir projetos seguros e confiáveis. Mas você precisa entender uma coisa: ele deve ser usado para aquilo que se propõe. Eu não usaria Delphi para construir um sistema web, assim como não usaria uma colher para bater num prego.”
Construir APIs RESTful com Delphi e Datasnap é a prova de que, sim, a ferramenta continua relevante e, mais do que isso, robusta, entregando um produto final com qualidade e que pode ser perfeitamente escalável. Basta escolher a arquitetura e abordagem mais adequadas. Mas, como tempo é dinheiro e eu não tenho nenhum dos dois, vamos ao que interessa.
APIS RESTful – Começando pelo começo
Após criarmos nosso módulo Apache onde nossas APIs RESTful ficarão abrigadas – se você não sabe como fazer isso, volte aqui e veja como -, chegou a hora de trabalhar. Como vimos no artigo sobre o modelo de maturidade de Richardson para APIs RESTful, é importante definir o comportamento inicial da nossa API. Isso significa definir como os endpoints serão apresentados, e isso definirá, inclusive, como modelaremos as respostas às consultas feitas à própria API.
Então, vale lembrar, como uma API CRUD, nossa função é realizar as “quatro operações básicas”, sem muita complexidade, portanto, não teremos ações de recursos sendo executadas aqui. Isso simplificará muito as coisas e, num futuro não tão distante, evoluiremos nosso projeto para abrigar ações mais complexas. Mas, agora,antes de voar, vamos aprender a caminhar.
Sendo assim, como definição minha, nosso endpoint raiz será http[s]://{servidor}[:porta]/api/devspace/cadastro/v1, onde adicionaremos o nome do recurso ao fim do endpoint para interagir diretamente com os recursos que deixaremos disponíveis para nosso projeto.
Outro ponto importante é: o nome dos recursos deve, sempre, ser criado em letras minúsculas e sem caracteres especiais, acentuados ou iniciando com números. É mais natural, em uma URI, vermos algo como /api/devspace/cadastro/v1/endereco ao invés de /api/devspace/cadastro/v1/Endereço. Além de uma boa prática, manter os nomes dos recursos em minúsculo ajuda na codificação, evitando a necessidade e decodificá-la para entender qual é o recurso acessado.
Criando os recursos para uso nas APIs RESTful
Como vimos no artigo sobre verticalização de dados, vamos criar, essencialmente, duas tabelas em uma base PostgreSQL e partiremos destas duas tabelas para criar nossas APIs RESTful. Assim, vamos seguir esse roteirinho maneiro e bem rápido para criar tudo que vamos precisar.
Aqui, vamos criar o usuário para o banco de dados e o próprio banco de dados.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | -- Criando o usuário devspace CREATE ROLE devspace WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE INHERIT NOREPLICATION NOBYPASSRLS CONNECTION LIMIT -1 PASSWORD 'devspace@123'; -- Criando o banco de dados devspace CREATE DATABASE devspace WITH OWNER = devspace ENCODING = 'UTF8' LC_COLLATE = 'pt_BR.UTF-8' LC_CTYPE = 'pt_BR.UTF-8' LOCALE_PROVIDER = 'libc' TABLESPACE = pg_default CONNECTION LIMIT = -1 IS_TEMPLATE = False; |
Em seguida, vamos preparar o banco de dados para trabalhar com UUIDs como chaves primárias e criar o schema que abrigará os cadastros das nossas APIs RESTful.
1 2 3 4 5 | -- Criando a extensão para funções com uuid CREATE EXTENSION "uuid-ossp"; -- Criando o schema cadastro no banco de dados devspace CREATE SCHEMA cadastro AUTHORIZATION devspace; |
E, por fim, as tabelas.
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 | -- Criando a tabela recurso no schema cadastro CREATE TABLE cadastro.recurso ( id uuid NOT NULL DEFAULT uuid_generate_v4(), nome character varying(32) NOT NULL, ativo boolean NOT NULL DEFAULT true, PRIMARY KEY (id) ); ALTER TABLE IF EXISTS cadastro.recurso OWNER to devspace; -- Criando a tabela registro no schema cadastro CREATE TABLE cadastro.registro ( id uuid NOT NULL DEFAULT uuid_generate_v4(), recurso uuid NOT NULL, descricao character varying(64) NOT NULL, atributos jsonb NOT NULL, ativo boolean NOT NULL DEFAULT true, PRIMARY KEY (id), CONSTRAINT fk_recurso_registro FOREIGN KEY (recurso) REFERENCES cadastro.recurso (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT NOT VALID ); ALTER TABLE IF EXISTS cadastro.registro OWNER to devspace; |
Com tudo criado, vamos criar alguns recursos que poderemos usar para nosso CRUD.
1 2 3 | insert into cadastro.recurso values (uuid_generate_v4(), 'cidade'); insert into cadastro.recurso values (uuid_generate_v4(), 'moeda'); insert into cadastro.recurso values (uuid_generate_v4(), 'veiculo'); |
E, checando o que acabamos de fazer, chegamos nisso:

Só não esqueçam do seguinte: quando o cadastro é efetuado sem especificar um UUID, a função uuid_generate_v4() se encarregará de criar um novo, portanto, os valores do campo id serão diferentes dos que aparecem na imagem.
Criando a estrutura para as APIs RESTful
Com o banco de dados criado e alguns recursos cadastrados, podemos começar a colocar a mão na massa. Para criar APIs RESTful dinâmicas, vamos partir do seguinte princípio: uma aplicação do tipo Datasnap REST Application, como módulo para o webserver Apache. Tudo muito simples e rápido. Após todo o processo que já vimos aqui, chegaremos nesse resultado:

Além desses dois carinhas aí, hoje, vamos inserir mais alguns componentes que vamos utilizar em nossas APIs RESTful: uma conexão com o banco de dados, seu devido driver, seu devido complemento exigido por ela e uma consulta onde executaremos as ações pertinentes, chegando nisso:

A conexão com o banco de dados, logicamente, deve ser configurada para apontar para o banco de dados PostgreSQL criado para este projeto. Como geralmente isso é feito na mesma máquina em que se desenvolve, a configuração apontará para localhost, assim como nossas APIs RESTful também serão direcionadas para este banco de dados. Portanto, todo o conjunto de banco de dados e o servidor web Apache devem estar no mesmo local para que nenhuma mudança seja necessária.
Criando APIs RESTful dinâmicas
Agora, vamos nos concentrar naquilo que realmente interessa: a codificação. É importante entender que, como em todo projeto que desenvolvo, duas características são muito marcantes. A primeira é a utilização massiva dos princípios de Clean Code, dando nomes aos métodos, variáveis e constantes que realmente façam sentido para a compreensão do código. A segunda é o cuidado com o nível de toxicidade do código gerado, fazendo com que ele seja um código mais simples de efetuar a manutenção futuramente.
Quando verificarmos o resultado final da codificação, teremos uma visão muito clara disso.
Codificando APIs RESTful dinâmicas
Basicamente, vamos organizar as coisas da seguinte forma: uma unit com constantes para as consultas, outra com as constantes para os nós de objetos JSON e uma terceira para as constantes das mensagens que serão adicionadas às respostas 400, 404 e 500. Sendo assim, nossas units serão:
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 | unit Cadastro.Consts.SQL; interface const SQL_SEL_RECURSO = 'select count(1) ' + ' from cadastro.recurso' + ' where nome = %s '; SQL_SEL_REGISTRO = 'select reg.id::text ' + ' , reg.atributos::text ' + ' from cadastro.registro reg ' + ' inner join cadastro.recurso rec on rec.id = reg.recurso' + ' where rec.nome = %s '; SQL_SEL_REGISTRO_POR_ID = 'select reg.atributos::text ' + ' from cadastro.registro reg ' + ' inner join cadastro.recurso rec on rec.id = reg.recurso' + ' where rec.nome = %s ' + ' and reg.id = %s '; SQL_INS_REGISTRO = 'insert into cadastro.registro values (uuid_generate_v4() ' + ' , (select id ' + ' from cadastro.recurso' + ' where nome = %s) ' + ' , %s ' + ' , %s) ' + 'returning id::text '; SQL_UPD_REGISTRO = 'update cadastro.registro ' + ' set atributos = %s ' + ' where id = %s ' + ' and recurso = (select id ' + ' from cadastro.recurso' + ' where nome = %s) '; SQL_DEL_REGISTRO = 'delete ' + ' from cadastro.registro ' + ' where id = %s ' + ' and recurso = (select id ' + ' from cadastro.recurso' + ' where nome = %s) '; implementation end. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | unit Cadastro.Consts.JSON; interface const NO_SUCESSO = 'sucesso'; NO_MENSAGEM = 'mensagem'; NO_DETALHE = 'detalhe'; NO_QTDE = 'qtde'; NO_ITENS = 'itens'; NO_DESCRICAO = 'descricao'; NO_ATRIBUTOS = 'atributos'; NO_ID = 'id'; NO_RELACAO = 'rel'; NO_TIPO_ACAO = 'type'; NO_HREF = 'href'; NO_ACOES = 'acoes'; implementation end. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | unit Cadastro.Consts.Respostas; interface const RECURSO_NAO_DEFINIDO = 'Recurso não definido'; RECURSO_NAO_ENCONTRADO = 'Recurso não encontrado'; ERRO_PROCESSAMENTO = 'Erro ao processar requisição.'; METODO_NAO_PERMITIDO = 'Método não permitido. Use PUT ao invés de PATCH.'; ID_NAO_FORNECIDO = 'Id não fornecido.'; REGISTRO_NAO_ENCONTRADO = 'Registro não encontrado.'; JSON_INVALIDO = 'Conteúdo da requisição inválido.'; implementation end. |
Já a unit do nosso datamodule ficará desta forma:
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 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | unit Cadastro.Datamodules.Container; interface uses System.SysUtils, System.Classes, Web.HTTPApp, Datasnap.DSHTTPCommon, Datasnap.DSHTTPWebBroker, Datasnap.DSServer, Web.WebFileDispatcher, Web.HTTPProd, DataSnap.DSAuth, Datasnap.DSProxyJavaScript, IPPeerServer, Datasnap.DSMetadata, Datasnap.DSServerMetadata, Datasnap.DSClientMetadata, Datasnap.DSCommonServer, Datasnap.DSHTTP, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.PG, FireDAC.Phys.PGDef, FireDAC.ConsoleUI.Wait, FireDAC.Comp.UI, Data.DB, FireDAC.Comp.Client, System.JSON, Cadastro.Consts.JSON, Cadastro.Consts.Respostas, Cadastro.Consts.SQL, FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt, FireDAC.Comp.DataSet; type TContainer = class(TWebModule) hwdServer: TDSHTTPWebDispatcher; dssServer: TDSServer; fdcServer: TFDConnection; gwcServer: TFDGUIxWaitCursor; pdlServer: TFDPhysPgDriverLink; qryServer: TFDQuery; procedure ContainerDefaultAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); end; var WebModuleClass: TComponentClass = TContainer; implementation {%CLASSGROUP 'System.Classes.TPersistent'} {$R *.dfm} uses Web.WebReq; { TContainer } procedure TContainer.ContainerDefaultAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var PathInfo: TArray<string>; Recurso: string; Id: string; URL: string; procedure Resposta201(const AId: string); begin Response.StatusCode := 201; with TJSONObject.Create do begin AddPair(NO_SUCESSO, TJSONBool.Create(True)); AddPair(NO_ID, AId); Response.Content := ToString; Free; end; end; procedure Resposta400(const AMensagem: string); begin Response.StatusCode := 400; with TJSONObject.Create do begin AddPair(NO_SUCESSO, TJSONBool.Create(False)); AddPair(NO_MENSAGEM, AMensagem); Response.Content := ToString; Free; end; end; procedure Resposta404; begin Response.StatusCode := 404; with TJSONObject.Create do begin AddPair(NO_SUCESSO, TJSONBool.Create(False)); AddPair(NO_MENSAGEM, REGISTRO_NAO_ENCONTRADO); Response.Content := ToString; Free; end; end; procedure Resposta405; begin Response.StatusCode := 405; with TJSONObject.Create do begin AddPair(NO_SUCESSO, TJSONBool.Create(False)); AddPair(NO_MENSAGEM, METODO_NAO_PERMITIDO); Response.Content := ToString; Free; end; end; procedure ExecutaAcaoDelete; begin // Executa a exclusão baseada no Id fornecido // Se afetou uma linha, retorna uma resposta 204 (No content) // Se não afetou nenhuma, retrna uma resposta 404 (Not found) case qryServer.ExecSQL(Format(SQL_DEL_REGISTRO, [Id.QuotedString, Recurso.QuotedString])) = 0 of True : Resposta404; False: Response.StatusCode := 204; end; end; procedure ExecutaConsultaHead; begin // Executa uma consulta baseada no Id fornecido qryServer.Open(Format(SQL_SEL_REGISTRO_POR_ID, [Recurso.QuotedString, Id.QuotedString])); // Se encontrou o registro, retorna uma resposta 200 (Sucesso) // Se não encontrou, retrna uma resposta 404 (Not found) case qryServer.RecordCount = 0 of True : Resposta404; False: Response.StatusCode := 200; end; end; procedure ExecutaAcaoPut; begin // Executa o update baseado no Id fornecido // Se afetou uma linha, retorna uma resposta 202 (Accepted) // Se não afetou nenhuma, retrna uma resposta 404 (Not found) case qryServer.ExecSQL(Format(SQL_UPD_REGISTRO, [Request.Content.QuotedString, Id.QuotedString, Recurso.QuotedString])) = 0 of True : Resposta404; False: Response.StatusCode := 202; end; end; procedure VerificaId; begin // Verifica se o Id foi fornecido na URL // Se sim, invoca a ação correspondente ao verbo // Se não, retorna uma mensagem com código 400 (Bad request) case Length(PathInfo) >= 6 of True : begin Id := PathInfo[5]; case Request.MethodType of mtPut : ExecutaAcaoPut; mtHead : ExecutaConsultaHead; mtDelete: ExecutaAcaoDelete; end; end; False: Resposta400(ID_NAO_FORNECIDO); end; end; procedure ExecutaAcaoPost; var Body: TJSONObject; Descricao: string; Atributos: TJSONObject; begin Body := nil; // Trata o corpo da requisição, obtendo as informações do objeto JSON fornecido try try // Converte o corpo em um objeto JSON Body := TJSONObject.ParseJSONValue(Request.Content) as TJSONObject; // Lê o nó descrição Descricao := Body.GetValue(NO_DESCRICAO).ToString.Replace('"', ''); // Lê o nó atributos Atributos := Body.GetValue(NO_ATRIBUTOS) as TJSONObject; except on E: Exception do begin // Caso haja algum problema com o JSON, retorna uma resposta 400 (Bad request) Resposta400(JSON_INVALIDO); Exit; end; end; // Insere a informação no banco de dados // A descrição é inserida como recurso.descricao qryServer.Open(Format(SQL_INS_REGISTRO, [Recurso.QuotedString, Recurso.Join('.', [Descricao]).QuotedString, Atributos.ToString.QuotedString])); // Se tudo deu certo, retorna uma resposta 201 (Created) Resposta201(qryServer.Fields[0].AsString); finally if Assigned(Body) then begin Body.Free; end; end; end; procedure MontaRespostaGetSemId; var Itens: TJSONArray; Item : TJSONObject; begin // Inicializa o processamento qryServer.FetchAll; qryServer.First; Itens := TJSONArray.Create; // Varre o dataset para montar o array de itens while not qryServer.Eof do begin Item := TJSONObject.ParseJSONValue(qryServer.Fields[1].AsString) as TJSONObject; Item.AddPair(NO_ID, qryServer.Fields[0].AsString); Itens.Add(Item); qryServer.Next; end; // Adiciona o código 200 (Sucesso) à resposta Response.StatusCode := 200; // Monta o objeto JSON com a resposta with TJSONObject.Create do begin AddPair(NO_QTDE, TJSONNumber.Create(qryServer.RecordCount)); AddPair(NO_ITENS, Itens); Response.Content := ToString; Free; end; end; procedure ExecutaConsultaGetSemId; begin // Executa a consulta com base no recurso qryServer.Open(Format(SQL_SEL_REGISTRO, [Recurso.QuotedString])); // Se não há registros, retorna uma resposta 404 (Not found) // Sehá registros, monta a resposta correspondente case qryServer.RecordCount = 0 of True : Resposta404; False: MontaRespostaGetSemId; end; end; procedure InsereAcaoHATEOAS(const AAcoes: TJSONArray; const AVerbo: string); begin AAcoes.Add(TJSONObject.Create .AddPair(NO_RELACAO, 'self') .AddPair(NO_TIPO_ACAO, AVerbo) .AddPair(NO_HREF, URL)); end; procedure MontaRespostaGetComId; var Resposta: TJSONObject; Acoes: TJSONArray; begin // Atribui o código 200 (Sucesso) à resposta Response.StatusCode := 200; // Cria o array para indicar as ações impostas pelo HATEOAS Acoes := TJSONArray.Create; // Insere as ações possíveis com o objeto no array InsereAcaoHATEOAS(Acoes, 'GET'); InsereAcaoHATEOAS(Acoes, 'PUT'); InsereAcaoHATEOAS(Acoes, 'DELETE'); InsereAcaoHATEOAS(Acoes, 'HEAD'); // Cria o objeto de resposta conforma inserido na base de dados Resposta := TJSONObject.ParseJSONValue(qryServer.Fields[0].AsString) as TJSONObject; // Adiciona o array com as ações possíveis Resposta.AddPair(NO_ACOES, Acoes); // Atribui o conteúdo do objeto JSON ao corpo da resposta Response.Content := Resposta.ToString; Resposta.Free; end; procedure ExecutaConsultaGetComId; begin Id := PathInfo[5]; // Executa uma consulta baseada no Id fornecido qryServer.Open(Format(SQL_SEL_REGISTRO_POR_ID, [Recurso.QuotedString, Id.QuotedString])); // Se encontrou o registro, monta a resposta apropriada // Se não encontrou, retrna uma resposta 404 (Not found) case qryServer.RecordCount = 0 of True : Resposta404; False: MontaRespostaGetComId; end; end; procedure ExecutaConsultaGet; begin // Verifica se fará a consulta com ou sem o Id case Length(PathInfo) >= 6 of True : ExecutaConsultaGetComId; False: ExecutaConsultaGetSemId; end; end; procedure TrataVerbo; begin // Trata o verbo correspondente case Request.MethodType of mtGet : ExecutaConsultaGet; mtPost : ExecutaAcaoPost; mtPut, mtDelete, mtHead : VerificaId; mtPatch : Resposta405; end; end; procedure VerificaExistenciaRecurso; begin // Verifica se o recurso foi cadastrado na base de dados qryServer.Open(Format(SQL_SEL_RECURSO, [Recurso.QuotedString])); // Se não foi cadastrado, retorna uma mensagem com código 400 (Bad request) // Se foi, chama a rotina de tratamento do verbo case qryServer.Fields[0].AsInteger = 0 of True : Resposta400(RECURSO_NAO_ENCONTRADO); False: TrataVerbo; end; end; procedure VerificaRecursoDefinido; begin // Verifica se o nome do recurso foi colocado na URL // Se não foi colocado, retorna uma mensagem com código 400 (Bad request) // Se foi, chama a rotina de verificação case Length(PathInfo) < 5 of True : Resposta400(RECURSO_NAO_DEFINIDO); False: begin Recurso := PathInfo[4]; VerificaExistenciaRecurso; end; end; end; procedure TrataRequisicao; begin // Divide o PathInfo da requisição para obter as informações necessárias PathInfo := Request.InternalPathInfo.Remove(1, 1).Split(['/']); // Atribui o Content-Type padrão da resposta Response.ContentType := 'application/json;encoding=utf-8'; // Chama a primeira verificação para execução das ações em cascata VerificaRecursoDefinido; end; begin URL := Request.URL; try TrataRequisicao; except on E: Exception do begin Response.StatusCode := 500; with TJSONObject.Create do begin AddPair(NO_SUCESSO, TJSONBool.Create(False)); AddPair(NO_MENSAGEM, ERRO_PROCESSAMENTO); AddPair(NO_DETALHE, E.Message); Response.Content := ToString; Free; end; end; end; end; initialization finalization Web.WebReq.FreeWebModules; end. |
E, por fim, nosso arquivo DPR fica desta maneira:
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 | library mod_api_cadastro_module_v1; uses {$IFDEF MSWINDOWS} Winapi.ActiveX, System.Win.ComObj, {$ENDIF } Web.WebBroker, Web.ApacheApp, Web.HTTPD24Impl, Data.DBXCommon, Datasnap.DSSession, Cadastro.Datamodules.Container in 'src\Cadastro.Datamodules.Container.pas' {Container: TWebModule}, Cadastro.Consts.SQL in 'src\Cadastro.Consts.SQL.pas', Cadastro.Consts.Respostas in 'src\Cadastro.Consts.Respostas.pas', Cadastro.Consts.JSON in 'src\Cadastro.Consts.JSON.pas'; {$R *.res} // httpd.conf entries: // (* LoadModule api_cadastro_module_v1_module modules/mod_api_cadastro_module_v1.dll <Location /api/devspace/cadastro/v1> SetHandler mod_api_cadastro_module_v1-handler </Location> *) // // These entries assume that the output directory for this project is the apache/modules directory. // // httpd.conf entries should be different if the project is changed in these ways: // 1. The TApacheModuleData variable name is changed. // 2. The project is renamed. // 3. The output directory is not the apache/modules directory. // 4. The dynamic library extension depends on a platform. Use .dll on Windows and .so on Linux. // // Declare exported variable so that Apache can access this module. var GModuleData: TApacheModuleData; exports GModuleData name 'api_cadastro_module_v1_module'; procedure TerminateThreads; begin TDSSessionManager.Instance.Free; Data.DBXCommon.TDBXScheduler.Instance.Free; end; begin {$IFDEF MSWINDOWS} CoInitFlags := COINIT_MULTITHREADED; {$ENDIF} Web.ApacheApp.InitApplication(@GModuleData); Application.Initialize; Application.WebModuleClass := WebModuleClass; TApacheApplication(Application).OnTerminate := TerminateThreads; Application.Run; end. |
Entendendo a codificação das APIs RESTful dinâmicas
Num primeiro momento, a codificação destas APIs RESTful podem parecer um tanto confusas, eu sei. O datamodule, em si, tem apenas um método – ContainerDefaultAction – e este tem uma infinidade de sub-rotinas. Para quem não está familiarizado com o conceito de sub-rotinas, realmente é estranho, já que todas elas poderiam ter sido criadas como métodos de visibilidade privada dentro do datamodule. Isso até faria sentido se esses métodos fossem invocados por mais de um método, o que não acontece aqui.
Mesmo que tivéssemos mais algum método do datamodule, dificilmente ele teria a necessidade de invocar as mesmas sub-rotinas que o método atual invoca e, por uma questão simples de escopo, isso já é o suficiente para que eu não as coloque com visibilidade privada no escopo do datamodule.
Outra razão para isso é o tamanho final dos métodos implementados. Se toda a codificação fosse feita no evento do datamodule, ele ultrapassaria facilmente as 200 linhas de código. Em termos de toxicidade de código, isso é terrível. Se acessarmos os parâmetros de toxicidade padrão do Delphi, teremos algo como isso:

Isso nos diz que, se um método tem mais que 20 linhas, 6 parâmetros, 5 níveis de profundidade em estruturas de decisão ou uma complexidade ciclomática acima de 6, nosso código está mais próximo de Chernobil do que imaginamos. Então, se verificarmos a toxicidade do código gerado para nossas APIs RESTful, teremos o seguinte:

O nível de toxicidade é medido partindo do 0 e tem o valor máximo, antes de se tornar tóxico, 999. Então, qualquer coisa acima de 1.000 indica que nosso código não está bom o suficiente e precisa ser melhorado. Como, no caso deste código, não ultrapassamos 400, nosso código é tão limpo quanto aquele rio que nasce no alto de uma serra.
No mais, a codificação está bem documentada, com comentários que indicam o comportamento de cada rotina, por mais que isso pareça até óbvio. Uma leitura mais atenta do próprio código nos trará o panorama geral de seu funcionamento e, por conseguinte, sua compreensão.
Compilando e testando nossas APIs RESTful
Como já vimos como fazer a compilação e o deploy de nossas APIs RESTful, não vamos descrever todo o processo novamente. Partindo diretamente para os testes, vamos testar a URL http://localhost/api/devspace/cadastro/v1 sem definir o recurso e ver o que a API responde:

Se dermos uma olhada no código do datamodule, mais precisamente na linha 350 – sub-rotina VerificaRecursoDefinido -, veremos que, quando não definimos o recurso, a resposta será, exatamente, a apresentada na imagem acima. Já se fizermos o teste com um recurso diferente dos que cadastramos aqui, teremos o seguinte:

Mais uma vez, se olharmos o código desenvolvido para nossas APIs RESTful, veremos na linha 337, sub-rotina VerificaExistenciaRecurso, que a resposta também é adequada. Já se fizermos a requisição usando um dos recursos cadastrados,como ainda não cadastramos nenhum registro, teremos o seguinte:

Inserindo registros em nossas APIs RESTful
Já publicamos e fizemos testes básicos com nossa API, então é hora de inserir dados e entender o comportamento por trás dessa estrutura. Se usarmos este objeto JSON no corpo de uma requisição POST, obteremos o seguinte:

Se consultarmos este registro que acabamos de inserir, usando seu Id, teremos o seguinte:

Já se fizermos uma consulta sem especificar o Id consultado, teremos:

Ampliando nossas APIs RESTful sem a necessidade de codificação
Uma das grandes vantagens de construirmos APIs RESTful dinâmicas é eliminar qualquer necessidade de codificação para ampliar os recursos disponíveis nela, o que as torna APIs RESTful de evolução no code, algo que, sinceramente, muita gente nem imagina que seja possível com Delphi. Se fizermos uma requisição GET à URL http://localhost/api/devspace/cadastro/v1/tributo, receberemos uma resposta 400 (Bad request), dizendo que o recurso não foi encontrado em nossa API. Contudo, se executarmos a instrução insert into cadastro.recurso values (uuid_generate_v4(), ‘tributo’); em nosso banco de dados e refizermos nossa requisição GET, a resposta muda para 404 (Not found), já que o recurso foi encontrado na API, mas ele ainda não tem registros.
O que fizemos aqui, afinal, foi criar uma estrutura que nos possibilita construir APIs RESTful com baixíssima necessidade de manutenção, altamente escaláveis e com evolução sem qualquer necessidade de codificação. Isso torna nossas APIs RESTful extremamente versáteis, capazes de atender às necessidades de qualquer projeto que necessite de cadastros simples.
Por óbvio, dentro dessa estrutura básica, não foram previstas formas de validação de objetos JSON antes de sua inserção. Mas, caso você queira entender como isso funciona e tiver um pouquinho de paciência, um dos próximos passos será o artigo sobre esse tema, trazendo diversas informações para que você possa implementar este recurso utilizando todo o poder do PostgreSQL e eliminar a necessidade de fazê-lo com Delphi.
Testando os demais verbos em nossas APIs RESTful
Ao chegarmos neste ponto, já está bem claro como funcionarão nossas APIs RESTful construídas sobre esta estrutura. Com qualquer cliente REST é possível fazer todas as requisições, em todos os verbos disponíveis, sendo eles GET, POST, PUT, PATCH, DELETE e HEAD. É claro que, para isso, é necessária uma boa compreensão de como funcionam APIs RESTful e como elas se comportam conforme mudamos os verbos das requisições.
Aqui, você é livre para efetuar seus próprios testes e verificar as respostas mas, isso eu posso garantir, tudo será simples e rápido, como as boas APIs RESTful devem ser.
Posso aplicar tudo isso em minhas APIs REStful?
Algo que sempre falo por aqui é: o nível de maturidade de quem trabalha com o desenvolvimento é o que determina a aplicação ou não de uma técnica, tecnologia ou conhecimento. Mas, sim, com esta estrutura você pode construir APIs RESTful extremamente confiáveis e que te permitirão expandir qualquer negócio de forma rápida e simples e, o melhor de tudo, com pouquíssimo ou até nenhum código adicional.
“Quanto mais buscamos o conhecimento sobre aquilo que fazemos, mais percebemos que ainda não sabemos tudo. Se eu, hoje, desenvolvesse soluções como há 15 anos, teria desperdiçado 15 anos da minha carreira. Se daqui a 2 anos eu ainda desenvolver soluções como desenvolvo hoje, serão mais dois anos da minha carreira jogados fora.”
E o que vem depois?
Quero terminar este artigo como aquela série que sempre deixa um gancho gigante no último episódio de cada temporada. Para o futuro, existem outros artigos planejados, como estruturas de validação de objetos JSON através de JSON schemas usando o PostgreSQL, para que possamos garantir que os objetos que estamos inserindo ou atualizando na base de dados correspondem ao objeto esperado, ou utilização de padrões de projetos, interfaces e RTTI para manipularmos objetos e efetuarmos operações complexas em nossas APIs RESTful. Mas, até lá, vale a pena explorar a estrutura que desenvolvemos aqui.
Ah, e como sempre, o código fonte desse projeto pode ser baixado aqui.
Abraços e até a próxima.
Faça um comentário