Desenvolvendo REST APIs com NodeJS – Parte I

É crescente o número de aplicações que utilizam APIs. A facilidade de criar uma aplicação cujo backend e o frontend se comunicam apenas por requisições HTTP é muito maior atualmente do que quanto tínhamos apenas a possibilidade de fazer chamadas via Ajax e manipular o DOM com JQuery.

Surgiram vários frameworks de fontend voltados para criar SPAs (Single Page Apps), que são aplicações de página única, cujo os conteúdos são trocados por javascript sem que toda a página seja renderizada. Exemplos desses frameworks são: Angular, React, VueJS, EmberJS, entre outros.

Também aumentou a necessidade de troca de informação entre um servidor de aplicação e um App de smartphone. Desta forma, a necessidade de se ter APIs independentes da apresentação do conteúdo vem se tornando cada vez mais forte.

Neste primeiro tutorial sobre o desenvolvimento de APIs, vamos explorar os passos básicos para criação de uma API: conceito de REST API, preparação do ambiente de desenvolvimento e criação de alguns serviços.

Para um próximo artigo, complementar a este, vamos abordar a parte de autenticação e segurança dos serviços. O código fonte pode ser baixado no seguinte link: https://github.com/CodDev2018/node-api

Veja aqui a parte II deste tutorial: Desenvolvendo REST APIs com NodeJS – Parte II

Então vamos lá!

REST

Se você já ouviu falar em APIs ultimamente, provavelmente deve ter ouvido falar de REST (Representational State Transfer), que nada mais é que um estilo arquitetural que define um conjunto de regras baseados em HTTP.  Um web service RESTful, ou seja, implementado obedecendo a arquitetura REST, é interoperável via internet e fornece para o utilizador, acesso e possibilidade de manipulação dos recursos web por representações textuais.

Um conjunto de serviços reste para o recurso Usuários poderia ser:

Perceba que a identificação de um recurso (URL) é sempre a mesma, o que muda são os protocolos HTTP (GET, POST, PUT e DELETE).

Crie um site profissional em apenas 24 horas.

Preparando o Ambiente

Primeiramente para se desenvolver em NodeJS é necessário efetuar a sua instalação. Para instalar o NodeJS você pode:

  1. Baixar o instalador do site do node de acordo com o seu sistema operacional: https://nodejs.org.
  2. Ou instalar via o NVM (Node Version Manager): https://github.com/creationix/nvm.

O NVM é um gerenciador de versão do NodeJS, que vai facilitar muito sua vida quando necessitar utilizar diferentes versões do Node. NVM tem apenas um problema, não suporta Windows! 😥

MongoDB

Utilizaremos neste exemplo o MongoDB para armazenar nossos dados. O MongoDB é um banco de dados não relacional, também conhecido como banco de dados NoSQL, por não utilizar SQL para realização das suas consultas. Outra peculiaridade é que os dados ficam armazenados em esquemas semelhantes ao JSON. Para baixar o MongoDB acesse: https://www.mongodb.com.

Visual Studio Code

Para escrever o código fonte você pode optar pelo Visual Studio Code, que é altamente compatível com o código ECMAScript que iremos escrever. Acesse https://code.visualstudio.com/ para baixar e instalar o VSC.

Criando projeto

Agora com tudo instalado vamos iniciar um novo projeto. O primeiro passo é a criação de pasta, vou supor que você está utilizando ou Linux ou pelo menos um terminal compatível com o Linux como o Cmder por exemplo.

$ mkdir node-api
$ cd node-api
$ npm init --force

Com isso, será criado dentro do diretório node-api um arquivo chamado package.json. Este arquivo trás todas as configurações das dependências do nosso projeto. E por falar em dependências, vamos adicionar algumas:

$ npm install express --save
$ npm install body-parser --save

Agora com as dependências instaladas o seu arquivo package.json deve ter ficado parecido com este aqui:

{
  "name": "node-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
  }
}

Vamos ao código fonte

Como deve ter visto nas dependências, acrescentamos o Express, este é o nosso framework arquitetural, ou seja é ele quem faz todo controle da execução do código, chamando o código adequado para responder cada uma das requisições recebidas pela aplicação. Ele também levanta nossa aplicação em uma determinada porta que é por onde a aplicação irá receber e responder as requisições. Para saber mais sobre o Express, e ver sua documentação completa acesse: https://expressjs.com/.

Primeiro passo é criar nosso arquivo principal da aplicação que configura todos os outros componentes utilizados vamos chamar este arquivo de server.js.

var express = require('express');
var app = express();
var bodyParser = require('body-parser');

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}));

//Routes
app.get('/test', function(req, res){
   res.json('OK')
})

//Escutando porta
app.listen(PORT)
console.log('Magic happens on port ' + PORT);
exports = module.exports = app;

