Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save santiagosilas/917ce1f42f1595195b9dda1c9ca56e02 to your computer and use it in GitHub Desktop.

Select an option

Save santiagosilas/917ce1f42f1595195b9dda1c9ca56e02 to your computer and use it in GitHub Desktop.

Definição de um Cliente Simples

Este tutorial é apresentado como continuação do projeto em desenvolvimento. Há um conjunto de conceitos heterogêneos como Backend (Expres / Rotas), Arquitetura (Controllers) e um pouco de Frontend (HTML, JS, EJS).

Passo 1

Instale a dependência EJS. O EJS (Embedded JavaScript) é um Template Engine (motor de template).

npm install ejs

Na raiz do projeto, crie uma pasta public/ contendo os arquivos public/scripts/site.js e public/styles/site.css. Adicione também os arquivos estáticos css e js referentes ao bootstrap.

Em src/, crie a pasta views/ com o arquivo home.ejs - ../src/views/home.ejs com o conteúdo:

<!DOCTYPE html>
<html lang="pt-br">
  <head>
    <meta charset="UTF-8" />
    <title>Home</title>

    <!-- "/" indica a raiz da pasta public -->
    <link rel="stylesheet" href="/styles/site.css" />
  </head>
  <body>
    <h1 id="titulo">Carregando . . .</h1>

    . . .

    <!-- Importando o arquivo JS da pasta public/js -->
    <script src="/scripts/site.js"></script>
  </body>
</html>

