Iniciante 3 horas

Aula 3: Introdução ao Express

Aprenda a criar seu primeiro servidor Express, compreender rotas básicas e servir arquivos estáticos

🎯 Objetivos da Aula

  • Criar primeiro servidor Express
  • Compreender rotas básicas
  • Servir arquivos estáticos

📋 Legenda das Atividades

🔨 CÓDIGO PRÁTICO Atividades para implementar e praticar
📖 EXEMPLO TEÓRICO Conceitos e exemplos para estudo
📖 EXEMPLO TEÓRICO

PASSO 1: Introdução Completa ao Express.js

Objetivos da Aula: Aprender a criar servidores web com Express.js, entender rotas, middleware e como servir arquivos estáticos. Ao final desta aula, você será capaz de criar um servidor web completo do zero!

🤔 O que é um Framework Web?

Imagine que você quer construir uma casa. Você poderia fazer tudo do zero: misturar cimento, cortar madeira, fazer a fiação elétrica... Ou você poderia usar materiais pré-fabricados que facilitam o trabalho.

Express.js é como um "kit de construção" para servidores web! Ele fornece peças prontas (funções, métodos) que facilitam muito a criação de aplicações web, sem precisar programar tudo do zero.

🌐 O que é Express.js?

Express.js é um framework web minimalista e flexível para Node.js que fornece um conjunto robusto de recursos para aplicações web e móveis. Ele é como uma "caixa de ferramentas" que torna muito mais fácil criar servidores web.

🔧 Sem Express (Node.js puro)

  • • Muito código para tarefas simples
  • • Difícil de organizar
  • • Muita repetição
  • • Mais propenso a erros

⚡ Com Express

  • • Código limpo e organizado
  • • Funcionalidades prontas
  • • Fácil de entender
  • • Desenvolvimento mais rápido

Agora que você já domina os fundamentos do Node.js e o sistema de módulos, é hora de dar o próximo passo: criar aplicações web! O Express.js é o framework web mais popular para Node.js.

Com Express, você pode criar APIs robustas, aplicações web completas e microserviços de forma rápida e eficiente.

💡 Curiosidade

Express.js é usado por empresas gigantes como Netflix, Uber, WhatsApp, IBM e muitas outras! Aprender Express é uma habilidade muito valorizada no mercado de trabalho.

9.2 Melhorando o JavaScript - produtos.js

Agora vamos criar um arquivo JavaScript dedicado para a página de produtos, com funcionalidades avançadas como filtros, busca e tratamento de erros.

📁 public/js/produtos.js - JavaScript Avançado

// Variáveis globais
let todosProdutos = [];
let produtosFiltrados = [];
let categoriaAtual = 'todos';

// Elementos do DOM
const produtosContainer = document.getElementById('produtos-container');
const loadingElement = document.getElementById('loading');
const erroContainer = document.getElementById('erro-container');
const filtroButtons = document.querySelectorAll('.filtro-btn');

// Função para mostrar loading
function mostrarLoading() {
    loadingElement.style.display = 'block';
    produtosContainer.style.display = 'none';
    erroContainer.style.display = 'none';
}

// Função para esconder loading
function esconderLoading() {
    loadingElement.style.display = 'none';
    produtosContainer.style.display = 'block';
}

// Função para mostrar erro
function mostrarErro(mensagem = 'Erro ao carregar produtos') {
    loadingElement.style.display = 'none';
    produtosContainer.style.display = 'none';
    erroContainer.style.display = 'block';
    erroContainer.querySelector('p').textContent = `❌ ${mensagem}`;
}

// Função para buscar produtos da API
async function buscarProdutos() {
    try {
        mostrarLoading();
        
        const response = await fetch('/api/produtos');
        
        if (!response.ok) {
            throw new Error(`Erro HTTP: ${response.status}`);
        }
        
        const produtos = await response.json();
        
        if (!Array.isArray(produtos)) {
            throw new Error('Formato de dados inválido');
        }
        
        todosProdutos = produtos;
        produtosFiltrados = produtos;
        
        esconderLoading();
        renderizarProdutos();
        
    } catch (error) {
        console.error('Erro ao buscar produtos:', error);
        mostrarErro(`Erro ao carregar produtos: ${error.message}`);
    }
}

// Função para buscar categorias da API
async function buscarCategorias() {
    try {
        const response = await fetch('/api/categorias');
        
        if (!response.ok) {
            throw new Error(`Erro HTTP: ${response.status}`);
        }
        
        const categorias = await response.json();
        return categorias;
        
    } catch (error) {
        console.error('Erro ao buscar categorias:', error);
        return [];
    }
}

// Função para obter nome da categoria
function obterNomeCategoria(categoriaId) {
    const categorias = {
        1: 'Pães',
        2: 'Doces', 
        3: 'Salgados'
    };
    return categorias[categoriaId] || 'Categoria';
}

// Função para renderizar um produto
function criarCardProduto(produto) {
    const disponibilidade = produto.disponivel ? 
        '✅ Disponível' :
        '❌ Indisponível';
    
    return `
        
${obterNomeCategoria(produto.categoria_id)}

${produto.nome}

${produto.descricao}

R$ ${produto.preco.toFixed(2)}
${disponibilidade}
`; } // Função para renderizar todos os produtos function renderizarProdutos() { if (produtosFiltrados.length === 0) { produtosContainer.innerHTML = `

😔 Nenhum produto encontrado para esta categoria.

`; return; } const produtosHTML = produtosFiltrados .map(produto => criarCardProduto(produto)) .join(''); produtosContainer.innerHTML = produtosHTML; } // Função para filtrar produtos por categoria function filtrarPorCategoria(categoria) { categoriaAtual = categoria; if (categoria === 'todos') { produtosFiltrados = todosProdutos; } else { produtosFiltrados = todosProdutos.filter( produto => produto.categoria_id.toString() === categoria ); } renderizarProdutos(); // Atualizar botões ativos filtroButtons.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.categoria === categoria) { btn.classList.add('active'); } }); } // Função para adicionar event listeners function adicionarEventListeners() { // Event listeners para filtros filtroButtons.forEach(button => { button.addEventListener('click', () => { const categoria = button.dataset.categoria; filtrarPorCategoria(categoria); }); }); // Event listener para retry em caso de erro erroContainer.addEventListener('click', () => { buscarProdutos(); }); } // Função de inicialização async function inicializar() { console.log('🚀 Inicializando página de produtos...'); adicionarEventListeners(); await buscarProdutos(); console.log('✅ Página de produtos carregada com sucesso!'); } // Inicializar quando o DOM estiver carregado document.addEventListener('DOMContentLoaded', inicializar);

🚀 Funcionalidades Avançadas:

  • Async/Await: Código moderno para requisições
  • Tratamento de Erros: Try/catch com mensagens específicas
  • Estados de Loading: Feedback visual durante carregamento
  • Filtros Dinâmicos: Filtrar produtos por categoria
  • Validação de Dados: Verificação de formato da API
  • Retry Automático: Clique no erro para tentar novamente

🔄 Melhorias no main.js Original

Também podemos melhorar o arquivo main.js original da página inicial:

📁 public/js/main.js - Versão Melhorada

// Versão melhorada do main.js com tratamento de erros
class PadariaApp {
    constructor() {
        this.categorias = [];
        this.categoriasContainer = document.getElementById('categorias-container');
        this.init();
    }
    
    async init() {
        console.log('🥖 Inicializando Padaria Doce Sabor...');
        
        try {
            await this.carregarCategorias();
            this.renderizarCategorias();
            console.log('✅ Aplicação carregada com sucesso!');
        } catch (error) {
            console.error('❌ Erro ao inicializar aplicação:', error);
            this.mostrarErroCarregamento();
        }
    }
    
    async carregarCategorias() {
        try {
            const response = await fetch('/api/categorias');
            
            if (!response.ok) {
                throw new Error(`Erro HTTP: ${response.status}`);
            }
            
            this.categorias = await response.json();
            
            if (!Array.isArray(this.categorias)) {
                throw new Error('Formato de dados inválido');
            }
            
        } catch (error) {
            console.error('Erro ao buscar categorias:', error);
            throw error;
        }
    }
    
    renderizarCategorias() {
        if (!this.categoriasContainer) {
            console.warn('Container de categorias não encontrado');
            return;
        }
        
        if (this.categorias.length === 0) {
            this.categoriasContainer.innerHTML = `
                

😔 Nenhuma categoria disponível no momento.

`; return; } const categoriasHTML = this.categorias .map(categoria => this.criarCardCategoria(categoria)) .join(''); this.categoriasContainer.innerHTML = categoriasHTML; // Adicionar event listeners this.adicionarEventListeners(); } criarCardCategoria(categoria) { return `

${categoria.nome}

${categoria.descricao}

`; } adicionarEventListeners() { const botoesProdutos = document.querySelectorAll('.btn-ver-produtos'); botoesProdutos.forEach(botao => { botao.addEventListener('click', (e) => { const categoriaId = e.target.dataset.categoria; this.navegarParaProdutos(categoriaId); }); }); } navegarParaProdutos(categoriaId = 'todos') { // Navegar para página de produtos com categoria específica const url = `/produtos.html?categoria=${categoriaId}`; window.location.href = url; } mostrarErroCarregamento() { if (this.categoriasContainer) { this.categoriasContainer.innerHTML = `

❌ Erro ao carregar categorias.

