sexta-feira, 25 de julho de 2014

Curso de MEAN - Sessão 3: RESTful Webservices com Express.js

Sessão 3: RESTful Webservices com Express.js

Vamos aprender a criar Webservices REST/JSON como Express.js, além de servir páginas estáticas também. Ele organiza a aplicação e agiliza o desenvolvimento.


O trabalho Workshop de Desenvolvimento com Stack MEAN de Cleuton Sampaio de Melo Jr está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual 4.0 Internacional.

Isso inclui: Textos, páginas, gráficos e código-fonte.

O Express é um framework de criação de aplicações Web (Websites, Webservices) para o Node.js. Ele organiza sua aplicação e provê uma série de recursos, como, por exemplo, o compartilhamento automático de arquivos via Web. É ideal para criação de RESTful Webservices, pois organiza as rotas baseadas em URI.

Por que eu preciso do Express?


Boa pergunta... Vamos pensar em uma aplicação típica, por exemplo, um Webservice que busque os “artistas” em nosso banco de dados do exemplo anterior, retornando um JSON. Até aí, é simples, não? É só retornar o “Content-type” como “application/json”. Não precisamos do Express para isso.

Porém, e se quisermos criar uma API REST ? Ou seja, criarmos um conjunto de URIs, que nos permita acessar Artistas? Por exemplo:
  • Obter um determinado artista: “/artista/Lenny+Kravitz;
  • Obter uma lista de artistas: “/artistas”;

Teríamos que manipular a URL do Request para saber o que está sendo pedido. Nosso código iria ficar mais complexo, pois teríamos que fazer muita coisa.

E tem outro problema: Além da API REST, queremos criar algumas páginas para acessá-la. Onde hospedaríamos isso? Em um Servidor Apache? Se quiséssemos que essas páginas fossem obtidas a partir do Node.js, teríamos que escrever código para isso, conforme já vimos na primeira Aula.

O Express resolve esses dois problemas (API REST e Servir arquivos via Web), de maneira transparente para nós.

Para você entender como o Express funciona, nada melhor que um “Test drive”.

Test drive com o Express.js


Antes de mais nada, estamos usando o Express 4, que apresenta algumas diferenças para as versões anteriores. Se você já leu algum artigo sobre o Express, pode encontrar algumas diferenças.

Vamos começar:

  1. Crie uma pasta onde você deseja criar seus projetos Express. O ideal é que a pasta superior, onde você a criou, não contenha uma pasta “node_modules”. Se tiver, apague-a;
  2. Digite “express exemplo”, onde “exemplo” é o nome do projeto que queremos criar;
  3. O executável do Express criou um projeto, dentro de uma subpasta chamada “exemplo”. Vá para essa subpasta;
  4. Digite “npm install”, para que o “npm” instale todas as dependências do projeto recém criado;
  5. Agora, inicie a aplicação com “npm start”;
  6. Abra outra janela Terminal e digite: “wget -qO - localhost:3000”, ou então, abra um navegador e vá para “http://localhost:3000”.

Atenção: O “npm” não consegue compilar nada em partições NTFS montadas no Linux, então, se você estiver rodando o Linux em uma VM sobre Microsoft Windows, use uma pasta nativa, do File System da VM, ao invés de usar uma pasta compartilhada.

Bem, o que você fez? Criou uma aplicação Node.js, que usa o Express!

Vamos ver as pastas criadas:


O arquivo que está selecionado, chamado: “www”, dentro de “exemplo/bin”, é o principal “executável” da nossa aplicação. Se abrirmos o arquivo “package.json”, veremos que a declaração “start” manda executá-lo:

{
  "name": "exemplo",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "express": "~4.2.0",
    "static-favicon": "~1.0.0",
    "morgan": "~1.0.0",
    "cookie-parser": "~1.0.1",
    "body-parser": "~1.0.0",
    "debug": "~0.7.4",
    "jade": "~1.3.0"
  }
}

