Produzir e consumir APIs de diferentes fontes é o pão com manteiga de todo aplicativo da web moderno. No lado do cliente, é a forma como nos comunicamos com o servidor e atualizamos constantemente os estados de nosso aplicativo.

APIs REST servidas por HTTP são indiscutivelmente a forma mais comum de troca no momento. Eles fornecem uma forma fácil de troca no formato JSON. Podemos nos comunicar facilmente com o servidor por meio de um simples curl ou fetch .

Acredito que a integração e a comunicação com o servidor devem ser tão intuitivas quanto possível, no entanto, e isso se torna especialmente aparente depois de trabalhar com GraphQL e Apollo. Saber exatamente que serviço chamar, como chamá-lo e quais resultados esperar melhora muito a eficiência e a produtividade para nós, engenheiros de front-end.

Neste tutorial, veremos como integrar um serviço gerado por OpenAPI a partir do servidor e, em seguida, usar esse serviço gerado em um aplicativo React. Usando estruturas da Web como Django, Rails, Spring ou NestJS, podemos produzir facilmente definições OpenAPI por meio de nosso código de aplicativo.

A Especificação OpenAPI (anteriormente a Especificação Swagger) é independente de estrutura e pode ser usada para gerar informações sobre rotas, tipos de dados, etc. OpenAPI serve como um formato de troca sólido para ajudar os metadados da API a cruzarem entre diferentes linguagens.

Objetivos deste tutorial

O que você deve aprender com esta postagem? Dado um ponto de extremidade OpenAPI (JSON ou YAML), gostaríamos de gerar:

  • Interfaces para os objetos de transferência de dados (DTO)-ou seja, interfaces para chamar terminais específicos
  • Serviços que podemos usar para interagir com as APIs

Os serviços gerados devem incluir todas as rotas expostas; todos os tipos de dados de que precisam para comunicação; uma lista exaustiva de todos os parâmetros obrigatórios/opcionais; e, mais importante, os tipos de dados que as chamadas retornam.

Faremos isso fazendo com que o servidor gere todos os serviços para comunicação com os terminais. Isso oferece três vantagens críticas para o engenheiro de front-end:

  • Elimina a necessidade de escrever todo o código clichê necessário para chamar as APIs
  • Oferece clareza ao lado do cliente sobre todas as interações que o servidor pode processar
  • Transparência-o cliente sempre saberá quando uma alteração foi feita no servidor

Veremos tudo isso em primeira mão construindo uma lista de tarefas simples. Sem mais delongas, vamos às partes mais técnicas do tutorial. Aqui está um esboço do que iremos cobrir:

O que é a especificação OpenAPI?

De acordo com a documentação:

A especificação OpenAPI (OAS) define uma interface padrão independente de linguagem para APIs RESTful. Quando definido corretamente, um consumidor pode entender e interagir com o serviço remoto com uma quantidade mínima de lógica de implementação.

A OAS facilita a visualização das capacidades dos serviços. Com geradores de documentação como o Swagger, você pode gerar uma IU para testar as chamadas de API. Abaixo está um exemplo do servidor todo com o qual estaremos interagindo.

Código de aplicativo gerado do Swagger Codegen
Código gerado do codegen.

Geração de documentos Swagger para nossa API REST com Swagger Codegen

Neste servidor de exemplo, usei NestJS com Swagger. A linguagem e os detalhes de implementação do servidor são irrelevantes-contanto que tenhamos as especificações OpenAPI e a IU Swagger implementadas, teremos o mesmo resultado.

Vamos examinar rapidamente o código do servidor. Nós o mantivemos mínimo, porque este não é o ponto principal aqui. No entanto, examinaremos os serviços do lado do servidor para que possamos apreciar a facilidade com que obtemos as interfaces, funções de API, etc. transferidas para o cliente. O código completo pode ser encontrado neste repositório GitHub .

Parte do código é específico do NestJS, mas não é muito importante entendê-lo totalmente; ter uma ideia geral do que está acontecendo será suficiente. Vou mostrar o código e examiná-lo-pronto? Como um lembrete, isso está relacionado às funções de criação, leitura, atualização e exclusão (CRUD):

 import {TodoService} de'./../services/todo/todo.service';
