Criando um Cliente REST com Fetch API em JavaScript
Utilizando o Fetch API em JavaScript, é possível implementar um cliente completo para Web APIs no estilo REST, facilitando a interação com o servidor. A arquitetura REST utiliza métodos HTTP específicos para a comunicação com o servidor:
GET
para obter dados.POST
para adicionar dados.PUT
para atualizar dados.DELETE
para remover dados.
Vamos explorar como criar um cliente em JavaScript para interagir com uma API.
Criando o Servidor com Node.js
Primeiramente, vamos definir o servidor que representará a Web API. Utilizaremos Node.js para isso. Para processar as requisições, crie um arquivo server.js
com o seguinte conteúdo:
const http = require("http");
const fs = require("fs");
// Dados com os quais o cliente irá interagir
const users = [
{ id: 1, name: "Tom", age: 24 },
{ id: 2, name: "Bob", age: 27 },
{ id: 3, name: "Alice", age: 23 },
];
// Função para processar os dados recebidos do cliente
function getReqData(req) {
return new Promise(async (resolve, reject) => {
try {
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
}
const data = JSON.parse(Buffer.concat(buffers).toString());
resolve(data);
} catch (error) {
reject(error);
}
});
}
http
.createServer(async (request, response) => {
// Obtenção de todos os usuários
if (request.url === "/api/users" && request.method === "GET") {
response.end(JSON.stringify(users));
}
// Obtenção de um usuário específico por ID
else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "GET") {
const id = request.url.split("/")[3];
const user = users.find((u) => u.id === parseInt(id));
if (user) {
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
}
// Exclusão de um usuário por ID
else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") {
const id = request.url.split("/")[3];
const userIndex = users.findIndex((u) => u.id === parseInt(id));
if (userIndex > -1) {
const user = users.splice(userIndex, 1)[0];
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
}
// Adição de um novo usuário
else if (request.url === "/api/users" && request.method === "POST") {
try {
const userData = await getReqData(request);
const user = { name: userData.name, age: userData.age };
const id = Math.max.apply(
Math,
users.map((u) => u.id)
);
user.id = id + 1;
users.push(user);
response.end(JSON.stringify(user));
} catch (error) {
response.writeHead(400, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Solicitação inválida" }));
}
}
// Atualização de um usuário
else if (request.url === "/api/users" && request.method === "PUT") {
try {
const userData = await getReqData(request);
const user = users.find((u) => u.id === parseInt(userData.id));
if (user) {
user.age = userData.age;
user.name = userData.name;
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
} catch (error) {
response.writeHead(400, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Solicitação inválida" }));
}
} else if (request.url === "/" || request.url === "/index.html") {
fs.readFile("index.html", (error, data) => response.end(data));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Recurso não encontrado" }));
}
})
.listen(3000, () => console.log("Servidor iniciado em http://localhost:3000"));
Analise do Código
Vamos analisar este código de forma geral. Primeiro, são definidos os dados com os quais o cliente irá trabalhar:
const users = [
{ id: 1, name: "Tom", age: 24 },
{ id: 2, name: "Bob", age: 27 },
{ id: 3, name: "Alice", age: 23 },
];
Para simplificação, os dados são definidos como um array comum de objetos, mas em situações reais esses dados geralmente são extraídos de um banco de dados.
Em seguida, é definida a função getReqData()
, que extrai os dados enviados pelo cliente na requisição e os converte para o formato JSON (assumindo que o cliente enviará os dados em JSON):
function getReqData(req) {
return new Promise(async (resolve, reject) => {
try {
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
}
const data = JSON.parse(Buffer.concat(buffers).toString());
resolve(data);
} catch (error) {
reject(error);
}
});
}
O resultado da função é definido como uma promise. Se os dados forem analisados com sucesso, a promise retorna o objeto parseado. Caso ocorra um erro, uma mensagem de erro é retornada.
Para cada tipo de requisição, é definido um cenário específico.
Quando a aplicação recebe uma requisição do tipo GET no endereço "api/users", o seguinte código é executado:
if (request.url === "/api/users" && request.method === "GET") {
response.end(JSON.stringify(users));
}
Aqui, simplesmente enviamos o array users
definido anteriormente.
Quando o cliente solicita um objeto específico por ID em uma requisição GET no endereço "api/users/", o seguinte código é executado:
else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "GET") {
const id = request.url.split("/")[3];
const user = users.find((u) => u.id === parseInt(id));
if (user) {
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
}
Nesse caso, precisamos encontrar o usuário no array pelo ID e, se não for encontrado, retornamos um código de status 404 com uma mensagem de erro em JSON.
Ao receber uma requisição DELETE no endereço "/api/users/", encontramos o índice do objeto no array e, se encontrado, o removemos do array e enviamos a resposta ao cliente:
else if (request.url.match(/\/api\/users\/([0-9]+)/) && request.method === "DELETE") {
const id = request.url.split("/")[3];
const userIndex = users.findIndex((u) => u.id === parseInt(id));
if (userIndex > -1) {
const user = users.splice(userIndex, 1)[0];
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
}
Se o objeto não for encontrado, retornamos um código de status 404.
Ao receber uma requisição POST no endereço "/api/users", usamos a função getReqData()
para extrair os dados da requisição:
else if (request.url === "/api/users" && request.method === "POST") {
try {
const userData = await getReqData(request);
const user = { name: userData.name, age: userData.age };
const id = Math.max.apply(Math, users.map(u => u.id)) + 1;
user.id = id;
users.push(user);
response.end(JSON.stringify(user));
} catch (error) {
response.writeHead(400, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Solicitação inválida" }));
}
}
Como a execução da função pode lançar um erro (por exemplo, ao analisar JSON), todo o código é encapsulado em um bloco try..catch
. Após obter os dados, criamos um novo objeto e o adicionamos ao array.
Se a aplicação receber uma requisição PUT, também usamos a função getReqData()
para obter os dados enviados pelo cliente. Se o objeto for encontrado no array, atualizamos os dados, caso contrário, enviamos um código de status 404:
else if (request.url === "/api/users" && request.method === "PUT") {
try {
const userData = await getReqData(request);
const user = users.find((u) => u.id === parseInt(userData.id));
if (user) {
user.age = userData.age;
user.name = userData.name;
response.end(JSON.stringify(user));
} else {
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Usuário não encontrado" }));
}
} catch (error) {
response.writeHead(400, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "Solicitação inválida" }));
}
}
Dessa forma, definimos uma API básica. Agora, vamos adicionar o código do cliente.
Definindo o Cliente
Ao acessar a raiz da aplicação web ou o endereço /index.html
, o servidor retornará o arquivo index.html. Portanto, na mesma pasta do arquivo do servidor, defina um arquivo index.html com o seguinte código:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Programício</title>
<style>
tr {
height: 30px;
}
td,
th {
min-width: 40px;
text-align: left;
}
a {
cursor: pointer;
padding: 5px;
text-decoration: underline;
color: navy;
}
input {
width: 180px;
}
</style>
</head>
<body>
<h2>Lista de Usuários</h2>
<form name="userForm">
<p>
<label for="name">Nome:</label><br />
<input name="name" />
</p>
<p>
<label for="age">Idade:</label><br />
<input name="age" type="number" min="1" max="110" />
</p>
<p>
<button type="submit">Salvar</button>
<button type="reset">Resetar</button>
</p>
</form>
<table>
<thead>
<tr>
<th>Id</th>
<th>Nome</th>
<th>Idade</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
<script>
let userId = 0; // Identificador do usuário que está sendo editado
const userForm = document.forms["userForm"]; // Formulário de entrada
// Função para obter todos os usuários
async function getUsers() {
const response = await fetch("/api/users", {
method: "GET",
headers: { Accept: "application/json" },
});
if (response.ok === true) {
const users = await response.json();
const rows = document.querySelector("tbody");
users.forEach((user) => rows.append(row(user)));
}
}
// Função para obter um usuário específico
async function getUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "GET",
headers: { Accept: "application/json" },
});
if (response.ok === true) {
const user = await response.json();
userId = user.id;
userForm.elements["name"].value = user.name;
userForm.elements["age"].value = user.age;
}
}
// Função para criar um novo usuário
async function createUser(userName, userAge) {
const response = await fetch("/api/users", {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
name: userName,
age: parseInt(userAge, 10),
}),
});
if (response.ok === true) {
const user = await response.json();
reset();
document.querySelector("tbody").append(row(user));
}
}
// Função para editar um usuário
async function editUser(userId, userName, userAge) {
const response = await fetch("/api/users", {
method: "PUT",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
id: userId,
name: userName,
age: parseInt(userAge, 10),
}),
});
if (response.ok === true) {
const user = await response.json();
reset();
document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user));
}
}
// Função para deletar um usuário
async function deleteUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (response.ok === true) {
const user = await response.json();
document.querySelector(`tr[data-rowid='${user.id}']`).remove();
}
}
// Função para resetar o formulário
function reset() {
userForm.reset();
userId = 0;
}
// Função para criar uma linha na tabela
function row(user) {
const tr = document.createElement("tr");
tr.setAttribute("data-rowid", user.id);
const idTd = document.createElement("td");
idTd.append(user.id);
tr.append(idTd);
const nameTd = document.createElement("td");
nameTd.append(user.name);
tr.append(nameTd);
const ageTd = document.createElement("td");
ageTd.append(user.age);
tr.append(ageTd);
const linksTd = document.createElement("td");
const editLink = document.createElement("a");
editLink.setAttribute("data-id", user.id);
editLink.append("Editar");
editLink.addEventListener("click", async (e) => {
e.preventDefault();
await getUser(user.id);
});
linksTd.append(editLink);
const removeLink = document.createElement("a");
removeLink.setAttribute("data-id", user.id);
removeLink.append("Deletar");
removeLink.addEventListener("click", async (e) => {
e.preventDefault();
await deleteUser(user.id);
});
linksTd.append(removeLink);
tr.appendChild(linksTd);
return tr;
}
// Evento de reset do formulário
userForm.addEventListener("reset", (e) => reset());
// Evento de submit do formulário
userForm.addEventListener("submit", (e) => {
e.preventDefault();
const name = userForm.elements["name"].value;
const age = userForm.elements["age"].value;
if (userId === 0) {
createUser(name, age);
} else {
editUser(userId, name, age);
}
});
// Carregar os usuários ao carregar a página
getUsers();
</script>
</body>
</html>
Explicação do Código
A lógica principal está no código JavaScript. Inicialmente, definimos dados globais:
let userId = 0; // Identificador do usuário que está sendo editado
const userForm = document.forms["userForm"]; // Formulário de entrada
A constante userForm
representa o formulário para adicionar ou editar um usuário. A variável userId
rastreia o identificador do usuário carregado. Se for igual a 0, significa que um novo usuário está sendo criado. Por padrão, ao carregar a página, essa variável é 0, indicando que nenhum usuário está carregado no formulário. Se userId
não for 0, um usuário foi carregado previamente através da função getUser
, e estamos prontos para editar esse usuário.
Ao carregar a página, obtemos todos os usuários do banco de dados usando a função getUsers
:
async function getUsers() {
const response = await fetch("/api/users", {
method: "GET",
headers: { Accept: "application/json" },
});
if (response.ok === true) {
const users = await response.json();
const rows = document.querySelector("tbody");
users.forEach((user) => rows.append(row(user)));
}
}
Para adicionar linhas na tabela, usamos a função row
, que retorna uma linha com links para editar e deletar o usuário.
A função getUser
obtém o usuário do servidor e preenche o formulário com os dados:
async function getUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "GET",
headers: { Accept: "application/json" },
});
if (response.ok === true) {
const user = await response.json();
userId = user.id;
userForm.elements["name"].value = user.name;
userForm.elements["age"].value = user.age;
}
}
Se userId for 0, a função createUser
envia os dados em uma requisição POST:
async function createUser(userName, userAge) {
const response = await fetch("/api/users", {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
name: userName,
age: parseInt(userAge, 10),
}),
});
if (response.ok === true) {
const user = await response.json();
reset();
document.querySelector("tbody").append(row(user));
}
}
Se um usuário já foi carregado no formulário, a função editUser
envia uma requisição PUT para atualizar os dados:
async function editUser(userId, userName, userAge) {
const response = await fetch("/api/users", {
method: "PUT",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
id: userId,
name: userName,
age: parseInt(userAge, 10),
}),
});
if (response.ok === true) {
const user = await response.json();
reset();
document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user));
}
}
Executando o Servidor
Execute o arquivo do servidor com o comando node server.js
:
C:\app>node server.js Servidor iniciado em http://localhost:3000
O servidor Node.js com a API para a função fetch em JavaScript será iniciado. Acesse "http://localhost:3000"
no navegador para gerenciar os usuários armazenados no arquivo JSON.