Temos várias dependências incluídas automaticamente pelo assistente do Express. Mas não se preocupe com isso agora.

Na pasta raiz do projeto gerado (“exemplo”), temos os arquivos:

  • app.js” : Contém definições principais da aplicação, suas rotas e módulos carregados;
  • package.json” : Contém a descrição do projeto e suas dependências.

Nas versões mais antigas do Express, o arquivo “app.js” servia para iniciar a aplicação (“node app.js”), agora, o gerador cria isso dentro do arquivo “www”.

A pasta “routes” é muito importante, pois ela define as “rotas” REST de nossa aplicação, baseadas na URI utilizada pelo Cliente. É aqui que a lógica de negócio é realmente definida. Cara arquivo pode conter várias rotas.

A pasta “views” contém templates Jade para geração de respostas HTML. Ao invés de ficar enviando strings HTML como resposta (como fizemos até agora), podemos usar um template e “preencher as lacunas”, deixando que o Jade gere o string HTML de resposta. Podemos enviar o HTML “na mão”, ou podemos usar outro mecanismo de templates, como o “ejs” (“Embedded Javascript - http://embeddedjs.com/).

A pasta “public” contém todos os arquivos que podem ser solicitados pelo Cliente. Se você quiser deixar algum arquivo para ser baixado, como: Imagens, Javascripts ou arquivos CSS, é nessa pasta que você deve colocar. O que indica qual é a pasta “pública” é uma configuração feita no “middleware” (módulo) “express-static”, dentro do arquivo “app.js”:

app.use(express.static(path.join(__dirname, 'public')));


Agora, vamos ver um pouco sobre a arquitetura de uma aplicação Express.



O Express trabalha com módulos do Node.js. Alguns desses módulos são chamados de “middleware” e desempenham funções arquiteturais na aplicação. Por exemplo, temos o “Router” e o “Static”, dois “middlewares” que usamos constantemente.

Quando chega um request a uma aplicação Express, ele é analisado para saber qual “middleware” vai lidar com ele. Se for um request para uma página estática, o “express-static” vai atendê-lo, enviando o arquivo solicitado. O default é buscar esses arquivos estáticos na pasta “public”.

Se o request for para uma “rota” definida, então o “Router” vai direcionar para a adequada, de acordo com a URI. O código da “rota” pode usar o Jade para gerar uma resposta HTML, ou pode enviar um JSON como resposta.

Como é que eu crio uma aplicação Express?




Uma aplicação Node/Express pode ser completa, ou seja, retorna até páginas HTML, ou pode ser apenas um RESTful Webservice, que retorna objetos JSON.



O que eu mais tenho visto é a arquitetura de “Single Page Application” (http://en.wikipedia.org/wiki/Single-page_application), na qual existe uma página única, baixada a partir do Express, que depois fará requisições dinâmicas, usando Ajax e REST. Eis um gráfico dessa arquitetura:

Normalmente, temos um request inicial para uma página estática, que pode ser gerada com o Jade. Esta página faz requests para outros recursos, como: arquivos CSS, imagens e Javascripts. Depois, os elementos da página podem realizar requests Ajax para nossa API REST, que responderá com documentos JSON (“Content-type: application/json”). Então, usando jQuery ou Angular.js, os elementos da página são modificados para espelhar a resposta (através do DOM).



Agora, se sua aplicação Node/Express tem como cliente uma aplicação móvel, pode ser que não tenha o conteúdo estático inicial.



Você pode criar uma aplicação Express do zero, sem usar o executável “Express”. Porém, isso é trabalhoso e não agrega valor ao seu software. O melhor é criar uma aplicação com o próprio Express e alterá-la para suas necessidades. O que precisamos fazer é:
  1. Configuração: Importar os módulos necessários. Se vamos usar “mongoose”, precisamos importar o módulo e criar o nosso esquema do Banco;
  2. Rotas: Precisamos indicar ao Express quais são as URIs possíveis e quais são os arquivos de rota para atendê-las;
  3. Templates: Criar os templates Jade. Se vamos retornar páginas HTML;
  4. Lógica: Codificar as rotas de acordo com a URI, dentro do arquivo de rotas apropriado.



Vamos ver os elementos da aplicação de acordo com essas etapas descritas acima.



Configuração

O arquivo “app.js” é o local apropriado para configurar seus módulos. Vamos ver como é um “app.js” gerado pelo Express:


var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.use('/users', users);
/// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});
/// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});
module.exports = app;
O ideal é incluir toda a sua configuração de módulos ANTES do último comando do arquivo “module.exports = app”.



Por exemplo, vamos supor que vamos usar o “mongoose” com aquele nosso banco de artistas. Nossa configuração poderia ser assim:


// Database:
mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
     throw new Error('erro ao conectar com o banco: ' + erro);
    }
    else {
     console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
 { 
  nome : String
 }
);
Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');
schemaArtista = new mongoose.Schema(
 {
     "nome" : String, 
     "pais" : String, 
     "musicas" : [ { "titulo" : String }], 
     "estilo_id" : {type: mongoose.Schema.Types.ObjectId, ref: 'Estilo'}
 }
);
Artista = mongoose.model('Artista',schemaArtista, 'artista');
module.exports = app;

Rotas

No arquivo “app.js”, temos duas configurações de rota baseadas em URI:



app.use('/', routes);
app.use('/users', users);



Esse comando “app.use” associa um “middleware” (um módulo Node.js) a uma URI (ou “mount path”). Se vier um request que contenha “/users”, o “middleware” “users” será executado, caso contrário, se vier um request apenas com “/”, o “middleware” “index” será executado.



Mas o que são esses “middlewares”? Eles foram definidos anteriormente, veja só:



var routes = require('./routes/index');
var users = require('./routes/users');



São módulos Node.js (http://nodejs.org/docs/latest/api/modules.html), que criamos em nossa aplicação. Por exemplo, o módulo “index.js” (pasta “routes”), contém esses comandos:



var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});