`; } } } // Inicializar aplicação quando DOM estiver carregado document.addEventListener('DOMContentLoaded', () => { new PadariaApp(); });

9.3 Rotas de API Avançadas - Operações CRUD

Vamos expandir nossa API com operações CRUD completas (Create, Read, Update, Delete) para produtos e adicionar funcionalidades como busca e paginação.

📁 app.js - APIs CRUD Completas

// Adicione estas rotas ao seu app.js existente

// ===== ROTAS PARA PRODUTOS =====

// GET /api/produtos/:id - Buscar produto específico
app.get('/api/produtos/:id', (req, res) => {
    try {
        const id = parseInt(req.params.id);
        
        if (isNaN(id)) {
            return res.status(400).json({
                erro: 'ID inválido',
                mensagem: 'O ID deve ser um número'
            });
        }
        
        const produto = produtos.find(p => p.id === id);
        
        if (!produto) {
            return res.status(404).json({
                erro: 'Produto não encontrado',
                mensagem: `Produto com ID ${id} não existe`
            });
        }
        
        res.json(produto);
    } catch (error) {
        res.status(500).json({
            erro: 'Erro interno do servidor',
            mensagem: error.message
        });
    }
});

// POST /api/produtos - Criar novo produto
app.post('/api/produtos', (req, res) => {
    try {
        const { nome, descricao, preco, categoria_id, disponivel } = req.body;
        
        // Validações
        if (!nome || !descricao || !preco || !categoria_id) {
            return res.status(400).json({
                erro: 'Dados obrigatórios ausentes',
                mensagem: 'Nome, descrição, preço e categoria são obrigatórios'
            });
        }
        
        if (typeof preco !== 'number' || preco <= 0) {
            return res.status(400).json({
                erro: 'Preço inválido',
                mensagem: 'O preço deve ser um número positivo'
            });
        }
        
        // Verificar se categoria existe
        const categoriaExiste = categorias.find(c => c.id === categoria_id);
        if (!categoriaExiste) {
            return res.status(400).json({
                erro: 'Categoria inválida',
                mensagem: 'A categoria especificada não existe'
            });
        }
        
        // Criar novo produto
        const novoId = Math.max(...produtos.map(p => p.id)) + 1;
        const novoProduto = {
            id: novoId,
            nome: nome.trim(),
            descricao: descricao.trim(),
            preco: parseFloat(preco),
            categoria_id,
            disponivel: disponivel !== false // default true
        };
        
        produtos.push(novoProduto);
        
        res.status(201).json({
            mensagem: 'Produto criado com sucesso',
            produto: novoProduto
        });
        
    } catch (error) {
        res.status(500).json({
            erro: 'Erro interno do servidor',
            mensagem: error.message
        });
    }
});

// PUT /api/produtos/:id - Atualizar produto
app.put('/api/produtos/:id', (req, res) => {
    try {
        const id = parseInt(req.params.id);
        const { nome, descricao, preco, categoria_id, disponivel } = req.body;
        
        if (isNaN(id)) {
            return res.status(400).json({
                erro: 'ID inválido',
                mensagem: 'O ID deve ser um número'
            });
        }
        
        const produtoIndex = produtos.findIndex(p => p.id === id);
        
        if (produtoIndex === -1) {
            return res.status(404).json({
                erro: 'Produto não encontrado',
                mensagem: `Produto com ID ${id} não existe`
            });
        }
        
        // Validações opcionais (só valida se fornecido)
        if (preco !== undefined && (typeof preco !== 'number' || preco <= 0)) {
            return res.status(400).json({
                erro: 'Preço inválido',
                mensagem: 'O preço deve ser um número positivo'
            });
        }
        
        if (categoria_id !== undefined) {
            const categoriaExiste = categorias.find(c => c.id === categoria_id);
            if (!categoriaExiste) {
                return res.status(400).json({
                    erro: 'Categoria inválida',
                    mensagem: 'A categoria especificada não existe'
                });
            }
        }
        
        // Atualizar produto
        const produtoAtualizado = {
            ...produtos[produtoIndex],
            ...(nome && { nome: nome.trim() }),
            ...(descricao && { descricao: descricao.trim() }),
            ...(preco !== undefined && { preco: parseFloat(preco) }),
            ...(categoria_id !== undefined && { categoria_id }),
            ...(disponivel !== undefined && { disponivel })
        };
        
        produtos[produtoIndex] = produtoAtualizado;
        
        res.json({
            mensagem: 'Produto atualizado com sucesso',
            produto: produtoAtualizado
        });
        
    } catch (error) {
        res.status(500).json({
            erro: 'Erro interno do servidor',
            mensagem: error.message
        });
    }
});

// DELETE /api/produtos/:id - Deletar produto
app.delete('/api/produtos/:id', (req, res) => {
    try {
        const id = parseInt(req.params.id);
        
        if (isNaN(id)) {
            return res.status(400).json({
                erro: 'ID inválido',
                mensagem: 'O ID deve ser um número'
            });
        }
        
        const produtoIndex = produtos.findIndex(p => p.id === id);
        
        if (produtoIndex === -1) {
            return res.status(404).json({
                erro: 'Produto não encontrado',
                mensagem: `Produto com ID ${id} não existe`
            });
        }
        
        const produtoRemovido = produtos.splice(produtoIndex, 1)[0];
        
        res.json({
            mensagem: 'Produto removido com sucesso',
            produto: produtoRemovido
        });
        
    } catch (error) {
        res.status(500).json({
            erro: 'Erro interno do servidor',
            mensagem: error.message
        });
    }
});

// GET /api/produtos/buscar - Buscar produtos
app.get('/api/produtos/buscar', (req, res) => {
    try {
        const { q, categoria, disponivel, limite, pagina } = req.query;
        
        let produtosFiltrados = [...produtos];
        
        // Filtro por texto (nome ou descrição)
        if (q) {
            const termo = q.toLowerCase();
            produtosFiltrados = produtosFiltrados.filter(produto => 
                produto.nome.toLowerCase().includes(termo) ||
                produto.descricao.toLowerCase().includes(termo)
            );
        }
        
        // Filtro por categoria
        if (categoria) {
            const categoriaId = parseInt(categoria);
            if (!isNaN(categoriaId)) {
                produtosFiltrados = produtosFiltrados.filter(
                    produto => produto.categoria_id === categoriaId
                );
            }
        }
        
        // Filtro por disponibilidade
        if (disponivel !== undefined) {
            const isDisponivel = disponivel === 'true';
            produtosFiltrados = produtosFiltrados.filter(
                produto => produto.disponivel === isDisponivel
            );
        }
        
        // Paginação
        const limitePorPagina = parseInt(limite) || 10;
        const paginaAtual = parseInt(pagina) || 1;
        const inicio = (paginaAtual - 1) * limitePorPagina;
        const fim = inicio + limitePorPagina;
        
        const produtosPaginados = produtosFiltrados.slice(inicio, fim);
        const totalProdutos = produtosFiltrados.length;
        const totalPaginas = Math.ceil(totalProdutos / limitePorPagina);
        
        res.json({
            produtos: produtosPaginados,
            paginacao: {
                pagina_atual: paginaAtual,
                total_paginas: totalPaginas,
                total_produtos: totalProdutos,
                produtos_por_pagina: limitePorPagina
            },
            filtros_aplicados: {
                busca: q || null,
                categoria: categoria || null,
                disponivel: disponivel || null
            }
        });
        
    } catch (error) {
        res.status(500).json({
            erro: 'Erro interno do servidor',
            mensagem: error.message
        });
    }
});

🔧 Operações CRUD Implementadas:

  • CREATE: POST /api/produtos - Criar novos produtos
  • READ: GET /api/produtos/:id - Buscar produto específico
  • UPDATE: PUT /api/produtos/:id - Atualizar produto
  • DELETE: DELETE /api/produtos/:id - Remover produto
  • SEARCH: GET /api/produtos/buscar - Busca avançada

🧪 Testando as APIs

Você pode testar essas APIs usando ferramentas como Postman, Insomnia ou curl:

💻 Exemplos de Teste com curl

# Buscar produto específico
curl http://localhost:3000/api/produtos/1

# Criar novo produto
curl -X POST http://localhost:3000/api/produtos \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "Croissant de Chocolate",
    "descricao": "Delicioso croissant recheado com chocolate",
    "preco": 8.50,
    "categoria_id": 2,
    "disponivel": true
  }'

# Atualizar produto
curl -X PUT http://localhost:3000/api/produtos/1 \
  -H "Content-Type: application/json" \
  -d '{
    "preco": 6.00,
    "disponivel": false
  }'

# Buscar produtos
curl "http://localhost:3000/api/produtos/buscar?q=pão&categoria=1&limite=5"

# Deletar produto
curl -X DELETE http://localhost:3000/api/produtos/1

🛡️ Validações Implementadas:

  • Validação de tipos: Verificação de números e strings
  • Campos obrigatórios: Validação de dados necessários
  • Existência de recursos: Verificação se produto/categoria existe
  • Tratamento de erros: Respostas padronizadas para erros
  • Códigos HTTP corretos: 200, 201, 400, 404, 500

9.4 Tratamento de Erros Avançado para APIs

Um bom tratamento de erros é essencial para APIs profissionais. Vamos implementar um sistema robusto de tratamento de erros com middleware personalizado e logs.

📁 middleware/errorHandler.js - Middleware de Erros

// Crie o arquivo middleware/errorHandler.js

// Classe personalizada para erros da API
class ApiError extends Error {
    constructor(message, statusCode, code = null) {
        super(message);
        this.statusCode = statusCode;
        this.code = code;
        this.isOperational = true;
        
        Error.captureStackTrace(this, this.constructor);
    }
}

// Middleware para capturar erros assíncronos
const asyncHandler = (fn) => {
    return (req, res, next) => {
        Promise.resolve(fn(req, res, next)).catch(next);
    };
};

// Middleware de tratamento de erros global
const errorHandler = (err, req, res, next) => {
    let error = { ...err };
    error.message = err.message;
    
    // Log do erro para desenvolvimento
    console.error('🚨 Erro capturado:', {
        message: err.message,
        stack: err.stack,
        url: req.originalUrl,
        method: req.method,
        ip: req.ip,
        timestamp: new Date().toISOString()
    });
    
    // Erro de validação do Mongoose (se usando MongoDB)
    if (err.name === 'ValidationError') {
        const message = Object.values(err.errors).map(val => val.message).join(', ');
        error = new ApiError(message, 400, 'VALIDATION_ERROR');
    }
    
    // Erro de recurso não encontrado
    if (err.name === 'CastError') {
        const message = 'Recurso não encontrado';
        error = new ApiError(message, 404, 'RESOURCE_NOT_FOUND');
    }
    
    // Erro de chave duplicada
    if (err.code === 11000) {
        const field = Object.keys(err.keyValue)[0];
        const message = `${field} já existe`;
        error = new ApiError(message, 400, 'DUPLICATE_FIELD');
    }
    
    // Erro de JSON malformado
    if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
        error = new ApiError('JSON inválido na requisição', 400, 'INVALID_JSON');
    }
    
    // Resposta padronizada de erro
    const response = {
        sucesso: false,
        erro: {
            mensagem: error.message || 'Erro interno do servidor',
            codigo: error.code || 'INTERNAL_ERROR',
            status: error.statusCode || 500
        },
        timestamp: new Date().toISOString(),
        path: req.originalUrl
    };
    
    // Adicionar stack trace apenas em desenvolvimento
    if (process.env.NODE_ENV === 'development') {
        response.erro.stack = err.stack;
    }
    
    res.status(error.statusCode || 500).json(response);
};

// Middleware para rotas não encontradas
const notFound = (req, res, next) => {
    const error = new ApiError(
        `Rota ${req.originalUrl} não encontrada`,
        404,
        'ROUTE_NOT_FOUND'
    );
    next(error);
};

module.exports = {
    ApiError,
    asyncHandler,
    errorHandler,
    notFound
};

📁 app.js - Implementando o Tratamento de Erros

const express = require('express');
const { ApiError, asyncHandler, errorHandler, notFound } = require('./middleware/errorHandler');

const app = express();

// Middleware para parsing JSON
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Middleware para logs de requisições
app.use((req, res, next) => {
    console.log(`📝 ${req.method} ${req.originalUrl} - ${new Date().toISOString()}`);
    next();
});

// Servir arquivos estáticos
app.use(express.static('public'));

// ===== ROTAS COM TRATAMENTO DE ERROS =====

// Rota com validação robusta
app.get('/api/produtos/:id', asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    
    // Validação de ID
    if (isNaN(id) || id <= 0) {
        throw new ApiError('ID deve ser um número positivo', 400, 'INVALID_ID');
    }
    
    const produto = produtos.find(p => p.id === id);
    
    if (!produto) {
        throw new ApiError(
            `Produto com ID ${id} não encontrado`,
            404,
            'PRODUCT_NOT_FOUND'
        );
    }
    
    res.json({
        sucesso: true,
        dados: produto,
        timestamp: new Date().toISOString()
    });
}));

// Rota de criação com validações completas
app.post('/api/produtos', asyncHandler(async (req, res) => {
    const { nome, descricao, preco, categoria_id, disponivel } = req.body;
    
    // Validações detalhadas
    const erros = [];
    
    if (!nome || nome.trim().length === 0) {
        erros.push('Nome é obrigatório');
    } else if (nome.trim().length < 2) {
        erros.push('Nome deve ter pelo menos 2 caracteres');
    }
    
    if (!descricao || descricao.trim().length === 0) {
        erros.push('Descrição é obrigatória');
    }
    
    if (preco === undefined || preco === null) {
        erros.push('Preço é obrigatório');
    } else if (typeof preco !== 'number' || preco <= 0) {
        erros.push('Preço deve ser um número positivo');
    } else if (preco > 10000) {
        erros.push('Preço não pode exceder R$ 10.000');
    }
    
    if (!categoria_id) {
        erros.push('Categoria é obrigatória');
    } else {
        const categoriaExiste = categorias.find(c => c.id === categoria_id);
        if (!categoriaExiste) {
            erros.push('Categoria especificada não existe');
        }
    }
    
    // Se há erros de validação, lançar erro
    if (erros.length > 0) {
        throw new ApiError(
            `Erros de validação: ${erros.join(', ')}`,
            400,
            'VALIDATION_ERRORS'
        );
    }
    
    // Verificar se produto já existe
    const produtoExistente = produtos.find(
        p => p.nome.toLowerCase() === nome.trim().toLowerCase()
    );
    
    if (produtoExistente) {
        throw new ApiError(
            'Já existe um produto com este nome',
            409,
            'PRODUCT_ALREADY_EXISTS'
        );
    }
    
    // Criar produto
    const novoId = Math.max(...produtos.map(p => p.id), 0) + 1;
    const novoProduto = {
        id: novoId,
        nome: nome.trim(),
        descricao: descricao.trim(),
        preco: parseFloat(preco.toFixed(2)),
        categoria_id,
        disponivel: disponivel !== false,
        criado_em: new Date().toISOString()
    };
    
    produtos.push(novoProduto);
    
    res.status(201).json({
        sucesso: true,
        mensagem: 'Produto criado com sucesso',
        dados: novoProduto,
        timestamp: new Date().toISOString()
    });
}));

// Rota para simular erro interno
app.get('/api/erro-teste', asyncHandler(async (req, res) => {
    // Simular erro inesperado
    throw new Error('Este é um erro de teste para demonstração');
}));

// Rota para testar diferentes tipos de erro
app.get('/api/erro/:tipo', asyncHandler(async (req, res) => {
    const { tipo } = req.params;
    
    switch (tipo) {
        case 'validacao':
            throw new ApiError('Erro de validação simulado', 400, 'VALIDATION_ERROR');
        case 'nao-encontrado':
            throw new ApiError('Recurso não encontrado', 404, 'NOT_FOUND');
        case 'nao-autorizado':
            throw new ApiError('Acesso não autorizado', 401, 'UNAUTHORIZED');
        case 'conflito':
            throw new ApiError('Conflito de dados', 409, 'CONFLICT');
        case 'limite-excedido':
            throw new ApiError('Limite de requisições excedido', 429, 'RATE_LIMIT');
        default:
            throw new ApiError('Tipo de erro inválido', 400, 'INVALID_ERROR_TYPE');
    }
}));

// ===== MIDDLEWARE DE TRATAMENTO DE ERROS =====

// Middleware para rotas não encontradas (deve vir antes do errorHandler)
app.use(notFound);

// Middleware global de tratamento de erros (deve ser o último)
app.use(errorHandler);

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

app.listen(PORT, () => {
    console.log(`🚀 Servidor rodando na porta ${PORT}`);
    console.log(`📱 Acesse: http://localhost:${PORT}`);
});

🛡️ Recursos do Sistema de Erros:

  • Classe ApiError personalizada: Erros estruturados com códigos
  • AsyncHandler: Captura automática de erros assíncronos
  • Validações robustas: Múltiplas verificações de dados
  • Logs detalhados: Informações completas para debugging
  • Respostas padronizadas: Formato consistente de erro
  • Códigos HTTP corretos: Status apropriados para cada situação

🧪 Testando o Sistema de Erros

Teste diferentes cenários de erro para verificar se o sistema está funcionando:

💻 Exemplos de Teste de Erros

# Teste de ID inválido
curl http://localhost:3000/api/produtos/abc

# Teste de produto não encontrado
curl http://localhost:3000/api/produtos/999

# Teste de validação (dados faltando)
curl -X POST http://localhost:3000/api/produtos \
  -H "Content-Type: application/json" \
  -d '{}'

# Teste de preço inválido
curl -X POST http://localhost:3000/api/produtos \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "Teste",
    "descricao": "Descrição",
    "preco": -5,
    "categoria_id": 1
  }'

# Teste de rota não encontrada
curl http://localhost:3000/api/rota-inexistente

# Teste de erro interno
curl http://localhost:3000/api/erro-teste

# Teste de diferentes tipos de erro
curl http://localhost:3000/api/erro/validacao
curl http://localhost:3000/api/erro/nao-encontrado
curl http://localhost:3000/api/erro/nao-autorizado

✅ Benefícios do Tratamento Robusto:

  • Debugging facilitado: Logs detalhados ajudam a identificar problemas
  • UX melhorada: Mensagens de erro claras para o usuário
  • Segurança: Não exposição de informações sensíveis
  • Manutenibilidade: Código organizado e fácil de manter
  • Monitoramento: Base para sistemas de alertas

9.5 Processo de Build com Webpack

Configure um processo de build profissional para otimizar seu projeto Express com frontend moderno:

📦 Instalação do Webpack

# Dependências principais do Webpack
npm install --save-dev webpack webpack-cli webpack-dev-server

# Plugins e loaders essenciais
npm install --save-dev html-webpack-plugin css-loader style-loader
npm install --save-dev mini-css-extract-plugin terser-webpack-plugin

# Para desenvolvimento simultâneo
npm install --save-dev concurrently

⚙️ Configuração - webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    // Pontos de entrada para diferentes páginas
    entry: {
      main: './public/js/main.js',
      produtos: './public/js/produtos.js'
    },
    
    // Configuração de saída
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? '[name].[contenthash].js' : '[name].js',
      clean: true // Limpa a pasta dist a cada build
    },
    
    // Configuração de módulos e loaders
    module: {
      rules: [
        {
          test: /\.css$/i,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource'
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource'
        }
      ]
    },
    
    // Plugins para diferentes funcionalidades
    plugins: [
      // Plugin para index.html
      new HtmlWebpackPlugin({
        template: './public/index.html',
        filename: 'index.html',
        chunks: ['main']
      }),
      // Plugin para produtos.html
      new HtmlWebpackPlugin({
        template: './public/produtos.html',
        filename: 'produtos.html',
        chunks: ['produtos']
      }),
      // Plugin para extração de CSS em produção
      ...(isProduction ? [
        new MiniCssExtractPlugin({
          filename: '[name].[contenthash].css'
        })
      ] : [])
    ],
    
    // Otimizações para produção
    optimization: {
      minimize: isProduction,
      minimizer: [new TerserPlugin()],
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    },
    
    // Servidor de desenvolvimento
    devServer: {
      static: {
        directory: path.join(__dirname, 'dist')
      },
      compress: true,
      port: 8080,
      hot: true, // Hot Module Replacement
      proxy: {
        '/api': {
          target: 'http://localhost:3000',
          changeOrigin: true
        }
      }
    }
  };
};

📝 Scripts no package.json

{
  "name": "express-webpack-project",
  "version": "1.0.0",
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "start": "node app.js",
    "dev:full": "concurrently \"npm run start\" \"npm run dev\"",
    "preview": "npm run build && npm run start"
  },
  "devDependencies": {
    "concurrently": "^7.6.0",
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.0",
    "webpack-dev-server": "^4.15.0",
    "html-webpack-plugin": "^5.5.0",
    "css-loader": "^6.8.0",
    "style-loader": "^3.3.0",
    "mini-css-extract-plugin": "^2.7.0",
    "terser-webpack-plugin": "^5.3.0"
  }
}

🚀 Benefícios do Build Process:

  • Minificação: Reduz drasticamente o tamanho dos arquivos
  • Bundle Splitting: Separa código vendor do aplicativo
  • Cache Busting: Nomes com hash para cache eficiente
  • Hot Reload: Atualização automática durante desenvolvimento
  • Proxy API: Integração seamless com backend
  • Otimização CSS: Extração e minificação de estilos

🔧 Comandos de Uso

# Desenvolvimento com hot reload (apenas frontend)
npm run dev

# Build para produção
npm run build

# Desenvolvimento completo (backend + frontend simultaneamente)
npm run dev:full

# Preview da versão de produção
npm run preview

📁 Estrutura de Arquivos Recomendada

projeto/
├── app.js                 # Servidor Express
├── package.json
├── webpack.config.js      # Configuração do Webpack
├── public/               # Arquivos fonte
│   ├── index.html
│   ├── produtos.html
│   ├── css/
│   │   └── styles.css
│   └── js/
│       ├── main.js
│       └── produtos.js
├── dist/                 # Arquivos compilados (gerado automaticamente)
│   ├── index.html
│   ├── produtos.html
│   ├── main.[hash].js
│   ├── produtos.[hash].js
│   └── vendors.[hash].js
└── node_modules/

✅ Vantagens do Webpack:

  • Performance: Carregamento mais rápido com bundles otimizados
  • Desenvolvimento: Hot reload acelera o ciclo de desenvolvimento
  • Produção: Arquivos minificados e otimizados
  • Manutenibilidade: Código modular e organizado
  • Escalabilidade: Fácil adição de novos recursos e páginas

9.6 Motor de Template (EJS, Pug, Handlebars)

Implemente templates dinâmicos para renderizar HTML no servidor com dados do backend:

🎨 EJS (Embedded JavaScript)

# Instalação do EJS
npm install ejs

Configuração no app.js:

const express = require('express');
const path = require('path');
const app = express();

// Configurar EJS como motor de template
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Middleware para arquivos estáticos
app.use(express.static('public'));
app.use(express.json());

// Dados simulados
const produtos = [
  { id: 1, nome: 'Smartphone', preco: 899.99, categoria: 'eletrônicos', estoque: 15 },
  { id: 2, nome: 'Notebook', preco: 2499.99, categoria: 'eletrônicos', estoque: 8 },
  { id: 3, nome: 'Tênis', preco: 199.99, categoria: 'calçados', estoque: 25 }
];

// Rota para página inicial com EJS
app.get('/', (req, res) => {
  res.render('index', { 
    titulo: 'Loja Express',
    produtos: produtos.slice(0, 3),
    usuario: { nome: 'João', logado: true }
  });
});

// Rota para produtos com EJS
app.get('/produtos', (req, res) => {
  const categoria = req.query.categoria;
  const produtosFiltrados = categoria 
    ? produtos.filter(p => p.categoria === categoria)
    : produtos;
    
  res.render('produtos', {
    titulo: 'Nossos Produtos',
    produtos: produtosFiltrados,
    categorias: ['eletrônicos', 'calçados', 'roupas'],
    categoriaAtual: categoria || 'todas'
  });
});

app.listen(3000, () => {
  console.log('Servidor rodando na porta 3000');
});

Template views/index.ejs:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= titulo %></title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
    <%- include('partials/header') %>
    
    <main class="container mx-auto px-4 py-8">
        <h1 class="text-3xl font-bold text-gray-900 mb-8"><%= titulo %></h1>
        
        <% if (usuario.logado) { %>
            <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
                Bem-vindo, <%= usuario.nome %>!
            </div>
        <% } %>
        
        <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
            <% produtos.forEach(produto => { %>
                <div class="bg-white rounded-lg shadow-md p-6">
                    <h3 class="text-xl font-semibold mb-2"><%= produto.nome %></h3>
                    <p class="text-gray-600 mb-2">Categoria: <%= produto.categoria %></p>
                    <p class="text-2xl font-bold text-green-600">R$ <%= produto.preco.toFixed(2) %></p>
                    <p class="text-sm text-gray-500">Estoque: <%= produto.estoque %> unidades</p>
                </div>
            <% }); %>
        </div>
        
        <div class="mt-8 text-center">
            <a href="/produtos" class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600">
                Ver Todos os Produtos
            </a>
        </div>
    </main>
    
    <%- include('partials/footer') %>
</body>
</html>

Template views/produtos.ejs:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= titulo %></title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
    <%- include('partials/header') %>
    
    <main class="container mx-auto px-4 py-8">
        <h1 class="text-3xl font-bold text-gray-900 mb-8"><%= titulo %></h1>
        
        <!-- Filtros de Categoria -->
        <div class="mb-6">
            <h3 class="text-lg font-semibold mb-3">Filtrar por categoria:</h3>
            <div class="flex flex-wrap gap-2">
                <a href="/produtos" 
                   class="px-4 py-2 rounded <%= categoriaAtual === 'todas' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' %>">
                    Todas
                </a>
                <% categorias.forEach(categoria => { %>
                    <a href="/produtos?categoria=<%= categoria %>" 
                       class="px-4 py-2 rounded <%= categoriaAtual === categoria ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' %>">
                        <%= categoria.charAt(0).toUpperCase() + categoria.slice(1) %>
                    </a>
                <% }); %>
            </div>
        </div>
        
        <!-- Lista de Produtos -->
        <% if (produtos.length > 0) { %>
            <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                <% produtos.forEach(produto => { %>
                    <div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
                        <h3 class="text-xl font-semibold mb-2"><%= produto.nome %></h3>
                        <p class="text-gray-600 mb-2">Categoria: <%= produto.categoria %></p>
                        <p class="text-2xl font-bold text-green-600 mb-2">R$ <%= produto.preco.toFixed(2) %></p>
                        <p class="text-sm text-gray-500 mb-4">Estoque: <%= produto.estoque %> unidades</p>
                        <button class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600">
                            Adicionar ao Carrinho
                        </button>
                    </div>
                <% }); %>
            </div>
        <% } else { %>
            <div class="text-center py-12">
                <p class="text-gray-500 text-lg">Nenhum produto encontrado nesta categoria.</p>
                <a href="/produtos" class="text-blue-500 hover:underline">Ver todos os produtos</a>
            </div>
        <% } %>
    </main>
    
    <%- include('partials/footer') %>
</body>
</html>

Partial views/partials/header.ejs:

<header class="bg-blue-600 text-white shadow-lg">
    <nav class="container mx-auto px-4 py-4">
        <div class="flex justify-between items-center">
            <h1 class="text-2xl font-bold">
                <a href="/" class="hover:text-blue-200">Loja Express</a>
            </h1>
            <ul class="flex space-x-6">
                <li><a href="/" class="hover:text-blue-200">Início</a></li>
                <li><a href="/produtos" class="hover:text-blue-200">Produtos</a></li>
                <li><a href="/contato" class="hover:text-blue-200">Contato</a></li>
            </ul>
        </div>
    </nav>
</header>

🔧 Pug (Template Engine Minimalista)

# Instalação do Pug
npm install pug

Configuração para Pug:

// No app.js
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

Template views/produtos.pug:

doctype html
html(lang='pt-BR')
  head
    meta(charset='UTF-8')
    meta(name='viewport', content='width=device-width, initial-scale=1.0')
    title= titulo
    script(src='https://cdn.tailwindcss.com')
  body.bg-gray-100
    include partials/header
    
    main.container.mx-auto.px-4.py-8
      h1.text-3xl.font-bold.text-gray-900.mb-8= titulo
      
      .mb-6
        h3.text-lg.font-semibold.mb-3 Filtrar por categoria:
        .flex.flex-wrap.gap-2
          a(href='/produtos', 
            class=categoriaAtual === 'todas' ? 'px-4 py-2 rounded bg-blue-500 text-white' : 'px-4 py-2 rounded bg-gray-200 text-gray-700') Todas
          each categoria in categorias
            a(href=`/produtos?categoria=${categoria}`, 
              class=categoriaAtual === categoria ? 'px-4 py-2 rounded bg-blue-500 text-white' : 'px-4 py-2 rounded bg-gray-200 text-gray-700')= categoria.charAt(0).toUpperCase() + categoria.slice(1)
      
      if produtos.length > 0
        .grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-6
          each produto in produtos
            .bg-white.rounded-lg.shadow-md.p-6.hover:shadow-lg.transition-shadow
              h3.text-xl.font-semibold.mb-2= produto.nome
              p.text-gray-600.mb-2 Categoria: #{produto.categoria}
              p.text-2xl.font-bold.text-green-600.mb-2 R$ #{produto.preco.toFixed(2)}
              p.text-sm.text-gray-500.mb-4 Estoque: #{produto.estoque} unidades
              button.w-full.bg-blue-500.text-white.py-2.rounded.hover:bg-blue-600 Adicionar ao Carrinho
      else
        .text-center.py-12
          p.text-gray-500.text-lg Nenhum produto encontrado nesta categoria.
          a.text-blue-500.hover:underline(href='/produtos') Ver todos os produtos
    
    include partials/footer

🎯 Handlebars (Template Engine Lógico)

# Instalação do Handlebars
npm install express-handlebars

Configuração para Handlebars:

const { engine } = require('express-handlebars');

// Configurar Handlebars
app.engine('handlebars', engine({
  defaultLayout: 'main',
  layoutsDir: path.join(__dirname, 'views/layouts'),
  partialsDir: path.join(__dirname, 'views/partials'),
  helpers: {
    eq: (a, b) => a === b,
    formatPrice: (price) => `R$ ${price.toFixed(2)}`,
    capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1)
  }
}));
app.set('view engine', 'handlebars');

Layout views/layouts/main.handlebars:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{titulo}}</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
    {{> header}}
    
    {{{body}}}
    
    {{> footer}}
</body>
</html>

Template views/produtos.handlebars:

<main class="container mx-auto px-4 py-8">
    <h1 class="text-3xl font-bold text-gray-900 mb-8">{{titulo}}</h1>
    
    <div class="mb-6">
        <h3 class="text-lg font-semibold mb-3">Filtrar por categoria:</h3>
        <div class="flex flex-wrap gap-2">
            <a href="/produtos" 
               class="px-4 py-2 rounded {{#if (eq categoriaAtual 'todas')}}bg-blue-500 text-white{{else}}bg-gray-200 text-gray-700{{/if}}">
                Todas
            </a>
            {{#each categorias}}
                <a href="/produtos?categoria={{this}}" 
                   class="px-4 py-2 rounded {{#if (eq ../categoriaAtual this)}}bg-blue-500 text-white{{else}}bg-gray-200 text-gray-700{{/if}}">
                    {{capitalize this}}
                </a>
            {{/each}}
        </div>
    </div>
    
    {{#if produtos.length}}
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {{#each produtos}}
                <div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
                    <h3 class="text-xl font-semibold mb-2">{{nome}}</h3>
                    <p class="text-gray-600 mb-2">Categoria: {{categoria}}</p>
                    <p class="text-2xl font-bold text-green-600 mb-2">{{formatPrice preco}}</p>
                    <p class="text-sm text-gray-500 mb-4">Estoque: {{estoque}} unidades</p>
                    <button class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600">
                        Adicionar ao Carrinho
                    </button>
                </div>
            {{/each}}
        </div>
    {{else}}
        <div class="text-center py-12">
            <p class="text-gray-500 text-lg">Nenhum produto encontrado nesta categoria.</p>
            <a href="/produtos" class="text-blue-500 hover:underline">Ver todos os produtos</a>
        </div>
    {{/if}}
</main>

📊 Comparação dos Template Engines:

EJS: Sintaxe familiar (HTML + JavaScript), fácil aprendizado

Pug: Sintaxe minimalista, menos código, mais limpo

Handlebars: Lógica separada, helpers customizados, mais estruturado

✅ Vantagens dos Template Engines:

  • Renderização Server-Side: SEO otimizado e carregamento inicial mais rápido
  • Reutilização: Partials e layouts para código DRY
  • Dados Dinâmicos: Integração direta com dados do backend
  • Segurança: Escape automático de HTML para prevenir XSS
  • Performance: Cache de templates compilados

9.7 Nodemon para Desenvolvimento

Configure o Nodemon para reinicialização automática durante o desenvolvimento:

📦 Instalação do Nodemon

# Instalação global (recomendado para uso geral)
npm install -g nodemon

# Instalação como dependência de desenvolvimento (recomendado para projetos)
npm install --save-dev nodemon

⚙️ Configuração no package.json

{
  "name": "express-api",
  "version": "1.0.0",
  "description": "API Express com Nodemon",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "dev:watch": "nodemon --watch . --ext js,json,html app.js",
    "dev:debug": "nodemon --inspect app.js",
    "dev:verbose": "nodemon --verbose app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

🔧 Arquivo de Configuração - nodemon.json

{
  "watch": ["."],
  "ext": "js,json,html,css",
  "ignore": [
    "node_modules",
    "public/dist",
    "*.test.js",
    "coverage",
    "logs",
    ".git"
  ],
  "delay": 1000,
  "env": {
    "NODE_ENV": "development",
    "DEBUG": "app:*",
    "PORT": "3000"
  },
  "events": {
    "restart": "echo 'Servidor reiniciado devido a mudanças nos arquivos'",
    "crash": "echo 'Script crashed for some reason'",
    "exit": "echo 'App has exited'",
    "start": "echo 'App has started'"
  },
  "colours": true,
  "legacyWatch": false,
  "pollingInterval": 100
}

🚀 Comandos de Uso

# Executar em modo desenvolvimento
npm run dev

# Executar com watch específico
npm run dev:watch

# Executar com debug habilitado
npm run dev:debug

# Executar com logs verbosos
npm run dev:verbose

# Executar diretamente com nodemon
nodemon app.js

# Com opções específicas
nodemon --watch src --ext js,json app.js

# Ignorar arquivos específicos
nodemon --ignore public/ --ignore tests/ app.js

🔥 Recursos Avançados

1. Monitoramento Seletivo

# Monitorar apenas arquivos específicos
nodemon --watch src --watch config app.js

# Monitorar extensões específicas
nodemon --ext js,json,env app.js

# Ignorar diretórios específicos
nodemon --ignore node_modules/ --ignore logs/ app.js

2. Integração com Debug

# Debug com Chrome DevTools
nodemon --inspect=0.0.0.0:9229 app.js

# Debug com breakpoint automático
nodemon --inspect-brk app.js

# Debug em porta específica
nodemon --inspect=9230 app.js

3. Variáveis de Ambiente

# Arquivo .env para desenvolvimento
NODE_ENV=development
PORT=3000
DB_URL=mongodb://localhost:27017/dev
DEBUG=app:*
LOG_LEVEL=debug

4. Scripts Personalizados

{
  "scripts": {
    "dev:api": "nodemon --watch api api/server.js",
    "dev:web": "nodemon --watch public --ext html,css,js --exec 'echo Arquivos web atualizados'",
    "dev:full": "concurrently \"npm run dev:api\" \"npm run dev:web\"",
    "dev:test": "nodemon --exec \"npm test\" --watch tests --watch src"
  }
}

⚡ Benefícios do Nodemon:

  • Reinicialização Automática: Detecta mudanças e reinicia o servidor automaticamente
  • Produtividade: Elimina a necessidade de reiniciar manualmente durante desenvolvimento
  • Configuração Flexível: Permite personalizar quais arquivos monitorar
  • Integração com Debug: Facilita o debugging com ferramentas de desenvolvimento
  • Logs Informativos: Mostra quando e por que o servidor foi reiniciado
  • Suporte a Múltiplos Formatos: Monitora JS, JSON, HTML, CSS e outros
  • Performance: Otimizado para não impactar a performance do desenvolvimento

💡 Dicas de Uso Avançado:

  • • Use --delay para evitar reinicializações muito frequentes
  • • Configure .nodemonignore para arquivos que não devem ser monitorados
  • • Use diferentes scripts para diferentes ambientes de desenvolvimento
  • • Combine com ferramentas como ESLint para validação automática
  • • Use --exec para executar comandos personalizados
  • • Configure eventos personalizados para automação adicional

🛠️ Exemplo Prático de Uso

// app.js - Servidor Express com logs para demonstrar reinicialização
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Log de inicialização
console.log('🚀 Servidor iniciando...', new Date().toLocaleTimeString());

// Middleware de log
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} - ${new Date().toLocaleTimeString()}`);
  next();
});

// Rotas
app.get('/', (req, res) => {
  res.json({ 
    message: 'Servidor rodando com Nodemon!',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

app.get('/api/status', (req, res) => {
  res.json({
    status: 'online',
    environment: process.env.NODE_ENV || 'development',
    version: '1.0.0'
  });
});

app.listen(PORT, () => {
  console.log(`✅ Servidor rodando na porta ${PORT}`);
  console.log(`🌐 Acesse: http://localhost:${PORT}`);
});

⚠️ Considerações Importantes:

  • Produção: Nunca use Nodemon em produção, apenas para desenvolvimento
  • Performance: Configure adequadamente os arquivos a serem ignorados
  • Memória: Reinicializações frequentes podem consumir mais memória
  • Debugging: Use --inspect apenas quando necessário

9.8 Testes Unitários e de Integração

Implemente testes robustos para garantir a qualidade e confiabilidade da sua API Express:

📦 Configuração do Ambiente de Testes

# Instalação das dependências de teste
npm install --save-dev jest supertest
npm install --save-dev @types/jest @types/supertest # Para TypeScript

# Alternativa com Mocha e Chai
npm install --save-dev mocha chai chai-http
npm install --save-dev @types/mocha @types/chai # Para TypeScript

⚙️ Configuração do package.json

{
  "name": "express-api-tests",
  "version": "1.0.0",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:integration": "jest --testPathPattern=integration",
    "test:unit": "jest --testPathPattern=unit",
    "test:ci": "jest --ci --coverage --watchAll=false"
  },
  "jest": {
    "testEnvironment": "node",
    "collectCoverageFrom": [
      "**/*.{js,jsx}",
      "!**/node_modules/**",
      "!**/coverage/**",
      "!**/tests/**"
    ],
    "testMatch": [
      "**/__tests__/**/*.js",
      "**/?(*.)+(spec|test).js"
    ]
  },
  "devDependencies": {
    "jest": "^29.5.0",
    "supertest": "^6.3.3",
    "nodemon": "^3.0.1"
  }
}

🧪 Estrutura de Testes

projeto/
├── app.js
├── routes/
│   ├── produtos.js
│   └── usuarios.js
├── middleware/
│   ├── auth.js
│   └── errorHandler.js
├── models/
│   └── produto.js
├── utils/
│   └── helpers.js
└── tests/
    ├── unit/
    │   ├── models/
    │   │   └── produto.test.js
    │   ├── utils/
    │   │   └── helpers.test.js
    │   └── middleware/
    │       └── auth.test.js
    ├── integration/
    │   ├── produtos.test.js
    │   └── usuarios.test.js
    ├── fixtures/
    │   └── testData.js
    └── setup/
        └── testSetup.js

🔧 Testes Unitários

1. Teste de Modelo (models/produto.js)

// models/produto.js
class Produto {
  constructor(id, nome, preco, categoria) {
    this.id = id;
    this.nome = nome;
    this.preco = preco;
    this.categoria = categoria;
  }

  static validar(produto) {
    const errors = [];
    
    if (!produto.nome || produto.nome.length < 2) {
      errors.push('Nome deve ter pelo menos 2 caracteres');
    }
    
    if (!produto.preco || produto.preco <= 0) {
      errors.push('Preço deve ser maior que zero');
    }
    
    if (!produto.categoria) {
      errors.push('Categoria é obrigatória');
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }

  calcularDesconto(percentual) {
    if (percentual < 0 || percentual > 100) {
      throw new Error('Percentual deve estar entre 0 e 100');
    }
    return this.preco * (1 - percentual / 100);
  }
}

module.exports = Produto;

2. Teste Unitário do Modelo

// tests/unit/models/produto.test.js
const Produto = require('../../../models/produto');

describe('Produto Model', () => {
  describe('Validação', () => {
    test('deve validar produto válido', () => {
      const produto = {
        nome: 'Smartphone',
        preco: 999.99,
        categoria: 'Eletrônicos'
      };
      
      const resultado = Produto.validar(produto);
      
      expect(resultado.isValid).toBe(true);
      expect(resultado.errors).toHaveLength(0);
    });

    test('deve rejeitar produto com nome inválido', () => {
      const produto = {
        nome: 'A',
        preco: 999.99,
        categoria: 'Eletrônicos'
      };
      
      const resultado = Produto.validar(produto);
      
      expect(resultado.isValid).toBe(false);
      expect(resultado.errors).toContain('Nome deve ter pelo menos 2 caracteres');
    });

    test('deve rejeitar produto com preço inválido', () => {
      const produto = {
        nome: 'Smartphone',
        preco: -10,
        categoria: 'Eletrônicos'
      };
      
      const resultado = Produto.validar(produto);
      
      expect(resultado.isValid).toBe(false);
      expect(resultado.errors).toContain('Preço deve ser maior que zero');
    });
  });

  describe('Cálculo de Desconto', () => {
    test('deve calcular desconto corretamente', () => {
      const produto = new Produto(1, 'Smartphone', 1000, 'Eletrônicos');
      
      const precoComDesconto = produto.calcularDesconto(10);
      
      expect(precoComDesconto).toBe(900);
    });

    test('deve lançar erro para percentual inválido', () => {
      const produto = new Produto(1, 'Smartphone', 1000, 'Eletrônicos');
      
      expect(() => produto.calcularDesconto(-5)).toThrow('Percentual deve estar entre 0 e 100');
      expect(() => produto.calcularDesconto(105)).toThrow('Percentual deve estar entre 0 e 100');
    });
  });
});

🌐 Testes de Integração

1. Configuração do App para Testes

// app.js
const express = require('express');
const produtosRouter = require('./routes/produtos');
const errorHandler = require('./middleware/errorHandler');

const app = express();

app.use(express.json());
app.use('/api/produtos', produtosRouter);
app.use(errorHandler);

// Não iniciar o servidor se estivermos em ambiente de teste
if (process.env.NODE_ENV !== 'test') {
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Servidor rodando na porta ${PORT}`);
  });
}

module.exports = app;

2. Testes de Integração da API

// tests/integration/produtos.test.js
const request = require('supertest');
const app = require('../../app');

describe('API de Produtos', () => {
  describe('GET /api/produtos', () => {
    test('deve retornar lista de produtos', async () => {
      const response = await request(app)
        .get('/api/produtos')
        .expect(200);
      
      expect(response.body).toHaveProperty('produtos');
      expect(Array.isArray(response.body.produtos)).toBe(true);
    });

    test('deve retornar produtos com estrutura correta', async () => {
      const response = await request(app)
        .get('/api/produtos')
        .expect(200);
      
      if (response.body.produtos.length > 0) {
        const produto = response.body.produtos[0];
        expect(produto).toHaveProperty('id');
        expect(produto).toHaveProperty('nome');
        expect(produto).toHaveProperty('preco');
        expect(produto).toHaveProperty('categoria');
      }
    });
  });

  describe('GET /api/produtos/:id', () => {
    test('deve retornar produto específico', async () => {
      const response = await request(app)
        .get('/api/produtos/1')
        .expect(200);
      
      expect(response.body).toHaveProperty('produto');
      expect(response.body.produto.id).toBe(1);
    });

    test('deve retornar 404 para produto inexistente', async () => {
      const response = await request(app)
        .get('/api/produtos/999')
        .expect(404);
      
      expect(response.body).toHaveProperty('error');
      expect(response.body.error).toBe('Produto não encontrado');
    });
  });

  describe('POST /api/produtos', () => {
    test('deve criar novo produto', async () => {
      const novoProduto = {
        nome: 'Tablet',
        preco: 599.99,
        categoria: 'Eletrônicos'
      };
      
      const response = await request(app)
        .post('/api/produtos')
        .send(novoProduto)
        .expect(201);
      
      expect(response.body).toHaveProperty('produto');
      expect(response.body.produto.nome).toBe(novoProduto.nome);
      expect(response.body.produto.preco).toBe(novoProduto.preco);
    });

    test('deve rejeitar produto inválido', async () => {
      const produtoInvalido = {
        nome: 'A',
        preco: -10
      };
      
      const response = await request(app)
        .post('/api/produtos')
        .send(produtoInvalido)
        .expect(400);
      
      expect(response.body).toHaveProperty('errors');
      expect(Array.isArray(response.body.errors)).toBe(true);
    });
  });

  describe('PUT /api/produtos/:id', () => {
    test('deve atualizar produto existente', async () => {
      const produtoAtualizado = {
        nome: 'Smartphone Pro',
        preco: 1299.99,
        categoria: 'Eletrônicos'
      };
      
      const response = await request(app)
        .put('/api/produtos/1')
        .send(produtoAtualizado)
        .expect(200);
      
      expect(response.body.produto.nome).toBe(produtoAtualizado.nome);
    });
  });

  describe('DELETE /api/produtos/:id', () => {
    test('deve deletar produto existente', async () => {
      await request(app)
        .delete('/api/produtos/1')
        .expect(204);
    });
  });
});

🔧 Configuração Avançada de Testes

1. Setup de Testes

// tests/setup/testSetup.js
const { beforeAll, afterAll, beforeEach, afterEach } = require('@jest/globals');

// Configuração global antes de todos os testes
beforeAll(async () => {
  process.env.NODE_ENV = 'test';
  process.env.PORT = '0'; // Porta aleatória para testes
  
  // Configurar banco de dados de teste
  // await setupTestDatabase();
});

// Limpeza após todos os testes
afterAll(async () => {
  // Limpar banco de dados de teste
  // await cleanupTestDatabase();
});

// Antes de cada teste
beforeEach(async () => {
  // Resetar dados de teste
  // await resetTestData();
});

// Após cada teste
afterEach(async () => {
  // Limpar cache ou estado
  jest.clearAllMocks();
});

2. Fixtures de Teste

// tests/fixtures/testData.js
const produtosMock = [
  {
    id: 1,
    nome: 'Smartphone',
    preco: 999.99,
    categoria: 'Eletrônicos',
    estoque: 50
  },
  {
    id: 2,
    nome: 'Notebook',
    preco: 2499.99,
    categoria: 'Eletrônicos',
    estoque: 25
  },
  {
    id: 3,
    nome: 'Livro JavaScript',
    preco: 89.90,
    categoria: 'Livros',
    estoque: 100
  }
];

const usuariosMock = [
  {
    id: 1,
    nome: 'João Silva',
    email: 'joao@email.com',
    role: 'admin'
  },
  {
    id: 2,
    nome: 'Maria Santos',
    email: 'maria@email.com',
    role: 'user'
  }
];

module.exports = {
  produtosMock,
  usuariosMock
};

🎯 Testes de Middleware

// tests/unit/middleware/auth.test.js
const authMiddleware = require('../../../middleware/auth');

describe('Auth Middleware', () => {
  let req, res, next;

  beforeEach(() => {
    req = {
      headers: {},
      user: null
    };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn()
    };
    next = jest.fn();
  });

  test('deve permitir acesso com token válido', () => {
    req.headers.authorization = 'Bearer valid-token';
    
    authMiddleware(req, res, next);
    
    expect(next).toHaveBeenCalled();
    expect(req.user).toBeDefined();
  });

  test('deve rejeitar acesso sem token', () => {
    authMiddleware(req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({
      error: 'Token de acesso requerido'
    });
    expect(next).not.toHaveBeenCalled();
  });

  test('deve rejeitar token inválido', () => {
    req.headers.authorization = 'Bearer invalid-token';
    
    authMiddleware(req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({
      error: 'Token inválido'
    });
  });
});

📊 Comandos de Execução

# Executar todos os testes
npm test

# Executar testes em modo watch
npm run test:watch

# Executar apenas testes unitários
npm run test:unit

# Executar apenas testes de integração
npm run test:integration

# Executar testes com cobertura
npm run test:coverage

# Executar testes para CI/CD
npm run test:ci

# Executar teste específico
npm test -- --testNamePattern="deve validar produto"

# Executar testes de arquivo específico
npm test tests/unit/models/produto.test.js

✅ Benefícios dos Testes:

  • Confiabilidade: Garante que o código funciona conforme esperado
  • Refatoração Segura: Permite mudanças no código com confiança
  • Documentação Viva: Testes servem como documentação do comportamento
  • Detecção Precoce: Identifica bugs antes da produção
  • Cobertura de Código: Mostra quais partes do código estão testadas
  • Integração Contínua: Facilita automação de deploy
  • Qualidade: Melhora a qualidade geral do software

🎯 Boas Práticas de Teste:

  • AAA Pattern: Arrange, Act, Assert - organize seus testes
  • Testes Independentes: Cada teste deve ser independente dos outros
  • Nomes Descritivos: Use nomes que expliquem o que está sendo testado
  • Dados de Teste: Use fixtures e mocks para dados consistentes
  • Cobertura Adequada: Mire em 80-90% de cobertura de código
  • Testes Rápidos: Mantenha os testes unitários rápidos
  • Falhas Claras: Mensagens de erro devem ser informativas

⚠️ Considerações Importantes:

  • Ambiente Isolado: Use banco de dados separado para testes
  • Cleanup: Sempre limpe dados após os testes
  • Mocks vs Real: Balance entre mocks e testes reais
  • Performance: Testes lentos desencorajam execução frequente
  • Manutenção: Testes também precisam ser mantidos
PASSO 2 📖 EXEMPLO TEÓRICO

O que é Express.js?

Definição: Express.js é um framework web minimalista, rápido e flexível para Node.js que fornece um conjunto robusto de recursos para aplicações web e mobile.

Principais Características:

Minimalista Framework leve e não opinativo
Flexível Permite diferentes arquiteturas
Rápido Performance otimizada
Middleware Sistema de middleware poderoso
Roteamento Sistema de rotas avançado
Templating Suporte a engines de template

Por que usar Express?

  • Simplifica o desenvolvimento de aplicações web
  • Grande ecossistema de middleware
  • Comunidade ativa e documentação excelente
  • Usado por grandes empresas (Netflix, Uber, WhatsApp)
PASSO 3 🔨 CÓDIGO PRÁTICO

Instalação e Configuração

Passo 1: Inicializar o Projeto

# Criar diretório do projeto
mkdir meu-primeiro-express
cd meu-primeiro-express

# Inicializar package.json
npm init -y

Nota para usuários Windows: Se estiver usando PowerShell, execute os comandos separadamente. O operador && não é suportado no PowerShell. Execute cada comando em uma linha separada.

Passo 2: Instalar Express

# Instalar Express
npm install express

# Instalar nodemon para desenvolvimento (opcional)
npm install --save-dev nodemon

Passo 3: Configurar Scripts

// package.json
{
  "name": "meu-primeiro-express",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
PASSO 4 🔨 CÓDIGO PRÁTICO

Criando seu Primeiro Servidor

Vamos criar um servidor Express básico que responde "Hello World!" na rota principal.

app.js - Servidor Básico

// Importar o Express
const express = require('express');

// Criar uma instância do Express
const app = express();

// Definir a porta
const PORT = process.env.PORT || 3000;

// Rota básica
app.get('/', (req, res) => {
    res.send('Hello World! Meu primeiro servidor Express!');
});

// Iniciar o servidor
app.listen(PORT, () => {
    console.log(`Servidor rodando na porta ${PORT}`);
    console.log(`Acesse: http://localhost:${PORT}`);
});

Para executar: Execute npm run dev no terminal e acesse http://localhost:3000 no seu navegador.

Entendendo o Código:

1. Importação e Instância

const express = require('express');
const app = express();

Importamos o Express e criamos uma instância da aplicação.

2. Definição de Rota

app.get('/', (req, res) => {
    res.send('Hello World!');
});

Definimos uma rota GET para o caminho raiz ('/') que responde com uma mensagem.

3. Inicialização do Servidor

app.listen(PORT, () => {
    console.log(`Servidor rodando na porta ${PORT}`);
});

Iniciamos o servidor na porta especificada e exibimos uma mensagem de confirmação.

PASSO 5 🔨 CÓDIGO PRÁTICO

Rotas Básicas e Métodos HTTP

As rotas determinam como uma aplicação responde a uma requisição do cliente para um endpoint específico, que é definido por um caminho e um método HTTP específico.

Principais Métodos HTTP:

GET Buscar dados
POST Criar novos dados
PUT Atualizar dados
DELETE Remover dados

Exemplos de Rotas:

Rotas GET

// Rota principal
app.get('/', (req, res) => {
    res.send('Página inicial');
});

// Rota sobre
app.get('/sobre', (req, res) => {
    res.send('Página sobre nós');
});

// Rota com parâmetro
app.get('/usuario/:id', (req, res) => {
    const userId = req.params.id;
    res.send(`Perfil do usuário ${userId}`);
});

// Rota com query parameters
app.get('/buscar', (req, res) => {
    const termo = req.query.q;
    res.send(`Buscando por: ${termo}`);
});

Rotas POST

// Middleware para parsing de JSON
app.use(express.json());

// Rota POST para criar usuário
app.post('/usuarios', (req, res) => {
    const { nome, email } = req.body;
    
    // Aqui você salvaria no banco de dados
    const novoUsuario = {
        id: Date.now(),
        nome,
        email,
        criadoEm: new Date()
    };
    
    res.status(201).json({
        message: 'Usuário criado com sucesso!',
        usuario: novoUsuario
    });
});

Rotas PUT e DELETE

// Atualizar usuário
app.put('/usuarios/:id', (req, res) => {
    const userId = req.params.id;
    const { nome, email } = req.body;
    
    res.json({
        message: `Usuário ${userId} atualizado`,
        dados: { nome, email }
    });
});

// Deletar usuário
app.delete('/usuarios/:id', (req, res) => {
    const userId = req.params.id;
    
    res.json({
        message: `Usuário ${userId} removido com sucesso`
    });
});
PASSO 6 🔨 CÓDIGO PRÁTICO

Servindo Arquivos Estáticos

O Express pode servir arquivos estáticos como HTML, CSS, JavaScript, imagens, etc. usando o middleware express.static.

Configuração Básica:

// Servir arquivos estáticos da pasta 'public'
app.use(express.static('public'));

// Ou com um prefixo virtual
app.use('/static', express.static('public'));

Estrutura de Pastas:

projeto/
├── app.js
├── package.json
└── public/
    ├── index.html
    ├── css/
    │   └── style.css
    ├── js/
    │   └── script.js
    └── images/
        └── logo.png

Exemplo Completo:

app.js

const express = require('express');
const path = require('path');
const app = express();

// Servir arquivos estáticos
app.use(express.static('public'));

// Rota para página inicial
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.listen(3000, () => {
    console.log('Servidor rodando na porta 3000');
});

public/index.html




    
    
    Meu Site Express
    


    

Bem-vindo ao Express!

Este é um arquivo HTML estático servido pelo Express.

PASSO 7 📖 EXEMPLO TEÓRICO

Middleware Básico

Middleware: Funções que têm acesso ao objeto de requisição (req), ao objeto de resposta (res) e à próxima função middleware no ciclo de requisição-resposta.

Tipos de Middleware:

  • Application-level Aplicado a toda a aplicação
  • Router-level Aplicado a rotas específicas
  • Built-in Middleware nativo do Express
  • Third-party Middleware de terceiros

Exemplos de Middleware:

Middleware de Logging

// Middleware personalizado para logging
const logger = (req, res, next) => {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${req.method} ${req.url}`);
    next(); // Chama o próximo middleware
};

// Usar o middleware
app.use(logger);

Middleware Built-in

// Middleware para parsing de JSON
app.use(express.json());

// Middleware para parsing de URL-encoded
app.use(express.urlencoded({ extended: true }));

// Middleware para arquivos estáticos
app.use(express.static('public'));

Middleware de Erro

// Middleware de tratamento de erro (sempre por último)
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({
        error: 'Algo deu errado!',
        message: err.message
    });
});

// Middleware para rotas não encontradas
app.use('*', (req, res) => {
    res.status(404).json({
        error: 'Rota não encontrada',
        path: req.originalUrl
    });
});
PASSO 8 🔨 CÓDIGO PRÁTICO

Projeto Aplicado: Servidor da Padaria

Vamos criar o servidor básico para o projeto da Padaria Doce Sabor, implementando as rotas iniciais e servindo arquivos estáticos.

🏗️ Estrutura do Projeto

padaria-doce-sabor/
├── app.js
├── package.json
├── public/
│   ├── index.html
│   ├── produtos.html
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
└── routes/
    ├── produtos.js
    └── categorias.js

app.js - Servidor Principal

const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

// Middleware de logging
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next();
});

// Rotas principais
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.get('/produtos', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'produtos.html'));
});

// API Routes
app.get('/api/produtos', (req, res) => {
    // Dados mockados por enquanto
    const produtos = [
        {
            id: 1,
            nome: 'Pão Francês',
            preco: 0.50,
            categoria: 'Pães',
            disponivel: true
        },
        {
            id: 2,
            nome: 'Croissant',
            preco: 3.50,
            categoria: 'Doces',
            disponivel: true
        },
        {
            id: 3,
            nome: 'Coxinha',
            preco: 4.00,
            categoria: 'Salgados',
            disponivel: true
        }
    ];
    
    res.json(produtos);
});

app.get('/api/categorias', (req, res) => {
    const categorias = [
        { id: 1, nome: 'Pães', descricao: 'Pães frescos e quentinhos' },
        { id: 2, nome: 'Doces', descricao: 'Doces e sobremesas deliciosas' },
        { id: 3, nome: 'Salgados', descricao: 'Salgados assados e fritos' }
    ];
    
    res.json(categorias);
});

// Middleware de erro 404
app.use('*', (req, res) => {
    res.status(404).json({
        error: 'Página não encontrada',
        path: req.originalUrl
    });
});

// Iniciar servidor
app.listen(PORT, () => {
    console.log(`🥖 Servidor da Padaria Doce Sabor rodando na porta ${PORT}`);
    console.log(`🌐 Acesse: http://localhost:${PORT}`);
});

public/index.html - Página Principal




    
    
    Padaria Doce Sabor
    


    

🥖 Padaria Doce Sabor

Bem-vindos à nossa padaria!

Produtos frescos e deliciosos todos os dias.

Nossas Categorias

public/js/main.js - JavaScript Frontend

// Carregar categorias quando a página carregar
document.addEventListener('DOMContentLoaded', async () => {
    try {
        const response = await fetch('/api/categorias');
        const categorias = await response.json();
        
        const container = document.getElementById('categorias-container');
        
        categorias.forEach(categoria => {
            const div = document.createElement('div');
            div.className = 'categoria-card';
            div.innerHTML = `
                

${categoria.nome}

${categoria.descricao}

`; container.appendChild(div); }); } catch (error) { console.error('Erro ao carregar categorias:', error); } });

9. Melhorias Avançadas do Projeto

Próximo Nível: Agora que você domina o básico do Express, vamos adicionar funcionalidades mais avançadas ao nosso projeto da padaria para torná-lo mais profissional!

9.1 Criando a Página de Produtos

Vamos criar uma página dedicada para listar todos os produtos da nossa padaria. Atualmente, o arquivo produtos.html está vazio, vamos preenchê-lo!

📁 public/produtos.html - Página de Produtos Completa




    
    
    Produtos - Padaria Doce Sabor
    


    

🥖 Padaria Doce Sabor

Nossos Produtos

Descubra todos os sabores frescos da nossa padaria!

Filtrar por Categoria

Carregando produtos...

💡 Novidades nesta página:

  • Filtros por categoria: Botões para filtrar produtos
  • Loading state: Indicador de carregamento
  • Tratamento de erro: Mensagem quando algo dá errado
  • Grid responsivo: Layout que se adapta ao tamanho da tela

🎨 CSS Adicional para Produtos

Adicione estes estilos ao seu arquivo public/css/style.css:

/* Estilos para página de produtos */
.filtros {
    padding: 2rem 0;
    text-align: center;
}

.filtro-buttons {
    display: flex;
    justify-content: center;
    gap: 1rem;
    margin-top: 1rem;
    flex-wrap: wrap;
}

.filtro-btn {
    padding: 0.5rem 1rem;
    border: 2px solid #8B4513;
    background: white;
    color: #8B4513;
    border-radius: 25px;
    cursor: pointer;
    transition: all 0.3s ease;
}

.filtro-btn:hover,
.filtro-btn.active {
    background: #8B4513;
    color: white;
}

.produtos-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 2rem;
    padding: 2rem;
}

.produto-card {
    background: white;
    border-radius: 15px;
    padding: 1.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s ease, box-shadow 0.3s ease;
    border: 3px solid #DAA520;
}

.produto-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}

.produto-card h4 {
    color: #8B4513;
    margin-bottom: 0.5rem;
    font-size: 1.2rem;
}

.produto-card .preco {
    font-size: 1.5rem;
    font-weight: bold;
    color: #DAA520;
    margin: 1rem 0;
}

.produto-card .categoria {
    background: #8B4513;
    color: white;
    padding: 0.25rem 0.75rem;
    border-radius: 15px;
    font-size: 0.8rem;
    display: inline-block;
    margin-bottom: 1rem;
}

.produto-card .disponibilidade {
    font-weight: bold;
}

.produto-card .disponivel {
    color: #10B981;
}

.produto-card .indisponivel {
    color: #EF4444;
}

.loading-container {
    text-align: center;
    padding: 3rem;
    font-size: 1.2rem;
    color: #8B4513;
}

.erro-message {
    text-align: center;
    padding: 2rem;
    background: #FEE2E2;
    border: 2px solid #EF4444;
    border-radius: 10px;
    margin: 2rem;
    color: #DC2626;
}

/* Responsividade */
@media (max-width: 768px) {
    .produtos-grid {
        grid-template-columns: 1fr;
        padding: 1rem;
    }
    
    .filtro-buttons {
        flex-direction: column;
        align-items: center;
    }
}
PASSO 9 🔨 CÓDIGO PRÁTICO

