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).
Instale a dependência EJS. O EJS (Embedded JavaScript) é um Template Engine (motor de template).
npm install ejsNa 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 RESTAinda 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/homeAgora, 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>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");
});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.
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;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.
- 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/.
- Para evitar repetições de código, pesquisem como usar páginas de layout em EJS e apliquem no site;