import {TodoDto, AddTodoDto, EditTodoDto} de'./../dto'; import { Controlador, Obter, Param, Publicar, Por, Corpo, Excluir,
} de'@ nestjs/common';
importar {ApiResponse, ApiTags} de'@ nestjs/swagger'; @ApiTags ('todos')
@Controller ('todos')
export class TodoController { construtor público (privado todoService somente leitura: TodoService) {} @Obter() @ApiResponse ({ status: 200, descrição:'todo encontrado', tipo: [TodoDto], }) public findAll (): Promise  { retornar this.todoService.findAll (); } @Get (': id') public findOne (@Param ('id') id: número): Promessa  { retornar this.todoService.findOne (id); } @Put (': id') edição pública ( @Param ('id') id: número, @Body () todo: EditTodoDto, ): Promessa  { retornar this.todoService.edit (id, todo); } @Publicar() public add (@Body () todo: AddTodoDto): Promise  { retornar this.todoService.add (todo); } @Delete (': id') public remove (@Param ('id') id: número): Promise  { retornar this.todoService.remove (id); }
} 

No código acima, você notará rapidamente um padrão-temos vários métodos públicos . Todos os seus nomes deixam claro o que fazem. Observou algum outro recurso?

Vejamos findAll . Obviamente, isso encontra todos os todos disponíveis; ele não espera nenhum parâmetro, apenas chama todoService.findAll () . O código completo para o todoService pode ser encontrado aqui , mas, mais uma vez, não nos importamos com os detalhes de implementação, apenas quais parâmetros ele espera e o que retorna.

Quando olhamos para edit , podemos ver que ele espera um id e um post body DTO com o tipo EditTodoDto . Com eles, podemos mapear os pontos de extremidade para a IU Swagger acima.

Agora que damos uma olhada na aparência do servidor, como geramos serviços no front-end que imitam todos os métodos públicos no servidor? Para demonstrar isso, criaremos uma tarefa simples com TypeScript para o cliente.

Geração de documentos API, tipos de dados e serviços CRUD em nosso frontend React

Criação do aplicativo React TypeScript

Vamos criar nosso aplicativo de anotações-vamos chamá-lo de Notado 😉. Isso irá gerar todos os boilerplates com Redux Toolkit para gerenciamento de estado .

 npx create-react-app notado--template redux-typescript
//ou
yarn create-react-app notado--template redux-typescript 

Antes de qualquer coisa, vamos gerar os serviços a partir do endpoint do servidor. Para isso, usaremos openapi-typescript e openapi-typescript-codegen . Vamos instalá-los primeiro.

 yarn adiciona openapi-typescript openapi-typescript-codegen.--dev
//ou
npm i openapi-typescript openapi-typescript-codegen-D 

Mas como fazemos com que eles façam algum trabalho? Como nos conectamos ao servidor? Precisamos de um servidor em execução para isso, então, por conveniência, execute o servidor NestJS que criamos anteriormente com yarn start .

Agora que temos um servidor em execução, criaremos um script para extrair os serviços do endpoint do servidor. Também poderíamos ter usado o JSON ou YAML baixado.

Seu servidor deve estar executando na porta -3000 . Agora execute:

 openapi-i http://localhost: 3000/api-json-o src/services/openapi 

Vamos adicionar isso como um script ao nosso package.json para referência mais fácil, ou talvez para um gancho de pré-confirmação:

"types: openapi":"openapi-i http://localhost: 3000/api-json-o src/services/openapi"

Esses comandos dizem ao gerador para gerar os serviços openApi dentro de src/services/openapi . Todas as outras tags e comandos podem ser encontrados na documentação .

Se olharmos dentro de src/services/openapi , podemos ver todo o código gerado:

Documentação Swagger para nosso servidor NestJS
documentação todo-nestjs Swagger.

De particular interesse são os modelos , que fornecem todos os tipos necessários para a interface com a API, bem como o TodoService.ts , que fornece todos os serviços, como aqueles que vimos no servidor. Vamos dar uma olhada.

 importar o tipo {AddTodoDto} de"../models/AddTodoDto";
importar tipo {EditTodoDto} de"../models/EditTodoDto";
importar o tipo {TodoDto} de"../models/TodoDto";
importar {solicitação como __request} de"../core/request"; export class TodosService { public static async findAll (): Promise > { const result=await __request ({ método:"GET", caminho: `/todos`, }); return result.body; } public static async add (requestBody: AddTodoDto): Promise  { const result=await __request ({ método:"POST", caminho: `/todos`, body: requestBody, }); return result.body; } public static async findOne (id: número): Promessa  { const result=await __request ({ método:"GET", caminho: `/todos/$ {id}`, }); return result.body; } edição assíncrona estática pública (id: número, requestBody: EditTodoDto): Promessa  { const result=await __request ({ método:"PUT", caminho: `/todos/$ {id}`, body: requestBody, }); return result.body; } remoção assíncrona estática pública (id: número): Promessa  { const result=await __request ({ método:"DELETE", caminho: `/todos/$ {id}`, }); return result.body; }
} 

Como podemos ver, são todos idênticos aos serviços do servidor, todos sem a necessidade de digitar uma única linha de código. Não é incrível? Observe que todos os tipos de parâmetros corretos, bem como os tipos de retorno, foram mapeados corretamente. Podemos dar uma olhada em src/services/openapi/core.request.ts para entender melhor como as chamadas de API são formuladas.

E é isso-geramos o código necessário para fazer chamadas de API. A seguir, abordaremos como usar esses serviços de código gerado.

Integrando os serviços e tipos gerados em nosso aplicativo

Por uma questão de brevidade, veremos snippets para realizar chamadas de API específicas, em vez de todo o aplicativo. O código completo para a tarefa pode ser encontrado aqui .

Agora vamos à parte divertida. Como usamos esses serviços gerados?

Temos muitas maneiras de fazer isso: poderíamos criar um gancho que envolva as solicitações e a chamada dentro de nosso aplicativo; no caso do Redux Toolkit, poderíamos chamá-los de dentro de um thunk. Vamos explorar os dois exemplos.

Primeiro, criaremos um invólucro reutilizável; podemos chamá-lo do que quisermos. É assim:

 import { AddTodoDto, EditTodoDto, OpenAPI, TodoDto, TodosService,
} de'../openapi'; const {adicionar, editar, findAll, findOne, remover}=TodosService; OpenAPI.BASE='http://localhost: 3000'; export const getTodos=async ()=> { tentar { const todos: TodoDto []=espera findAll (); return todos; } catch (erro) { lance novo erro (erro); }
}; export const getTodoById=async (id: number): Promise => { tentar { retorno aguarda findOne (id); } catch (erro) { lance novo erro (erro); }
}; export const addTodo=async (newTodo: AddTodoDto): Promise => { tentar { retornar, aguardar, adicionar (newTodo); } catch (erro) { lance novo erro (erro); }
}; export const updateTodo=async ( número de identidade, todo: EditTodoDto
): Promessa => { tentar { retornar, aguardar edição (id, todo); } catch (erro) { lance novo erro (erro); }
}; export const deleteTodo=async (id: number)=> { tentar { aguardar remoção (id); } catch (erro) { lance novo erro (erro); }
}; 

Dentro do aplicativo, podemos chamar findAll () -por exemplo, dentro de um useEffect . Veremos um exemplo mais concreto posteriormente. Mas, primeiro, observe a linha em que fizemos isso:

 OpenAPI.BASE='http://localhost: 3000'; 

O que fizemos aqui foi definir o URL base do cliente gerado para http://localhost: 3000 . Isso é trivial, mas vamos imaginar que queremos definir alguns padrões em nosso aplicativo, como cabeçalhos de autorização, etc.

Configurar e definir padrões para serviços de API gerados

Podemos definir alguns padrões para o OpenAPI. A interface completa para a configuração esperada se parece com esta:

 tipo Resolvedor =()=> Promessa ;
tipo Cabeçalhos=Registro ; tipo Config={ BASE: string; VERSÃO: string; WITH_CREDENTIALS: boolean; TOKEN ?: string | Resolver ; NOME DE USUÁRIO ?: string | Resolver ; SENHA ?: string | Resolver ; CABEÇALHOS ?: Cabeçalhos | Resolver ;
}; export const OpenAPI: Config={ BASE:"", VERSÃO:"1.0", WITH_CREDENTIALS: false, TOKEN: indefinido, NOME DE USUÁRIO: indefinido, SENHA: indefinido, CABEÇALHOS: indefinido,
}; 

Então, digamos que nossa API seja protegida por senha. Podemos dizer ao codegen para usar essa senha antes de cada solicitação com:

 OpenAPI.PASSWORD='minha-super-senha-secreta'

Ou imagine que você gostaria de obter um token e adicioná-lo ao cabeçalho da solicitação. Teríamos:

 const getToken=async ()=> { //Algum código que solicita um token... return'SOME_TOKEN';
} OpenAPI.TOKEN=getToken;
//ou
const myHeaderObject={ ...resto, Autorização: `Bearer $ {getToken ()}`
} OpenAPI.HEADERS={myHeaderObject} 

Consumindo os serviços gerados em nosso aplicativo React

Para usar qualquer um dos serviços, basta chamá-los dentro do aplicativo (levando em consideração como gerenciamos o estado, é claro). Um exemplo de nossa tarefa parece algo assim:

 import React, {useCallback, useEffect, useState} de'react';
import {AddTodo, TodoItem} de'./features/todo';
import {getTodos, addTodo, updateTodo, deleteTodo} de'./services/api/todo';
import {AddTodoDto, ApiError, EditTodoDto, TodoDto} de'./services/openapi'; import'./App.css' const App=()=> { const [todos, setTodos]=useState  ([]); const [erro, setError]=useState  (); const handleSaveTodo=useCallback ((e: React.FormEvent, formData: AddTodoDto)=> { e.preventDefault (); addTodo (formData) .então ((todo)=> todo) .catch ((err)=> setError (err)); }, []) const handleUpdateTodo=useCallback ((id: número, todo: EditTodoDto)=> { updateTodo (id, todo) .então ((updatedTodo)=> updatedTodo) .catch ((err)=> setError (err)); }, []); const handleDeleteTodo=useCallback ((id: número)=> { deleteTodo (id).catch ((err)=> setError (err)); }, []); useEffect (()=> { getTodos () .então ((allTodos)=> setTodos (allTodos)) .catch ((erro)=> setError (erro)); }, []); Retorna ( 

Meus Todos

{todos.map ((todo: TodoDto)=> ( ))}
); }; exportar aplicativo padrão;

Você pode ver como simplesmente chamamos os serviços dentro do aplicativo. Podemos até deixar nosso aplicativo mais limpo criando um gancho que é chamado uma vez e podemos eliminar todas as chamadas de API dentro do App.tsx . Não vou entrar em muitos detalhes, pois este não é o objetivo do artigo, mas isso certamente nos ajuda a visualizar o quão poderoso, mas maleável, isso é:

 import {useCallback, useState} de'react'
importar {ApiError, OpenAPI} de'../services/openapi' export function useApi () { const [erro, setError]=useState  (indefinido) const [isLoading, setIsloading]=useState  (verdadeiro) OpenAPI.BASE=process.env.REACT_APP_API_ENDPOINT como string const handleRequest=useCallback (função assíncrona  (solicitação: Promessa ) { setIsloading (verdadeiro) tentar { resposta const=pedido de espera setError (indefinido) resposta de retorno } catch (erro) { setError (erro) } finalmente { setIsloading (verdadeiro) } }, []) function disabledError () { setError (indefinido) } retornar {ignorarError, erro, isLoading, handleRequest}
} exportar useApi padrão 

Novamente, a implementação completa do código pode ser encontrada aqui .

Conclusão

Aí está-um passo a passo detalhado da geração de serviços OpenAPI a partir de um back-end. Nós usamos o NestJS neste caso, mas este processo é independente de estrutura.

Exploramos como gerar as funções de serviço correspondentes no cliente usando um aplicativo React como exemplo. Vimos como obter exposição aos serviços, tipos e interfaces subjacentes do servidor torna a vida do engenheiro de front-end muito mais fácil. Com o TypeScript especialmente, podemos colher os benefícios de um sistema de segurança de tipos que fornece previsibilidade e transparência.

A postagem Gerando e integrando serviços OpenAPI em um aplicativo React apareceu primeiro no LogRocket Blog .

Source link