No arquivo ``site.js`, inclua:

async function carregarDadosSite() {
  const res = await fetch("/api/home");
  const dados = await res.json();

  const h1 = document.querySelector("#titulo");
  h1.textContent = dados.titulo;
}

window.onload = carregarDadosSite;

No código javascript do servidor, inclua no local apropriado os seguintes trechos:

Em app.js:

. . .
import path from "node:path";
. . .

Ainda em app.js:

// Configurações do EJS
app.set("view engine", "ejs");
app.set("views", path.join(import.meta.dirname, "views")); // Node.js 20.11+

Ainda em app.js:

// Middlewares
app.use(express.static(path.join(import.meta.dirname, "public")));
app.use(express.json()); // Importante para rotas REST

Ainda em app.js (pode-se imaginar outro local para organizar esta rota)

// --- ROTA DE VIEW (EJS) ---
app.get("/home", (req, res) => {
  res.render("home"); // Renderiza o esqueleto da página
});

Em api.routes.js (pode-se imaginar outro local para organizar esta rota)

router.get("/home", (req, res) => {
  const dados = {titulo: "Loja Virtual"};
  res.json(dados); // Entrega o JSON puro
});

Agora, acessem:

http://localhost:3000/home

Passo 2

Agora, adicione arquivos estáticos do bootstrap à página home:

<!DOCTYPE html>
<html lang="pt-br">
  <head>
    <meta charset="UTF-8" />
    <title>Home</title>

    <!-- / indica a raiz da pasta public -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="/styles/site.css" />
  </head>
  <body>
    <h1 id="titulo">Carregando . . .</h1>

    . . .

    <!-- Importando o arquivo JS da pasta public/js -->
    <script
      src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
      integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js"
      integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"
      crossorigin="anonymous"
    ></script>
    <script src="/scripts/site.js"></script>
  </body>
</html>

Passo 3

Criem mais três páginas na pasta views: about.ejs, contact.ejs e products.ejs. Por enquanto, não é necessário nenhum conteúdo no body, apenas um elemento <h1>TEXTO</h1>.

Na página home, adicione links para as outras páginas:

<a href="/home">Home</a>
<a href="/about">Sobre</a>
<a href="/contact">Contato</a>
<a href="/products">Produtos</a>

Para estas páginas serem acessíveis, defina rotas para renderizá-las:

// --- ROTA DE VIEW (EJS) ---
app.get("/home", (req, res) => {
  res.render("home"); // Renderiza o esqueleto da página
});
app.get("/about", (req, res) => {
  res.render("about");
});
app.get("/contact", (req, res) => {
  res.render("contact");
});
app.get("/products", (req, res) => {
  res.render("products");
});

Passo 4

Vamos agora listar produtos na página de produtos. Edite products.ejs para incluir o seguinte código:

<div class="container">
  <% listaProdutos.forEach(produto => { %>
  <div class="card">
    <h3><%= produto.nome %></h3>

    <p>Status: <%= produto.disponivel ? "Em estoque" : "Indisponível" %></p>
  </div>
  <% }); %>
</div>

Você também deve editar a rota do seguinte modo:

app.get("/products", (req, res) => {
  const products = [
    {id: 1, nome: "Teclado", disponivel: true},
    {id: 2, nome: "Mouse", disponivel: true},
    {id: 3, nome: "Monitor", disponivel: false},
    {id: 4, nome: "Caixa de Som", disponivel: false},
  ];
  res.render("products", {listaProdutos: products});
});

Teste a rota products. Chame o professor para conferência.

Passo 5

Faremos uma próxima refatoração (atenção: há uma mudança de lógica - da renderização direta no servidor para a busca via fetch no cliente). A listagem de produtos será obtida a partir de uma chamada à API via fetch. Crie uma classe controller com uma rota para retornar a lista de produtos.

Em src/controllers/products.controller.js:

export default class ProductsController {
  constructor() {}

  async getProducts(req, res, next) {
    try {
      const products = [
        {id: 1, nome: "Teclado", disponivel: true},
        {id: 2, nome: "Mouse", disponivel: true},
        {id: 3, nome: "Monitor", disponivel: false},
        {id: 4, nome: "Caixa de Som", disponivel: false},
      ];
      res.json({productsList: products});
    } catch (error) {
      next(error);
    }
  }
}

Em src/routes/products.routes.js:

import {Router} from "express";
import ProductsController from "../controllers/products.controller.js";

const router = Router();

const controller = new ProductsController();

router.get("/products", (req, res, next) =>
  controller.getProducts(req, res, next),
);

export {router};

Em app.js:

. . .
. . .


import {router as apiRouter} from "./routes/api.routes.js";
import {router as productsRouter} from "./routes/products.routes.js";

. . .
. . .

app.use("/api", apiRouter);
app.use("/api", productsRouter);

. . .
. . .


app.get("/products", (req, res) => {
  res.render("products"); // o cliente obterá os dados via fetch
});

Em src/views/products.ejs:

<!DOCTYPE html>
<html lang="pt-br">
  <head>
    . . .
  </head>
  <body>
    <h1>Produtos</h1>

    <div id="container-produtos" class="container"></div>

    . . .

    <script src="/scripts/products.js"></script>
  </body>
</html>

Em public/scripts/products.js:

async function carregarProdutos() {
  const response = await fetch("/api/products");
  const dados = await response.json();

  const container = document.getElementById("container-produtos");
  console.log(dados);
  container.innerHTML = dados["productsList"]
    .map(
      (p) => `
      <div class="card">
        <h3>${p.nome}</h3>
        <p>${p.disponivel ? "Disponível" : "Esgotado"}</p>
        <a href="/products/${p.id}">Ver detalhes</a>
      </div>
    `,
    )
    .join("");
}

window.onload = carregarProdutos;

Passo 6

Arquivo src/controllers/products.controller.js:

  async getProductById(req, res, next) {
    try {
      const {id} = req.params; // Captura o "id" da URL
      const product = {id: 1, nome: "Teclado", disponivel: true}; // simulação de acesso ao BD

      if (!product) {
        return res.status(404).json({message: "Produto não encontrado"});
      }

      res.json(product);
    } catch (error) {
      next(error);
    }
  }

Arquivo src/routes/products.routes.js:

. . .

router.get("/products/:id", (req, res, next) =>
  controller.getProductById(req, res, next),
);

. . .

Arquivo product-details.ejs

<!DOCTYPE html>
<html lang="pt-br">
  <head>
    <meta charset="UTF-8" />
    <title>Detalhes do Produto</title>

    <!-- / indica a raiz da pasta public -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="/styles/site.css" />
  </head>
  <body>
    <h1>Detalhes do Produto</h1>

    <div id="container-produto" class="container"></div>

    <!-- Importando o arquivo JS da pasta public/js -->
    <script
      src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
      integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js"
      integrity="sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"
      crossorigin="anonymous"
    ></script>
    <script src="/scripts/product-details.js"></script>
  </body>
</html>

Arquivo public/scripts/product-details.js

async function carregarDetalhesProduto() {
  const path = window.location.pathname; // Pega o caminho da URL (ex: "/products/3")

  const partes = path.split("/");
  const id = partes[partes.length - 1]; // Divide a string pelas barras e pega o último elemento
  console.log(id);

  const response = await fetch(`/api/products/${id}`); // Usa o ID dinâmico no fetch
  const produto = await response.json();
  const container = document.getElementById("container-produto");

  container.innerHTML = `
      <div class="card">
        <h3>${produto.nome}</h3>
        <p>${produto.disponivel ? "Disponível" : "Esgotado"}</p>
        <p>${produto.id}</p>
      </div>
    `;
}

window.onload = carregarDetalhesProduto;

No arquivo app.js, adicione a rota para detalhes de produto:

app.get("/products/:id", (req, res) => {
  res.render("product-details"); // o cliente obterá os dados via fetch
});

Teste novamente a rota products. Chame o professor para conferência.

Passo 7 (Passo adicional)

  • Para não precisar copiar os links de navegação e os scripts do Bootstrap em todas as páginas, pesquise como criar um arquivo header.ejs e footer.ejs na pasta views e usar o comando <%- include('header') %> para evitar repetição de código;
  • Adicionem os arquivos .js e .css do bootstrap nas pastas scripts/ e styles/.

Passo 8 (Passo adicional)

  • Para evitar repetições de código, pesquisem como usar páginas de layout em EJS e apliquem no site;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment