Advertisement
  1. Code
  2. Node.js

Projeto de APIs RESTful com NodeJS e Restify

Scroll to top
Read Time: 14 min

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

Uma API RESTful consiste de dois conceitos principais: Recurso e Representação. Um Recurso pode ser qualquer objeto associado a um dado ou identificado a uma URI (mais de uma URI pode referir-se a um mesmo recurso), e que pode ser operado usando métodos HTTP. Uma Representação é uma forma de apresentar um recurso. Neste tutorial, passaremos um conteúdo teórico sobre projetos de APIs RESTful e implementaremos uma API de blog, usando o NodeJS.

Recurso

Escolher os recursos corretos para uma API RESTful é uma parte importante do projeto. Primeiro de tudo, você precisa analisar o domínio do seu negócio e, depois, decidir quantos e quais recursos serão usados e que são relevantes às necessidades do seu negócio. Se você estiver projetando uma API para um blog, provavelmente usará recursos como Artigo, Usuário e Comentário. Esses são os nomes dos recursos e os dados associados a eles são o recurso em si:

1
{
2
"title": "Como Projetar uma API RESTful",
3
    "content": "Projeto de API RESTful é muito importante no mundo do desenvolvimento de software.",
4
    "author": "erickpatrick",
5
    "tags": [
6
        "technology",
7
        "nodejs",
8
        "node-restify"
9
        ]
10
    "category": "NodeJS"
11
}

Verbos de Recursos

Podemos prosseguir com a operação do recurso após decidir quais são os recursos obrigatório. Operações referem-se aos métodos HTTP. Por exemplo, para criarmos um artigo, podemos realizar a seguinte requisição:

1
POST /articles HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json
4
5
{
6
  "title": "Projeto de API RESTful com Restify",
7
  "slug": "projeto-api-rest-restify",
8
  "content": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",
9
  "author": "erickpatrick"
10
}

Da mesma forma, podemos visualizar um artigo realizando a seguinte requisição:

1
GET /articles/123456789012 HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json

E quanto a atualizar um artigo já existente? Sei muito bem o que você está pensando:

Posso realizar outra requisição POST em relação a /articles/update/123456789012 com os dados a serem atualizados.

Talvez, mas a URI está ficando um pouco mais complexa. Como disse mais cedo, as operações referem-se aos métodos HTTP. Isso significa que devemos apresentar a operação de atualização no método HTTP ao invés de colocá-lo na URI. Por exemplo:

1
PUT /articles/123456789012 HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json
4
{
5
"title": "Como Projetar um API RESTful - Atualizada",
6
    "content": "Como Projetar um API RESTful - Atualizada - é muito importante no mundo do desenvolvimento de software.",
7
    "author": "erickpatrick",
8
    "tags": [
9
        "tecnologia",
10
        "nodejs",
11
        "restify",
12
        "outra tag"
13
        ]
14
    "category": "NodeJS"
15
}

Por falar em alterações, neste exemplo podemos ver campos de tags e categorias. Eles não são campos obrigatórios. Você pode deixá-los em branco e preenchê-los no futuro. 

Algumas vezes, você precisa remover um artigo que esteja desatualizado. Nestes casos, você pode usar uma requisição HTTP do tipo DELETE em relação à URI /articles/123456789012.

Os métodos HTTP são conceitos padrões. Se usá-los como operações, você terá URIs mais simples e esse tipo de API simples ajudará você a obter clientes, clientes felizes.

E se você quisesse publicar um comentário em um artigo? Você pode selecionar o artigo e adicionar um comentário novo a ele. Com essa expressão em mente, você pode realizar a seguinte requisição:

1
POST /articles/123456789012/comments HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json
4
{
5
"text": "Wow! Este é um ótimo tutorial",
6
    "author": "Zé niguém"
7
}

A forma de recurso acima é chamada de sub-recurso. Comentário é um sub-recurso de Artigo. Os Comentário e sua carga acima serão inseridos na base de dados como um filho de Artigo. Algumas vezes, uma URI diferente refere-se a uma mesmo recurso. Por exemplo, para ver um comentário específico, você pode usar tanto:

1
GET /articles/123456789012/comments/123 HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json

ou:

1
GET /comments/123456789012 HTTP/1.1
2
Host: localhost:3000
3
Content-Type: application/json

Versionamento

Geralmente, funcionalidades de uma API alteram-se frequentemente, para que se possa oferecer novas funcionalidades aos clientes. Neste caso, duas versões de uma mesma API podem existir ao mesmo tempo. Para separá-las, você pode usar versionamento. Há duas formas de versionamento

  1. Versão na URI: Você pode prover o número de versão na própria URI. Por exemplo, /v1.1/articles/123456789012.
  2. Versão no Cabeçalho: Você pode prover o número da versão no cabeçalho e nunca precisar mudar a URI. Por exemplo:
1
GET /articles/123456789012 HTTP/1.1
2
Host: localhost:3000
3
Accept-Version: 1.0

Na verdade, a versão apenas altera a representação do recurso, não o conceito do recurso em si. Então, você não precisa alterar a estrutura do URI. Na v1.1, talvez um novo campo seja adicionado ao Artigo. Entretanto, ele ainda retorna um artigo. Na segunda opção, a URI continua simples e os clientes não precisam alterar as URIs em suas implementações. 

É importante projetar uma estratégia para situações onde o cliente não provê um número de versão. Você pode lançar um erro quando um número de versão não é apresentado ou você pode retornar uma resposta usando a primeira versão. Se você usar a última versão estável como a versão padrão, os clientes podem obter vários erros em suas implementações.

Representação

A Representação é a forma como uma API apresenta um recurso. Ao invocar uma API através de uma URI, você receberá um recurso. Esse recurso pode estar em um formato como o XML, JSON, etc. É preferido o formato JSON caso esteja projetando uma nova API. Entretanto, se você estiver atualizando uma API já existente e que já é acostumada a retornar uma resposta XML, você pode prover outra versão com uma resposta em JSON. 

Certo, deixemos a teoria sobre projetos de APIs RESTful de lado. Vejamos como projetar e implementar uma API para blogs usando o Restify.

API RESTful de um Blog

Projeto

Para projetarmos uma API RESTful, precisamos analisar o domínio do negócio. Só então podemos definir nossos recursos. Em uma API de blogs, precisamos:

  • Criar, Atualizar, Apagar e Visualizar um Artigo
  • Criar comentários para um Artigo específico, além de Atualizar, Apagar e Visualizar um Comentário
  • Criar, Atualizar, Apagar e Visualizar um Usuário

Nesta API, não cobrirei a parte de autenticação necessária para criar um artigo ou comentário. Para essa parte, você pode referir-se ao artigo Autenticação com Tokens Usando AngularJS & NodeJS

Os nomes dos nossos recursos estão prontos. Operações de recursos são CRUD simples. Você pode referir-se à tabela a seguir para ter uma visualização geral da API.

Nome do Recurso Verbos HTTP Métodos HTTP
Artigo criar Artigo, atualizar Artigo, remover Artigo, visualizar Artigo POST /articles com os dados, PUT /articles/123 com os dados, DELETE /articles/123 e GET /article/123
Comentário criar Comentário, atualizar Comentário, remover Comentário, visualizar Comentário POST /articles/123/comments com os dados, PUT /comments/123 com os dados, DELETE /comments/123 e GET /comments/123
Usuário criar Usuário, atualizar Usuário, remover Usuário, visualizar Usuário POST /users com os dados, PUT /users/123 com os dados, DELETE /users/123 e GET /users/123

Configuração do Projeto

Neste projeto, usaremos o NodeJS com Restify. Os recursos serão salvos em uma base de dados MongoDB. Antes de tudo, devemos definir os recursos como modelos no Restify.

Artigo

