Servidor Web: RESTful

Escrito em 07 de setembro de 2019 por Lucas Vieira
lucasvieira@protonmail.com

REST

REST é uma arquitetura introduzida por Roy Fielding em 2000, e significa Transferência de Estado Representacional (Representational State Transfer).

Nesta arquitetura, todo componente é um recurso que é acessado via uma interface comum usando métodos-padrão do protocolo HTTP.

Um serviço web baseado na arquitetura REST é conhecido como um serviço web RESTful.

Métodos de requests HTTP

GET

Utilizado para requisitar dados de um recurso. Um dos métodos HTTP mais comuns.

POST

Utilizado para enviar dados a um servidor para criar/atualizar um recurso. Um dos métodos HTTP mais comuns.

PUT

Também utilizado para criação e atualização de recursos no servidor.

A diferença entre POST e PUT é que PUT é idempotente: criar o método PUT para um mesmo valor múltiplas vezes trará a mesma resposta; se isto for feito com POST, é possível que ocorram efeitos colaterais (por exemplo, recursos duplicados serão criados).

DELETE

Deleta o recurso especificado.

Outros métodos

  • HEAD: Similar ao GET, mas sem o retorno do corpo do request. Útil para verificar o que GET pode retornar, antes de recuperar os dados em si.
  • OPTIONS: Descreve as opções de comunicação para o recurso-alvo.

Mais informações sobre métodos de requests HTTP podem ser encontradas na w3schools.

Instalando middleware útil

Utilizaremos o mesmo projeto com o framework Express da última aula.

rm -rf express_test/
mkdir express_test/
cd express_test
npm init -y
npm install --save express

Começaremos instalando alguns módulos extras:

  • cookie-parser: Faz parsing de cabeçalhos de cookies, criando um objeto com os nomes dos cookies e seus respectivos dados.
  • multer: Lida com objetos codificados usando multipart/form-data.
cd express_test
npm install --save cookie-parser
npm install --save multer

Fazendo upload de um arquivo

Criando página de upload

upload.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Upload de arquivos</title>
  </head>
  <body>
    <h3>Upload de arquivos</h3>
    <p>
      Selecione um arquivo para enviar:
      <br/>
      <form action="http://127.0.0.1:8081/file_upload"
            method="POST"
            enctype="multipart/form-data">
        <input type="file" name="file" size="50"/>
        <br/>
        <input type="submit" value="Fazer upload"/>
      </form>
    </p>
  </body>
</html>

Fornecendo a página de upload

const express    = require('express');
const fs         = require('fs');
const multer     = require('multer');

let app          = express();

// Trabalhador de upload do Multer. Salvaremos
// os arquivos temporariamente em /tmp
let upload       = multer({ dest: '/tmp/' });

app.use(express.static('public'));

// Ponto de entrada para a página de upload
app.get('/upload', (request, response) => {
    response.sendFile(__dirname + "/" + "upload.html");
});

Fornecendo o serviço de upload

// Ponto de entrada para o POST request de upload
app.post('/file_upload', upload.single('file'), (request, response) => {
    // Saídas de debug
    console.log(request.file.filename);
    console.log(request.file.path);
    console.log(request.file.mimetype);

    let file = __dirname + "/" + request.file.originalname;

    fs.readFile(request.file.path, (error, data) => {
        // Após a leitura de todo o arquivo recebido,
        // escreva-o no disco
        let res;
        fs.writeFile(file, data, (err) => {
            // Após escrever o arquivo, verifique por erros
            // e ajuste a resposta apropriadamente
            if(err) {
                console.error(err);
            } else {
                res = {
                    message:  'Arquivo enviado com sucesso!',
                    filename: request.file.originalname
                };
            }

            // Saída de debug e envio da resposta como
            // JSON em formato de string
            console.log(res);
            response.end(JSON.stringify(res));
        });
    }); 
});

Ponto de entrada do servidor

let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log(`Servidor operando em http://${host}:${port}`);
});

Códigos de status HTTP

Tabela de códigos mais relevantes

Código Significado
200 Ok
301 Moved Permanently
403 Forbidden
404 Not Found
408 Request Timeout
500 Internal Server Error

Mais informações estão na Wikipedia.

Tabela rápida dos códigos

Código Significado
1xx "Espera aí"
2xx "Tome o que você queria"
3xx "Cai fora daqui"
4xx "Você fez merda"
5xx "Eu fiz merda"