Testando APIs com Thunder Client

Thunder Client: Extensão do VS Code que permite testar APIs REST diretamente no editor, similar ao Postman mas integrado ao VS Code.

1. Instalação do Thunder Client

📦 Como instalar:

  1. Abra o VS Code
  2. Vá em Extensions (Ctrl+Shift+X)
  3. Pesquise por "Thunder Client"
  4. Clique em "Install" na extensão do Ranga Vadhineni
  5. Após instalação, aparecerá um ícone de raio na barra lateral

2. Testando Rotas de Produtos

Vamos testar todas as rotas da API de produtos usando os métodos HTTP principais. Certifique-se de que seu servidor Express esteja rodando na porta 3000!

GET Buscar Todos os Produtos

URL: http://localhost:3000/api/produtos
Método: GET
Headers: Nenhum necessário
Body: Nenhum
Resposta esperada:
[
  {
    "id": 1,
    "nome": "Pão Francês",
    "preco": 0.50,
    "categoria": "Pães",
    "disponivel": true
  },
  {
    "id": 2,
    "nome": "Croissant",
    "preco": 3.50,
    "categoria": "Doces",
    "disponivel": true
  }
]

GET Buscar Produto por ID

URL: http://localhost:3000/api/produtos/1
Método: GET
Headers: Nenhum necessário
Body: Nenhum

POST Criar Novo Produto

URL: http://localhost:3000/api/produtos
Método: POST
Headers:
Content-Type: application/json
Body (JSON):
{
  "nome": "Brigadeiro",
  "preco": 2.50,
  "categoria": "Doces",
  "disponivel": true
}

PUT Atualizar Produto

URL: http://localhost:3000/api/produtos/1
Método: PUT
Headers:
Content-Type: application/json
Body (JSON):
{
  "nome": "Pão Francês Premium",
  "preco": 0.75,
  "categoria": "Pães",
  "disponivel": true
}

DELETE Deletar Produto

URL: http://localhost:3000/api/produtos/3
Método: DELETE
Headers: Nenhum necessário
Body: Nenhum

3. Passo a Passo no Thunder Client

  1. Abrir Thunder Client: Clique no ícone do raio na barra lateral do VS Code
  2. Nova Requisição: Clique em "New Request"
  3. Configurar Método: Selecione o método HTTP (GET, POST, PUT, DELETE)
  4. Inserir URL: Cole a URL da API que deseja testar
  5. Configurar Headers: Se necessário, adicione headers na aba "Headers"
  6. Configurar Body: Para POST/PUT, vá na aba "Body", selecione "JSON" e cole o JSON
  7. Enviar: Clique em "Send" para executar a requisição
  8. Verificar Resposta: Analise a resposta na parte inferior da tela

Importante: Para que os testes funcionem, você precisa implementar as rotas correspondentes no seu servidor Express. Os exemplos acima assumem que você tem rotas configuradas para todos os métodos HTTP.

📚 Resumo da Aula

O que você aprendeu:

  • O que é Express.js e suas características
  • Como criar um servidor Express básico
  • Rotas e métodos HTTP (GET, POST, PUT, DELETE)
  • Servir arquivos estáticos
  • Conceitos básicos de middleware

Próxima aula:

Aula 4: Rotas e Middleware

Aprenda a organizar rotas em módulos e criar middleware personalizado

Ir para próxima aula