1
var mongoose = require("mongoose");
2
var Schema   = mongoose.Schema;
3
4
var ArtigoSchema = new Schema({
5
title: String,
6
    slug: String,
7
    content: String,
8
    author: {
9
        type: String,
10
        ref: "Usuario"
11
    }
12
});
13
mongoose.model('Artigo', ArtigoSchema);

Comentários

1
var mongoose = require("mongoose");
2
var Schema   = mongoose.Schema;
3
4
var ComentarioSchema = new Schema({
5
text: String,
6
    article: {
7
        type: String,
8
        ref: "Artigo"
9
    },
10
    author: {
11
        type: String,
12
        ref: "Usuario"
13
    }
14
});
15
mongoose.model('Comentarios', ComentariosSchema);

Usuário

Não haverá operações para o recurso Usuário. Assumiremos que já sabemos o usuário atual que será capaz de operar os artigos ou comentários.

Você deve se perguntar de onde esse módulo mongoose vem. É o framework ORM para MongoDB escrito como módulo para NodeJS mais conhecido. Este módulo está incluso no projeto, dentro de outro arquivo de configuração. 

Agora, podemos definir nossos verbos HTTP para os recursos acima. Você pode ver abaixo:

1
var restify = require('restify')
2
, fs = require('fs')
3
4
5
var controllers = {}
6
    , controllers_path = process.cwd() + '/app/controllers'
7
fs.readdirSync(controllers_path).forEach(function (file) {
8
    if (file.indexOf('.js') != -1) {
9
        controllers[file.split('.')[0]] = require(controllers_path + '/' + file)
10
    }
11
})
12
13
var server = restify.createServer();
14
15
server
16
    .use(restify.fullResponse())
17
    .use(restify.bodyParser())
18
19
// Início dos Artigos

20
server.post("/articles", controllers.article.createArtigo)
21
server.put("/articles/:id", controllers.article.updateArtigo)
22
server.del("/articles/:id", controllers.article.deleteArtigo)
23
server.get({path: "/articles/:id", version: "1.0.0"}, controllers.article.viewArtigo)
24
server.get({path: "/articles/:id", version: "2.0.0"}, controllers.article.viewArtigo_v2)
25
// Fim dos Artigos

26
27
// Início dos Comentarios

28
server.post("/comments", controllers.comment.createComentarios)
29
server.put("/comments/:id", controllers.comment.viewComentarios)
30
server.del("/comments/:id", controllers.comment.deleteComentarios)
31
server.get("/comments/:id", controllers.comment.viewComentarios)
32
// Fim dos Comentarios

33
34
var port = process.env.PORT || 3000;
35
server.listen(port, function (err) {
36
    if (err)
37
        console.error(err)
38
    else
39
        console.log('Aplicativo pronto na porta: ' + port)
40
})
41
42
if (process.env.environment == 'production')
43
    process.on('uncaughtException', function (err) {
44
        console.error(JSON.parse(JSON.stringify(err, ['stack', 'message', 'inner'], 2)))
45
    })

Neste trecho de código, a primeira coisa feita é iterar sobre todos os arquivos dos controladores e inicializá-los, para que uma requisição específica possa ser realizada. Após isso, URIs para operações específicas são definidas em relação às operações CRUD básicas. Também há versionamento para uma das operações em relação ao recurso Artigo

Por exemplo, se você configurar a versão como sendo a 2 no cabeçalho Accept-Version, o método viewArtigo_v2 será executado. viewArtigo e viewArtigo_v2 realizam o mesmo trabalho, apresentando o recurso, mas mostram o recurso Artigo em formatos diferentes, como pode ser visto através do campo title, no exemplo abaixo. Finalmente, o servidor é iniciado numa porta específica, além da aplicação de alguns relatórios de erros. Podemos dar continuidade com os métodos dos controladores mapeados às operações HTTP em relação aos recursos.

article.js

1
var mongoose = require('mongoose'),
2
Artigo = mongoose.model("Artigo"),
3
    ObjectId = mongoose.Types.ObjectId