Serviços RESTful

Base de dados

Teremos, inicialmente, uma base de dados armazenada em JSON. Poderíamos também estar lidando diretamente com um banco de dados.

users.json:

{
    "user1" : {
        "name":       "Fulano",
        "password":   "senha1",
        "profession": "Professor",
        "id":         1
    },

    "user2" : {
        "name":       "Ciclano",
        "password":   "senha2",
        "profession": "Bibliotecário",
        "id":         2
    },

    "user3" : {
        "name":       "Beltrano",
        "password":   "senha3",
        "profession": "Clérigo",
        "id":         3
    }
}

Mapa dos serviços

Abaixo, temos uma tabela com os dados para cada um dos serviços que vamos criar.

URI Método Corpo Função
/users/list GET vazio Mostra a lista dos usuários.
/users/add POST String JSON Adiciona um novo usuário.
/users/delete DELETE String JSON Deleta um usuário existente.
/users/:id GET vazio Mostra detalhes de um usuário.

Listagem de usuários

Descrição geral

A listagem de usuários consiste em retornar o arquivo users.json em verbatim.

Implementação

const express = require('express');
const fs      = require('fs');

let app = express();

// Ponto de entrada: GET request em /users/list
app.get('/users/list', (request, response) => {
    fs.readFile(__dirname + '/users.json', 'utf8', (error, data) => {
        console.log("Requerida uma listagem de usuários");
        console.log(data);
        response.end(data);
    });
});

let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log("Servidor iniciado em http://%s:%s", host, port);
});

Teste REST

Para recuperar os dados, podemos usar um cliente REST qualquer, ou simplesmente usar a ferramenta curl:

curl http://localhost:8081/users/list

Adicionar usuário

Descrição geral

A API deverá permitir uma nova adição de um usuário na lista, baseado em uma especificação em JSON. Veja o que esperamos como dados da requisição:

{
    "user4": {
        "name":       "John Doe",
        "password":   "foobarbazquux",
        "profession": "Professor",
        "id":         4
    }
}

Implementação

const express = require('express');
const fs      = require('fs');

let app = express();

app.use(express.json()); // Middleware para bodies em JSON

let regex = /\0/g;

// Ponto de entrada: POST request em /users/add
app.post('/users/add', (request, response) => {
    console.log("Adicionando novo usuário:");
    console.log(request.body);

    // Leia o banco de dados e insira um novo usuário
    fs.readFile(__dirname + "/users.json", 'utf8', (error, data) => {
        // Mesclando ambos os objetos recebidos
        data = JSON.parse(data.toString().replace(regex, ""));
        data = Object.assign(data, request.body);
        let string_data = JSON.stringify(data);
        // Escreva de volta no arquivo JSON
        fs.writeFile(__dirname + "/users.json", string_data, 'utf8', (error) => {
            if(error) {
                // Erro interno do servidor
                console.log(error);
                response.status(500).end();
            } else {
                // Echo-back dos dados inseridos
                response.end(JSON.stringify(request.body));
            }
        });
    });
});


let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log("Servidor iniciado em http://%s:%s", host, port);
});

Teste REST

curl --header "Content-Type: application/json" \
     --request POST \
     --data '{ "user4": {"name":"John Doe", "password":"foobarbazquux", "profession":"Professor", "id":4} }' \
     http://127.0.0.1:8081/users/add

Remover um usuário

Descrição geral

Similar ao /users/add, recebemos os dados de entrada através de um objeto JSON. Neste caso, requisitamos um campo id com valor numérico para tal.

Implementação

const express = require('express');
const fs      = require('fs');

let app = express();

app.use(express.json());

let regex = /\0/g;

// Ponto de entrada: DELETE Request em /users/delete
app.delete('/users/delete', (request, response) => {
    // Salve a ID, vinda como JSON, em uma variável
    let id = request.body["id"];
    console.log(`Deletando usuário com ID = ${id}`);
    
    // Leia o banco de dados
    fs.readFile(__dirname + "/users.json", 'utf8', (error, data) => {
        // Remova o usuário com a id requisitada, dos dados na memória
        data = JSON.parse(data.toString().replace(regex, ""));
        delete data["user" + id];
        
        // Armazene no arquivo do banco de dados
        let string_data = JSON.stringify(data);
        fs.writeFile(__dirname + "/users.json", string_data, 'utf8', (error) => {
            if(error) {
                // Erro interno em caso de escrita
                console.log(error);
                response.status(500).end();
            } else {
                // Echo-back de todos os usuários em caso de sucesso
                response.end(string_data);
            }
        });
    });
});

let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log("Servidor iniciado em http://%s:%s", host, port);
});

Teste REST

curl --header "Content-Type: application/json" \
     --request DELETE \
     --data '{ "id": 4 }' \
     http://127.0.0.1:8081/users/delete

Mostrar detalhes de um usuário

Descrição geral

Usando a ID de um usuário (passada via URL), mostramos os dados do mesmo, de forma especializada. A implementação lembra /users/list.

Implementação

const express = require('express');
const fs      = require('fs');

let app = express();

let regex = /\0/g;

// Ponto de entrada: GET request em /users/<id do usuário>
app.get('/users/:id', (request, response) => {
    fs.readFile(__dirname + '/users.json', 'utf8', (error, data) => {
        console.log("Requeridos dados de um usuário");
        data = JSON.parse(data.toString().replace(regex, ""));

        // Recuperando dados de ID da URL
        let id   = request.params.id;
        let user = data["user" + id];
        console.log(user);
        response.end(JSON.stringify(user));
    });
});

let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log("Servidor iniciado em http://%s:%s", host, port);
});

Teste REST

curl http://localhost:8081/users/2

Servidor RESTful completo

Código

server.js:

const express = require('express');
const fs      = require('fs');

let app = express();
app.use(express.json());
let regex = /\0/g;

app.get('/users/list', (request, response) => {
    fs.readFile(__dirname + '/users.json', 'utf8', (error, data) => {
        console.log("Requerida uma listagem de usuários");
        console.log(data);
        response.end(data);
    });
});

app.post('/users/add', (request, response) => {
    console.log("Adicionando novo usuário:");
    console.log(request.body);

    fs.readFile(__dirname + "/users.json", 'utf8', (error, data) => {
        data = JSON.parse(data.toString().replace(regex, ""));
        data = Object.assign(data, request.body);
        let string_data = JSON.stringify(data);

        fs.writeFile(__dirname + "/users.json", string_data, 'utf8', (error) => {
            if(error) {
                console.log(error);
                response.status(500).end();
            } else {
                response.end(JSON.stringify(request.body));
            }
        });
    });
});

app.delete('/users/delete', (request, response) => {
    let id = request.body["id"];
    console.log(`Deletando usuário com ID = ${id}`);
    
    fs.readFile(__dirname + "/users.json", 'utf8', (error, data) => {
        data = JSON.parse(data.toString().replace(regex, ""));
        delete data["user" + id];
        
        let string_data = JSON.stringify(data);
        fs.writeFile(__dirname + "/users.json", string_data, 'utf8', (error) => {
            if(error) {
                console.log(error);
                response.status(500).end();
            } else {
                response.end(string_data);
            }
        });
    });
});

app.get('/users/:id', (request, response) => {
    fs.readFile(__dirname + '/users.json', 'utf8', (error, data) => {
        console.log("Requeridos dados de um usuário");
        data = JSON.parse(data.toString().replace(regex, ""));

        let id   = request.params.id;
        let user = data["user" + id];
        console.log(user);
        response.end(JSON.stringify(user));
    });
});

let server = app.listen(8081, () => {
    let host = server.address().address;
    let port = server.address().port;

    console.log("Servidor iniciado em http://%s:%s", host, port);
});

Testes REST

echo "Listando usuários"
curl http://localhost:8081/users/list
printf "\n\n"

echo "Adicionando um usuário"
curl --header "Content-Type: application/json" \
     --request POST \
     --data '{ "user4": {"name":"John Doe", "password":"foobarbazquux", "profession":"Professor", "id":4} }' \
     http://127.0.0.1:8081/users/add
printf "\n\n"

echo "Listando os dados do usuário adicionado"
curl http://localhost:8081/users/4
printf "\n\n"

echo "Removendo o usuário adicionado"
curl --header "Content-Type: application/json" \
     --request DELETE \
     --data '{ "id": 4 }' \
     http://127.0.0.1:8081/users/delete
printf "\n\n"

echo "Listando todos os usuários novamente"
curl http://localhost:8081/users/list

Exercícios

De volta à página anterior