module.exports = router;



O que define realmente um módulo é o fato dele exportar alguma coisa. Neste caso, ele está exportanto uma instância de Router.



Neste arquivo, temos um “callback” associado a uma URI. Como já definimos no “app.js”, se a URI contiver apenas “/”, essa rota será invocada. E, dentro da rota, temos um único “callback”. Isso poderia ser diferente, por exemplo:


pp.js:



var routes = require('./routes/index');
...
app.use('/', routes);



routes/index.js:



/* Retorna a página inicial com a lista dos artistas */
router.get('/artistas', function(req, res) {
...
});

/* Retorna um artista como um objeto JSON */
router.get('/artista/:nomeartista', function(req, res) {
});



Neste caso, temos um só arquivo de rotas e o comando “app.use('/', routes)” desvia todos os Requests para ele. Dentro do arquivo de rotas (“routes/index.js”), temos duas “subrotas”:
  • ../artistas” : Se o request for para listar os artistas;
  • ../artista/:nomeartista” : Se o request for para trazer os dados de um artista específico;



Assim, combinando o “app.use()” (“app.js”) com o “router.get()” (arquivo de rotas), podemos montar uma hierarquia de URIs e definir os “callbacks” a serem executados para cada uma delas.



Templates

Se você for gerar HTML como resposta, não precisa de template algum. Porém, como nós geramos a aplicação usando o Jade (default), eu recomendo que você o utilize.



Como funciona? Bem, você manda o Express gerar uma resposta e passa o nome de um arquivo de template, além das variáveis a serem substituídas na geração do HTML. Vamos ver o arquivo de rotas “routes/index.js”:


var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});


module.exports = router;

O método “res.render(view, [locals], callback)”, do objeto Response, usa um arquivo de template, para gerar um HTML e enviar ao Cliente. Eis os argumentos:
  • “view” : Nome do template, sem a extensão “jade”;
  • “locals” : Objetos JSON para substituir os “place holders” dentro do template;
  • “callback” : Um objeto “function (err, html)”, que nos permite interceptar o html gerado. Se passarmos um “callback”, a resposta não será enviada automaticamente ao Cliente.