4
5
exports.createArtigo = function(req, res, next) {
6
    var articleModel = new Artigo(req.body);
7
    articleModel.save(function(err, article) {
8
        if (err) {
9
            res.status(500);
10
            res.json({
11
                type: false,
12
                data: "Erro ocorrido: " + err
13
            })
14
        } else {
15
            res.json({
16
                type: true,
17
                data: article
18
            })
19
        }
20
    })
21
}
22
23
exports.viewArtigo = function(req, res, next) {
24
    Artigo.findById(new ObjectId(req.params.id), function(err, article) {
25
        if (err) {
26
            res.status(500);
27
            res.json({
28
                type: false,
29
                data: "Erro ocorrido: " + err
30
            })
31
        } else {
32
            if (article) {
33
                res.json({
34
                    type: true,
35
                    data: article
36
                })
37
            } else {
38
                res.json({
39
                    type: false,
40
                    data: "Artigo: " + req.params.id + " não encontrado"
41
                })
42
            }
43
        }
44
    })
45
}
46
47
exports.viewArtigo_v2 = function(req, res, next) {
48
    Artigo.findById(new ObjectId(req.params.id), function(err, article) {
49
        if (err) {
50
            res.status(500);
51
            res.json({
52
                type: false,
53
                data: "Erro ocorrido: " + err
54
            })
55
        } else {
56
            if (article) {
57
                article.title = article.title + " v2"
58
                res.json({
59
                    type: true,
60
                    data: article
61
                })
62
            } else {
63
                res.json({
64
                    type: false,
65
                    data: "Artigo: " + req.params.id + " não encontrado"
66
                })
67
            }
68
        }
69
    })
70
}
71
72
exports.updateArtigo = function(req, res, next) {
73
    var updatedArtigoModel = new Artigo(req.body);
74
    Artigo.findByIdAndUpdate(new ObjectId(req.params.id), updatedArtigoModel, function(err, article) {
75
        if (err) {
76
            res.status(500);
77
            res.json({
78
                type: false,
79
                data: "Erro ocorrido: " + err
80
            })
81
        } else {
82
            if (article) {
83
                res.json({
84
                    type: true,
85
                    data: article
86
                })
87
            } else {
88
                res.json({
89
                    type: false,
90
                    data: "Artigo: " + req.params.id + " não encontrado"
91
                })
92
            }
93
        }
94
    })
95
}
96
97
exports.deleteArtigo = function(req, res, next) {
98
    Artigo.findByIdAndRemove(new Object(req.params.id), function(err, article) {
99
        if (err) {
100
            res.status(500);
101
            res.json({
102
                type: false,
103
                data: "Erro ocorrido: " + err
104
            })
105
        } else {
106
            res.json({
107
                type: true,
108
                data: "Artigo: " + req.params.id + " removido com sucesso"
109
            })
110
        }
111
    })
112
}

Uma explicação básica das operações CRUD em relação ao Mongoose pode ser visto logo abaixo:

  • createArtigo: A operação save simplesmente salva os dados recebidos no corpo da requisição, usando o modelo articleModel. Um novo objeto Artigo pode ser criado, enviando os dados do corpo da requisição para o modelo em questão, dessa forma var articleModel = new Artigo(req.body)
  • viewArtigo: Para vermos os detalhes do artigo, é preciso que tenhamos o ID de um artigo como parâmetro na URL. O método findOne usando o ID do parâmetro é mais que o suficiente para retornar os detalhes do artigo.
  • updateArtigo: A atualização de um Artigo é uma simples consulta e manipulação de dados em relação ao artigo retornado. Por fim, o artigo atualizado precisa ser salvo na base de dados, através do comando save.
  • deleteArtigo: O método findByIdAndRemove é a melhor forma de removermos um artigo a partir do ID de um artigo.

Os comandos do Mongoose mencionados acima são quase como métodos estáticos do objeto Artigo, o qual é uma referência a um esquema do Mongoose.

comment.js

1
var mongoose = require('mongoose'),
2
Comentários = mongoose.model("Comentarios"),
3
    Artigo = mongoose.model("Artigo"),
4
    ObjectId = mongoose.Types.ObjectId