Pelo terminal você pode executar a nossa API com o momando node server.js para que a API seja servida em localhost:3000.

Nodemon

Algo muito chato que está acontecendo com nossa aplicação é que a cada alteração teremos que rodar novamente o comando node server.js para que a aplicação seja servida com a nova alteração. A maneira mais simples de resolver este problema, é instalando o modulo do NodeJS, o nodemon, e subir a aplicação utilizando ele. Este módulo além de servir a aplicação, faz o reload ao detectar que alguma alteração ocorreu.

Para instalar vamos dar o comando de instalação passando -g para que o modulo seja instalado globalmente, ou seja, fique acessível em qualquer diretório do sistema operacional.

$ npm install -g nodemon

Além de instalar o módulo que ira permitir executar nossa API, vamos configurar para que seja possível executar levantar nossa aplicação com o comando npm start. Para tanto, vamos acrescentar uma configuração no arquivo package.json conforme a seguir:

"scripts": {
   "start": "nodemon server.js"
},

Com isso, bastar executar o comando npm start que nossa aplicação já será servida com o nodemon e não precisaremos reiniciar a cada alteração de código.

Crie Single Page Application com Vue.JS

Estrutura de pastas

Para continuar vamos criar uma estrutura de pasta para que o código fonte fique armazenado da forma mais lógica possível. Criaremos dentro do diretório raiz uma pasta com o nome de /src e dentro desta pasta mais duas outras com o nome de /src/model e /src/controller. A estrutura do projeto deve ficar assim:

pastas

Criando modelos

Vamos começar a criar os modelos para nossa REST API. A idéia é um miniblog onde qualquer usuário cadastrado uma postagem. Os nossos modelos serão apenas um schema de usuário e outro de postagens.

Para trabalhar com os modelo precisamos de uma camadaque normalmente é dependente de um ORM (Object Relational Mappers), que é o padrão de projeto que mapeia dados de um banco de dados relacional para um modelo orientado a objeto.

Mas no caso do MongoDB, que não é um banco de dados relacional, não utilizaremos um ORM e sim um ODM (Object Document Mapper). Por ser orientado a documentos JSON sem relacionamentos e com interface de acesso sem SQL, um tradicional ORM não adiantaria nada para nós, precisamos de um mapeamento que saiba traduzir objetos em documentos do MongoDB.

Um dos ODMs mais conhecidos para MongoDB em NodeJS é certamente o Mongoose e é este que iremos utilizar. Para instalar:

$ npm install mongoose --save

Agora configuramos a conexão com o banco de dados em nossa aplicação, alterando o arquivo server.js e incluindo antes das linhas com as rotas.

...

// Banco de dados
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/node_api', { useNewUrlParser: true });

//Rotas
...

Tendo o Mongoose instalado e configurado em nosso projeto podemos criar os arquivos /src/model/user.js e /src/model/post.js, a seguir os códigos fonte:

  • /src/model/user.js
var mongoose = require('mongoose')

var userScheme = new mongoose.Schema({
    username: { type: String, require: true },
    password: { type: String, require: true },
});

exports = module.exports = mongoose.model('User', userScheme);
  • /src/model/post.js
var mongoose = require('mongoose')

var postScheme = new mongoose.Schema({
    titulo: { type: String, require: true },
    conteudo: { type: String, require: true },
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});

exports = module.exports = mongoose.model('Post', postScheme);

A declaração do postScheme tem uma referencia ao userSchema, mas perceba que o valor armazenado no banco de dados é do tipo ObjectId, que é a referencia única para um usuário no userSchema.

Vale ressaltar que esta referencia é apenas lógica, MongoDB não se preocupa com integridade referencial, ou outras coisas, que são implícitas de banco de dados relacionais. Desta forma ao excluir um usuário, seus posts continuariam intactos. Provavelmente dará um erro ao tentar acessar um post e pedir que o Mongoose mapeie também o user que já não existe mais no banco de dados. Então cabe a você tomar cuidado com as exclusões.

Controladores e rotas

Agora criaremos os seguintes arquivos:

  • /src/routes.js
  • /src/controller/userController.js
  • /src/controller/postController.js

Poderíamos criar as rotas e codificar tudo dentro do arquivo server.js como fizemos no primeiro exemplo que tínhamos a rota /test, mas imagine que um sistema cresce e pode ter centenas de rotas, ficaria inviável a manutenção. Então vamos separar nosso código de forma lógica logo no inicio para que seja mais simples mante-ló depois.

O primeiro passo é definir o arquivo de rotas e já vamos criar todas as rotas logo de início, assim já saberemos os métodos que precisaremos trabalhar em nossos controladores. Segue código do arquivo /src/routes.js:

var UserController = require('./controller/UserController')
var PostController = require('./controller/PostController')

module.exports = function (app) {
    //USER
    app.get('/users', UserController.index)
    app.post('/user', UserController.save)
    app.get('/user/:id', UserController.get)
    app.put('/user/:id', UserController.update)
    app.delete('/user/:id', UserController.delete)

    //POST
    app.get('/user/:userId/posts', PostController.index)
    app.post('/user/:userId/post', PostController.save)
    app.get('/user/:userId/post/:id', PostController.get)
    app.put('/user/:userId/post/:id', PostController.update)
    app.delete('/user/:userId/post/:id', PostController.delete)
}

Por enquanto temos apenas mapeada as rotas para nossos controladores fazerem o trabalho sujo. Vamos ao primeiro controlador que é o /src/controller/userController.js.

var User = require('../model/user')

class UserController {

    async index(req, res) {
        var users = await User.find().exec()
        res.json(users)
    }

    async save (req, res) {
        var user = await new User({
            username: req.body.username,
            password: req.body.password
        }).save()

        res.json(user)
    }

    async get(req, res) {
        var user = await User.findOne({ _id: req.params.id }).exec()
        if (!user) {
            return res.status(404).json({ 'message': 'Usuário não encontrado' })
        }

        return res.json(user)
    }

    async update(req, res) {
        var user = await User.findOne({ _id: req.params.id }).exec()
        if (!user) {
            return res.status(404).json({ 'message': 'Usuário não encontrado' })
        }
        user.username = req.body.username
        user.password = req.body.password
        user = await user.save()
     
        return res.json(user)
    }

    async delete(req, res) {
        await User.findByIdAndRemove(req.params.id)
        return res.status(204).json({})
    }
}

exports = module.exports = new UserController()

Observe que utilizamos todos os métodos assinados com async e nas operações com banco de dados utilizamos o await, esta dupla async/await, é um recurso interessante do JavaScript, pois facilita bastante o trabalho com promises. Caso não utilizássemos este recurso, teríamos que tratar os promises passando callbacks. O problema é quando um callback fica aninhado dentro de outro callback, temos assim um código com indentação cada vez mais longe da margem esquerda, formando aquele belo “hadouken”.

1_Co0gr64Uo5kSg89ukFD2dw

Basicamente o async avisa ao executor do método que o resultado depende de promises ou devolve uma promise, e o await avisa que o método que está sendo executado devolve uma promise e deve esperar este retorno para continuar executando o restante do código.

A seguir o código do arquivo /src/controller/postController.js:

var User = require('../model/user')
var Post = require('../model/post')

class PostController {
    async index (req, res) {
        var posts = await Post.find({ user: req.param.userId }).exec()
        res.json(posts)
    }
    
    async save (req, res) {
        var user = await User.findOne({ _id: req.params.userId }).exec()
        if (!user) {
            return res.status(404).json({ 'message': 'Usuário não encontrado' })
        }
        var post = await new Post({
            titulo: req.body.titulo,
            conteudo: req.body.conteudo,
            user: user
        }).save()
    
        res.json(post)
    }
    
    async get (req, res) {
        var post = await Post.findOne({
            _id: req.params.id,
            user: req.params.userId
        }).populate('user').exec()
        if (!post) {
            return res.status(404).json({ 'message': 'Post não encontrado' })
        }
        return res.json(post)
    }
    
    async update (req, res) {
        var post = await Post.findOne({
            _id: req.params.id,
            user: req.params.userId
        }).populate('user').exec()
        if (!post) {
            return res.status(404).json({ 'message': 'Post não encontrado' })
        }
        post.titulo = req.body.titulo
        post.conteudo = req.body.conteudo
    
        post = await post.save()
    
        return res.json(post)
    }
    
    async delete (req, res) {
        var post = await Post.findOne({
            _id: req.params.id,
            user: req.params.userId
        }).populate('user').exec()
        if (!post) {
            return res.status(404).json({ 'message': 'Post não encontrado' })
        }
        await Post.findByIdAndRemove(req.params.id)
        return res.status(204).json({})
    }
}

exports = module.exports = new PostController()

Temos um bad smells relativo a repetição do código que utilizando para consultar o post. Podemos eliminar esta repetição criando um método para fazer a consulta e utilizar ele como um middleware no nível de rotas. Nosso middleware ficou assim:

async findPostMiddleware(req, res, next) {
    var post = await Post.findOne({
        _id: req.params.id,
        user: req.params.userId
    }).populate('user').exec()
    if (!post) {
        return res.status(404).json({ 'message': 'Post não encontrado' })
    }
    req.post = post
    next(null)
}

Veja que injetamos o post encontrado no objeto req. Desta forma a consulta de post ficou sendo feita em apenas um único local e não mais replicada por todo nosso código. Aproveitei e tratei o controller do usuário que também estava com um mau cheiro! Veja a seguir como ficou as duas classes:

  • /src/controller/postController.js:
var User = require('../model/user')
var Post = require('../model/post')

class PostController {
    async index (req, res) {
        var posts = await Post.find({ user: req.params.userId })
            .populate('user').exec()
        console.log(posts)
        res.json(posts)
    }
    
    async save (req, res) {
        var user = await User.findOne({ _id: req.params.userId }).exec()
        if (!user) {
            return res.status(404).json(
                { 'message': 'Usuário não encontrado' })
        }
        var post = await new Post({
            titulo: req.body.titulo,
            conteudo: req.body.conteudo,
            user: user
        }).save()
    
        res.json(post)
    }
    
    async get (req, res) {
        return res.json(req.post)
    }
    
    async update (req, res) {
        req.post.titulo = req.body.titulo
        req.post.conteudo = req.body.conteudo
    
        req.post = await req.post.save()
    
        return res.json(req.post)
    }
    
    async delete (req, res) {
        await Post.findByIdAndRemove(req.params.id)
        return res.status(204).json({})
    }

    async findPostMiddleware(req, res, next) {
        var post = await Post.findOne({
            _id: req.params.id,
            user: req.params.userId
        }).populate('user').exec()
        if (!post) {
            return res.status(404).json(
               { 'message': 'Post não encontrado' })
        }
        req.post = post
        next(null)
    }
}

exports = module.exports = new PostController()
  • /src/controller/userController.js:
var User = require('../model/user')

class UserController {

    async index(req, res) {
        var users = await User.find().exec()
        res.json(users)
    }

    async save (req, res) {
        var user = await new User({
            username: req.body.username,
            password: req.body.password
        }).save()

        res.json(user)
    }

    async get(req, res) {
        return res.json(req.user)
    }

    async update(req, res) {
        req.user.username = req.body.username
        req.user.password = req.body.password

        req.user = await req.user.save()

        return res.json(req.user)
    }

    async delete(req, res) {
        await User.findByIdAndRemove(req.params.id)
        return res.status(204).json({})
    }

    async findUserMiddleware(req, res, next) {
        var user = await User.findOne({ _id: req.params.id }).exec()
        if (!user) {
            return res.status(404).json(
                   { 'message': 'Usuário não encontrado' })
        }
        req.user = user
        next()
    }
}

exports = module.exports = new UserController()

E finalmente nosso arquivo de rotas que ficou assim:

var UserController = require('./controller/UserController')
var PostController = require('./controller/PostController')

module.exports = function (app) {
    //USER
    app.get('/users', UserController.index)
    app.post('/user', UserController.save)
    app.get('/user/:id', UserController.findUserMiddleware, UserController.get)
    app.put('/user/:id', UserController.findUserMiddleware, UserController.update)
    app.delete('/user/:id', UserController.delete)

    //POST
    app.get('/user/:userId/posts', PostController.index)
    app.post('/user/:userId/post', PostController.save)
    app.get('/user/:userId/post/:id', PostController.findPostMiddleware, PostController.get)
    app.put('/user/:userId/post/:id', PostController.findPostMiddleware, PostController.update)
    app.delete('/user/:userId/post/:id', PostController.findPostMiddleware, PostController.delete)
}

E para que tudo isso funcione precisamos fazer algumas alteração no server.js, que deve ficar da seguinte forma:

const PORT = process.env.PORT || 3000;

var express = require('express');
var app = express();
var bodyParser = require('body-parser');

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }));

// Banco de dados
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/node_api', { useNewUrlParser: true });

//Routes
require('./src/routes')(app)

//Escutando porta
app.listen(PORT)

console.log('Magic happens on port ' + PORT);
exports = module.exports = app;

Neste ponto deve estar tudo funcionando, e você pode testar tudo utilizando o Postman. A seguir um print de exemplo de um teste realizado com o Postman:

user_post
Testando inclusão de usuário. Fique atendo ao marcar raw, para incluir o JSON da requisição, selecionar JSON(application/json) na caixa de seleção ao lado.

Conclusão

Vemos que é simples criar um web service baseado em arquitetura REST em formato JSON com NodeJS. É muito simples trabalhar com roteamentos e middlewares do Express, sem contar com a simplicidade de se trabalhar com o MongoDB com o Mongoose ODM.

Anteriormente, traduzi um post que explica detalhadamente como implantar uma aplicação Node.JS na DigitalOcean, veja: Tutorial: Como implantar seu aplicativo Node.js na DigitalOcean com SSL.

No próximo artigo iremos utilizar o Passport para fazer uma autenticação utilizando JWT (JSON Web Token), além de middlewares para validar se o usuário autenticado é o dono de fato dos dados requisitados.

Então até a próxima!

Deixe seu comentário, dúvida, reclamação, dicas, etc.

Compartilhe nas suas redes sociais e ajude o blog a crescer!

Um comentário em “Desenvolvendo REST APIs com NodeJS – Parte I

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.