É claro que você pode usar o método “res.send()” para enviar conteúdo sem passar pelo Jade. Mas vamos ver como ele é útil.



Como o Express sabe qual é o template engine a ser usado e onde buscar as views? Nós informamos isso no “app.js”:



// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');



Voltando à nossa rota “routes/index.js”, nós mandamos renderizar com esse comando: “res.render('index', { title: 'Express' });”, então, vamos ver o arquivo “views/index.jade”:


extends layout
block content
  h1= title
  p Welcome to #{title}

Para começar, o comando “extends layout” informa que esse template herda um template chamado “layout.jade”, logo, o Jade vai carregar os dois na memória. Eis o template pai (“views/layout.jade”):


doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

O template “index.jade” redefine o template “layout.jade”, informando um conteúdo para o bloco de texto chamado “content”. O HTML resultante da combinação desses dois templates é assim:


<!DOCTYPE html>
<html>
  <head>
    <title>Express</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
  </head>
  <body>
    <h1>Express</h1>
    <p>Welcome to Express</p>
  </body>
</html>

A parte em negrito corresponde ao bloco “content”, alterado pelo template “index”. Bem, a principal vantagem do Jade é a simplificação do HTML. Basta usara identação para criar “ninhos” de tags HTML:

Código do template:

doctype html
html
  head

HTML gerado:

<!DOCTYPE html>
<html>
  <head>

Quando identamos um tag, o Jade entende que há um “ninho” e automaticamente cria o tag final. A identação deve ser exata, ou seja, se você começou o tag “head” com identação de dois espaços, o tab “body” também deve ser.


Exemplo:

Código do template:

ul
  li Item A
  li Item B
  li Item C

HTML gerado:

<ul>
  <li>Item A</li>
  <li>Item B</li>
  <li>Item C</li>
</ul>

Atributos HTML são expressos entre parêntesis e separados por vírgulas:

Código do template:

a(class='button', href='google.com') Google

HTML gerado:

<a href="google.com" class="button">Google</a>

Variáveis:

Quando mandamos renderizar um template, passamos objetos JSON para o método “res.render()”. Estes objetos JSON podem ser acessados dentro do template, por exemplo:

routes/index.js:

res.render('index', { title: 'Express' });


views/layout.jade:

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

views/index.jade:

extends layout
block content
  h1= title
  p Welcome to #{title}

Vemos duas maneiras de usar o objeto “title”, que foi passado pelo método “render()”:
  • Como conteúdo único de um tag: “= title”;
  • Concatenado com um texto: “#{title}”;

Na verdade, tem muito mais coisas que podemos fazer com o Jade, mas não vamos explicar tudo aqui. Se você quiser saber mais, tem esses dois sites:

Lógica

É o que você vai fazer dentro do “callback” de cada rota, de modo a atender ao request. Por exemplo, você pode acessar um banco de dados e montar uma resposta com Jade ou então enviar um JSON.

Montando resposta com Jade:


/* Retorna a página inicial com a lista dos artistas */
router.get('/artistas', function(req, res) {
 Artista
     .find({})
     .populate('estilo_id')
     .exec(function(erro,Artistas) {
  if (!erro) {
   if(Artistas) {
                    res.render('index', { title: 'Catalogo de artistas', 
                        lista : Artistas });
   }
   else {
      throw new Error('Nao encontrado');
   }
  }
  else {
      throw new Error('Erro ao acessar o banco: ' + erro);
  }
 }); 
});

Montando uma reposta com JSON:

/* Retorna um artista como um objeto JSON */
router.get('/artista/:nomeartista', function(req, res) {
 Artista
     .findOne({ nome : {$regex : req.params.nomeartista, 
                  $options : 'i'}})
     .populate('estilo_id')
     .exec(function(erro, artista) {
  if (!erro) {
   if(artista) {
             res.json(200,artista);
   }
   else {
      throw new Error('Nao encontrado');
   }
  }
  else {
      throw new Error('Erro ao acessar o banco: ' + erro);
  }
 }); 
});

Exercício

Vamos fazer um exercício bem simples. Você vai criar uma aplicação express que retorna os artistas, dentro de uma página HTML. Não precisa formatar cada artista em HTML, apenas retorne o JSON resultante do “find()” em uma variável Jade. Use o “exemplo” como base.  



Resposta: “sessao3/respostas/exercicio1”.

Exercício 2


Ok. Funcionou, mas ficou uma bela porcaria! Vamos fazer bonitinho. Para começar, vamos criar duas rotas:
  • “/artistas” : Retorna uma página HTML com todos os artistas, formatados como uma UL. Mostra o nome e, quando clica em um artista, mostra os detalhes;
  • “/artista/:nomedoartista” : Retorna os dados de um artista como JSON;

Eu quero algo assim:  


Não se preocupe! Vou dar a “cola”...

Quando vier uma URI “/artistas”, a aplicação vai retornar uma página HTML como essa:

<!DOCTYPE html>
  <html>
    <head>
      <title>Catalogo de artistas</title>
      <link rel="stylesheet" href="/stylesheets/style.css">
      <script type="text/javascript" src="javascripts/jquery-2.1.1.min.js">
      </script>
      <script type="text/javascript" src="javascripts/index-code.js">
      </script>
    </head>
    <body>
      <h1>Catalogo de artistas</h1>
      <p>Bem vindo ao Catalogo de artistas</p>
      <ul>
        <li class="links">Lenny Kravitz</li>
        <li class="links">Fulano de Tal</li>
      </ul>
      <div id="saida">
      </div>
    </body>
  </html>
Notou esses dois Javascripts? Um é a versão do jQuery (http://jquery.com/download/) e o outro é um script que vamos criar para tratar os eventos da página. Ambos devem ser colocados em “public/javascripts”.

Como eu vou saber quantos artistas tem? Eu preciso fazer um “loop” com o Jade! E ele faz exatamente isso! Veja só, se invocarmos o Jade assim:

res.render('index', { title: 'Catalogo de artistas', lista : Artistas });


Teremos duas variáveis para trabalhar no Jade: “title” e “lista”, que é uma lista de artistas. Então, podemos fazer o seguinte:

extends layout
block content
  h1= title
  p Bem vindo ao #{title}
  ul
    each artista in lista
      li(class='links')= artista.nome
  div(id='saida')

O “each” funciona igual ao “foreach”, pegando cada elemento do vetor “lista” e colocando na variável local “artista”. Então, podemos criar um “<li>” para cada artista da lista.

E podemos usar o jQuery para preencher a “<div>” “saida” com os dados de um artista selecionado. Para isto, enviamos outro request à nossa aplicação, com a URI “/artista/:nomedoartista”. Vamos ver como ficaria isso no jQuery:



public/javascripts/index-code.js:

$(document).ready(function() {
  $("li").click(function() {
      $.getJSON('/artista/' + this.textContent, function(artista) {
        var saida = '<ul><li>Nome: ' + artista.nome + '</li>'
                  + '<li>Estilo: ' + artista.estilo_id.nome + '</li>'
                  + '<li>Musicas<table border>';
        $.each(artista.musicas, function(key,val) {
            saida += '<tr><th>' + val.titulo + '</th></tr>';
        });
        $("#saida").html(saida);
      });
  });
});
Sempre que um tag “<li>” for clicado, pegamos o seu conteúdo (“this.textContent”) e usamos para montar um request REST à nossa aplicação. Quando o request for completado, nós substituímos o conteúdo da div “saida”.

Não conseguiu? Então veja em “sessao3/respostas/exercicio2



Nenhum comentário:

Postar um comentário