5
6
exports.viewComentarios = function(req, res) {
7
    Artigo.findOne({"comments._id": new ObjectId(req.params.id)}, {"comments.$": 1}, function(err, comment) {
8
        if (err) {
9
            res.status(500);
10
            res.json({
11
                type: false,
12
                data: "Erro ocorrido: " + err
13
            })
14
        } else {
15
            if (comment) {
16
                res.json({
17
                    type: true,
18
                    data: new Comentarios(comment.comments[0])
19
                })
20
            } else {
21
                res.json({
22
                    type: false,
23
                    data: "Comentário: " + req.params.id + " não encontrado"
24
                })
25
            }
26
        }
27
    })
28
}
29
30
exports.updateComentarios = function(req, res, next) {
31
    var updatedComentariosModel = new Comentarios(req.body);
32
    console.log(updatedComentariosModel)
33
    Artigo.update(
34
        {"comments._id": new ObjectId(req.params.id)},
35
        {"$set": {"comments.$.text": updatedComentariosModel.text, "comments.$.author": updatedComentariosModel.author}},
36
        function(err) {
37
            if (err) {
38
                res.status(500);
39
                res.json({
40
                    type: false,
41
                    data: "Erro ocorrido: " + err
42
                })
43
            } else {
44
                res.json({
45
                    type: true,
46
                    data: "Comentário: " + req.params.id + " atualizado"
47
                })
48
            }
49
    })
50
}
51
52
exports.deleteComentarios = function(req, res, next) {
53
    Artigo.findOneAndUpdate({"comments._id": new ObjectId(req.params.id)},
54
        {"$pull": {"comments": {"_id": new ObjectId(req.params.id)}}},
55
        function(err, article) {
56
        if (err) {
57
            res.status(500);
58
            res.json({
59
                type: false,
60
                data: "Erro ocorrido: " + err
61
            })
62
        } else {
63
            if (article) {
64
                res.json({
65
                    type: true,
66
                    data: article
67
                })
68
            } else {
69
                res.json({
70
                    type: false,
71
                    data: "Comentários: " + req.params.id + " não encontrado"
72
                })
73
            }
74
        }
75
    })
76
}

Ao realizar uma requisição a uma das URIs, a função declarada no controlador será executada. Cada função dentro dos arquivos de controladores pode usar os objetos req e res. O recurso comentário logo acima, é um sub-recurso de Artigo. Todas as operações de consultas são realizadas através do modelo Artigo para, então, encontrarmos o subdocumento e realizar as atualizações necessárias. Contudo, toda vez que tentamos visualizar um recurso do tipo Comentário, você verá algo, mesmo que não exista uma coleção no MongoDB.  

Outras Sugestões de Design

  • Escolha recursos simples de entender para prover uso fácil aos clientes;
  • Não deixe a lógica do negócio ser implementada pelos clientes. Por exemplo, o recurso Artigo tem um campo chamado slug. Os clientes não precisam enviar esse detalhe pela API RESTful. A estratégia em relação ao slug deve ser administrada no lado da API RESTful para minimizar o acoplamento entre a API e os clientes. Os clientes só precisam enviar o título e você pode gerar o slug de acordo com as necessidades do negócio, no lado da API RESTful.
  • Implemente uma camada de autorização para suas APIs. Clientes não autorizados não podem acessar dados restritos pertencentes a outros usuários. Neste tutorial, não falamos sobre o recurso Usuário, mas você pode ler o artigo Autenticação com Tokens Usando AngularJS & NodeJS para mais informações em relação a autenticação de APIs.
  • URIs amigáveis ao invés de vetor de consulta. /articles/123 é bom, /articles?id=123 é ruim.
  • Não mantenha estados; sempre use entrada/saída instantâneas.
  • Use substantivos para seus recursos. Você poderá usar os métodos/verbos HTTP para operar sobre os recursos.

Finalmente, se você projetar uma API RESTful seguindo essas regras fundamentais, você sempre terá um sistema flexível, manutenível e compreensível.

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.