Desenvolvimento Full-Stack com Angular, Node.js e MySQL: 100 Exemplos do Zero ao Profissional

Desenvolvimento Full-Stack com Angular, Node.js e MySQL: 100 Exemplos do Zero ao Profissional

Uma aplicação full-stack tem três partes que precisam concordar entre si: um front end Angular que as pessoas usam no navegador, uma API em Node.js que aplica as regras, e um banco de dados MySQL que mantém os dados seguros. Este artigo constrói uma pequena loja desde uma pasta vazia até uma versão pensada para produção, em 100 exemplos. Você vai preparar o projeto, aprender o básico de Node.js e Express para criar uma API REST, modelar tabelas no MySQL e escrever consultas seguras, conectar a API ao banco com transações, validar entradas, adicionar login com senhas com hash e tokens JWT, criar componentes, serviços, rotas, formulários e interceptadores no Angular, ligar tudo por meio de proxy e arquivos de ambiente, testar cada camada, empacotar o sistema com Docker e prepará-lo para produção. Nota de transparência: o MySQL e o Angular CLI não estavam instalados no computador usado para preparar este artigo, então nenhum dos 100 exemplos foi executado aqui. O código segue a sintaxe atual de Node.js, Express, mysql2, Angular e MySQL, mas execute cada parte você mesmo antes de confiar nela.

O que você precisa

Node.js 20 ou mais recente com npm, MySQL 8.4 (ou Docker para executá-lo em um contêiner), o Angular CLI e um editor de código como o Visual Studio Code. Conhecimentos básicos de JavaScript e HTML ajudam. Use senhas de exemplo como <strong-password> e domínios de exemplo como shop.example.com, nunca envie um arquivo .env real para o repositório e não cole credenciais reais em código ou comandos.

Como usar um exemplo

Os exemplos em bash usam npm, o Angular CLI e o Docker. Os exemplos em JavaScript e TypeScript vão para os arquivos de projeto do Node.js e do Angular indicados no texto. Os exemplos em SQL rodam no MySQL 8.4, e os exemplos em HTML vão para os templates do Angular.

Tópico 1: Configuração do projeto

Comece com uma estrutura limpa: uma pasta para a API, uma para o aplicativo Angular e uma para os scripts do banco de dados. Uma boa estrutura torna cada parte fácil de testar, implantar e explicar para alguém novo.

Três partes, um repositório

Mantenha a API, o aplicativo web e os scripts do banco em um único repositório, para que uma mudança possa atualizar as três partes juntas. Cada parte tem seu próprio arquivo de pacotes e seus próprios comandos.

Ferramentas e versões

Verifique primeiro o Node.js, o npm e o MySQL. Recomenda-se o Node.js 20 ou mais recente, porque ele inclui o executor de testes nativo e o fetch moderno. O Angular CLI gera o código e executa o servidor de desenvolvimento.

Segredos fora do Git

Senhas e chaves de assinatura ficam em um arquivo .env listado no .gitignore. Envie um arquivo de exemplo com valores substitutos para que os outros saibam quais configurações são necessárias.

Ilustrações

Como as partes da aplicação funcionam juntas: o aplicativo Angular no navegador chama a API Node.js por meio de /api, a API verifica o token e consulta o MySQL, e a resposta volta para a tela
Como as partes da aplicação funcionam juntas: o aplicativo Angular no navegador chama a API Node.js por meio de /api, a API verifica o token e consulta o MySQL, e a resposta volta para a tela

Exemplo 1: Verificar as ferramentas

Confirme que Node.js, npm e MySQL estão instalados.

node --version
npm --version
mysql --version

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 2: Criar as pastas do projeto

Separe a API, o aplicativo web e o banco de dados.

mkdir shop-fullstack && cd shop-fullstack
mkdir api web db
git init

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 3: Preparar a API em Node.js

Instale o Express, o driver do MySQL e o dotenv.

cd api
npm init -y
npm install express mysql2 dotenv
npm install --save-dev nodemon

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 4: Instalar o Angular CLI

Crie o front end Angular com roteamento.

npm install -g @angular/cli
cd ..
ng new web --routing --style=css --skip-git

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 5: Estrutura do projeto

Mantenha as três partes em pastas claras.

shop-fullstack/
  api/        Node.js + Express + MySQL
  web/        Angular front end
  db/         SQL schema and seed data
  docker-compose.yml

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 6: Manter segredos fora do Git

Ignore o .env e envie um arquivo de exemplo.

cat > .gitignore <<'EOF'
node_modules/
.env
dist/
EOF
cat > api/.env.example <<'EOF'
DB_HOST=localhost
DB_USER=shop
DB_PASSWORD=<strong-password>
DB_NAME=shop
PORT=3000
EOF

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 2: Fundamentos do Node.js

O Node.js executa JavaScript fora do navegador. Aprenda módulos, código assíncrono, variáveis de ambiente e o pequeno conjunto de ideias que todo arquivo de servidor usa.

Módulos e código assíncrono

Cada arquivo é um módulo que exporta o que os outros arquivos precisam. A maior parte do trabalho de um servidor é assíncrona, então use Promises com async e await, e trate erros com try e catch para que uma falha não derrube o processo inteiro.

Configuração pelo ambiente

Leia configurações como a porta e o endereço do banco a partir de process.env. O pacote dotenv carrega um arquivo .env durante o desenvolvimento; servidores de produção normalmente recebem as variáveis da plataforma.

Scripts e o event loop

Scripts do npm dão a cada membro da equipe os mesmos comandos. Lembre-se de que uma única operação síncrona lenta bloqueia todas as requisições, então mantenha o trabalho pesado fora dos manipuladores de requisição.

Exemplo 7: Ler um arquivo com Node.js

Leitura síncrona de arquivos e seu custo.

const fs = require("node:fs");

const text = fs.readFileSync("notes.txt", "utf8");
console.log(text.split("\n").length, "lines");

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 8: Leitura assíncrona de arquivos

Use Promises com async e await.

const fs = require("node:fs/promises");

async function main() {
  const text = await fs.readFile("notes.txt", "utf8");
  console.log(text.trim());
}
main().catch(console.error);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 9: Módulos no Node.js

Exporte uma função e importe-a em outro arquivo.

// math.js
exports.add = (a, b) => a + b;

// main.js
const { add } = require("./math");
console.log(add(2, 3));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 10: Variáveis de ambiente

Leia as configurações do .env com dotenv.

require("dotenv").config();

const port = Number(process.env.PORT || 3000);
console.log("port", port);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 11: Arrays e filtros

Filtre e transforme dados como a API faz.

const orders = [
  { id: 1, total: 120 },
  { id: 2, total: 80 },
  { id: 3, total: 200 },
];
const big = orders.filter((o) => o.total >= 100).map((o) => o.id);
console.log(big); // [1, 3]

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 12: Promises e await

Espere o trabalho sem bloquear o servidor.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function run() {
  console.log("start");
  await wait(500);
  console.log("done after 500 ms");
}
run();

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 13: Erros com try e catch

Trate falhas e devolva um valor padrão seguro.

async function readConfig(path) {
  try {
    const fs = require("node:fs/promises");
    return JSON.parse(await fs.readFile(path, "utf8"));
  } catch (err) {
    console.error("could not read config:", err.message);
    return {};
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 14: Scripts do npm

Inicie, desenvolva e teste a API com um comando.

{
  "name": "api",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "node --test"
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 3: APIs REST com Express

O Express associa métodos e caminhos HTTP a funções. Uma API REST usa GET para ler, POST para criar, PUT ou PATCH para alterar e DELETE para remover, e devolve JSON que o front end pode exibir.

Rotas, parâmetros e corpos

app.get, app.post e similares associam um método a um caminho. Leia os parâmetros da URL em req.params, a query string em req.query e o corpo JSON em req.body depois de chamar app.use(express.json()).

Roteadores e middleware

Um roteador agrupa as rotas de um recurso em um arquivo. O middleware é executado em ordem para cada requisição, por isso é o lugar certo para registro de logs, autenticação e tratamento de erros.

Códigos de status com significado

Devolva 200 para leituras, 201 para recursos criados, 204 para exclusões sem corpo, 400 para entradas inválidas, 401 quando o usuário não está autenticado, 403 quando ele não tem permissão e 404 quando o item não existe.

Exemplo 15: Um primeiro servidor Express

Responda uma requisição em poucas linhas.

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

app.get("/", (req, res) => {
  res.send("Shop API is running");
});

app.listen(3000, () => console.log("listening on http://localhost:3000"));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 16: Uma rota que devolve JSON

Envie dados estruturados para o front end.

app.get("/api/health", (req, res) => {
  res.json({ status: "ok", time: new Date().toISOString() });
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 17: Parâmetros de rota e query string

Leia valores da URL.

app.get("/api/products/:id", (req, res) => {
  const id = Number(req.params.id);
  const detail = req.query.detail === "1";
  res.json({ id, detail });
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 18: Interpretar o corpo JSON

Receba dados de um formulário ou do Angular.

app.use(express.json());

app.post("/api/products", (req, res) => {
  const { name, price } = req.body;
  res.status(201).json({ name, price });
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 19: Organizar rotas em um roteador

Guarde cada funcionalidade em seu próprio arquivo.

// routes/products.js
const { Router } = require("express");
const router = Router();

router.get("/", (req, res) => res.json([]));
router.get("/:id", (req, res) => res.json({ id: req.params.id }));

module.exports = router;

// server.js
app.use("/api/products", require("./routes/products"));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 20: Middleware de registro de requisições

Registre método, caminho, status e tempo.

app.use((req, res, next) => {
  const started = Date.now();
  res.on("finish", () => {
    console.log(req.method, req.originalUrl, res.statusCode, Date.now() - started, "ms");
  });
  next();
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 21: Um tratador de erros central

Devolva um único formato de erro em todas as rotas.

app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: "Something went wrong" });
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 22: 404 para rotas desconhecidas

Responda com clareza quando a URL não existe.

app.use((req, res) => {
  res.status(404).json({ error: "Not found" });
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 4: Fundamentos do MySQL e do SQL

O MySQL armazena dados em tabelas com colunas tipadas, chaves e relacionamentos. O SQL cria, lê, altera e remove linhas, e um bom modelo evita muitos erros antes de qualquer código ser executado.

Bancos, usuários e menor privilégio

Crie um banco para a loja e um usuário que só consiga ler e gravar nesse banco. Nunca deixe a aplicação conectar como root, porque uma senha vazada exporia o servidor inteiro.

Tabelas, chaves e tipos

Dê a cada tabela uma chave primária, escolha tipos compatíveis com os dados, como DECIMAL para valores monetários, e use chaves estrangeiras para que um pedido não aponte para um produto inexistente. Use UNIQUE para valores que não podem se repetir, como endereços de e-mail.

Ler e alterar linhas

Um SELECT com WHERE, ORDER BY e LIMIT devolve apenas as linhas necessárias. UPDATE e DELETE devem sempre ter uma cláusula WHERE, testada antes com um SELECT, porque uma condição ausente altera todas as linhas.

Exemplo 23: Criar o banco de dados e o usuário

Dê à aplicação somente os direitos de que precisa.

CREATE DATABASE IF NOT EXISTS shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'shop'@'localhost' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop'@'localhost';

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 24: Criar uma tabela

Defina colunas, chaves e valores padrão.

USE shop;
CREATE TABLE products (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(120) NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  stock INT NOT NULL DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 25: Inserir linhas

Adicione várias linhas em uma única instrução.

INSERT INTO products (name, price, stock)
VALUES ('Keyboard', 790.00, 12),
       ('Mouse', 350.00, 30),
       ('Monitor', 5490.00, 4);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 26: SELECT com filtro e ordenação

Leia apenas as linhas necessárias, em ordem.

SELECT id, name, price
FROM products
WHERE stock > 0
ORDER BY price DESC;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 27: Atualizar uma linha

Altere valores com uma cláusula WHERE.

UPDATE products
SET price = 690.00
WHERE name = 'Keyboard';

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 28: Excluir uma linha com segurança

Limite sempre a exclusão às linhas pretendidas.

DELETE FROM products
WHERE id = 3
LIMIT 1;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 29: JOIN e agregação

Conte as vendas por produto com GROUP BY.

CREATE TABLE orders (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  product_id INT UNSIGNED NOT NULL,
  quantity INT NOT NULL,
  ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (product_id) REFERENCES products(id)
);
SELECT p.name, SUM(o.quantity) AS sold
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY p.name
ORDER BY sold DESC;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 30: Indexar uma coluna pesquisada

Acelere buscas e confira o plano com EXPLAIN.

CREATE INDEX idx_products_name ON products (name);
EXPLAIN SELECT * FROM products WHERE name = 'Mouse';

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 5: Acesso aos dados a partir do Node.js

A API conversa com o MySQL por um pool de conexões. Consultas parametrizadas separam a entrada do usuário do SQL, transações mantêm juntas as alterações relacionadas, e a paginação mantém as respostas pequenas.

Pools de conexões

Um pool mantém um conjunto de conexões abertas e entrega uma a cada requisição, devolvendo-a depois. Crie o pool uma vez na inicialização e reutilize-o, em vez de abrir uma nova conexão para cada requisição.

Placeholders evitam injeção de SQL

Nunca junte a entrada do usuário ao texto SQL. Escreva ? na consulta e passe os valores em um array, e o driver os escapa. Esse único hábito evita uma das falhas de segurança mais comuns na web.

Transações e paginação

Uma transação agrupa várias instruções para que todas tenham sucesso ou nenhuma. Isso importa quando um pedido reduz o estoque e cria a linha do pedido. Use LIMIT e OFFSET, ou melhor, keyset, para devolver uma página por vez.

Exemplo 31: Um pool de conexões

Reutilize conexões em vez de abrir uma por requisição.

const mysql = require("mysql2/promise");

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
});
module.exports = pool;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 32: Consultar a partir do Node.js

Envie SQL a partir de uma rota e devolva as linhas.

const pool = require("./db");

app.get("/api/products", async (req, res, next) => {
  try {
    const [rows] = await pool.query("SELECT id, name, price, stock FROM products ORDER BY name");
    res.json(rows);
  } catch (err) {
    next(err);
  }
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 33: Placeholders contra injeção de SQL

Nunca junte a entrada do usuário ao texto SQL.

// Unsafe: never build SQL by joining user input
// const [rows] = await pool.query("SELECT * FROM products WHERE name = '" + name + "'");

// Safe: the driver escapes the value
const [rows] = await pool.query("SELECT * FROM products WHERE name = ?", [name]);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 34: Inserir e devolver o novo id

Crie a linha e informe o id ao cliente.

app.post("/api/products", async (req, res, next) => {
  try {
    const { name, price, stock = 0 } = req.body;
    const [result] = await pool.execute(
      "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
      [name, price, stock]
    );
    res.status(201).json({ id: result.insertId, name, price, stock });
  } catch (err) {
    next(err);
  }
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 35: Atualizar com verificação

Devolva 404 quando nenhuma linha corresponder.

app.put("/api/products/:id", async (req, res, next) => {
  try {
    const { price, stock } = req.body;
    const [result] = await pool.execute(
      "UPDATE products SET price = ?, stock = ? WHERE id = ?",
      [price, stock, req.params.id]
    );
    if (result.affectedRows === 0) return res.status(404).json({ error: "Not found" });
    res.json({ updated: true });
  } catch (err) {
    next(err);
  }
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 36: Excluir com verificação

Devolva 204 ou 404 conforme o resultado.

app.delete("/api/products/:id", async (req, res, next) => {
  try {
    const [result] = await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
    res.status(result.affectedRows ? 204 : 404).end();
  } catch (err) {
    next(err);
  }
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 37: Uma transação

Reserve o estoque e crie o pedido juntos, ou não faça nada.

async function placeOrder(productId, quantity) {
  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();
    const [stock] = await conn.execute("SELECT stock FROM products WHERE id = ? FOR UPDATE", [productId]);
    if (stock[0].stock < quantity) throw new Error("not enough stock");
    await conn.execute("UPDATE products SET stock = stock - ? WHERE id = ?", [quantity, productId]);
    await conn.execute("INSERT INTO orders (product_id, quantity) VALUES (?, ?)", [productId, quantity]);
    await conn.commit();
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 38: Paginação

Devolva páginas de resultados em vez da tabela inteira.

app.get("/api/products", async (req, res, next) => {
  const page = Math.max(1, Number(req.query.page) || 1);
  const size = Math.min(50, Number(req.query.size) || 20);
  try {
    const [rows] = await pool.execute(
      "SELECT id, name, price FROM products ORDER BY id LIMIT ? OFFSET ?",
      [size, (page - 1) * size]
    );
    res.json({ page, size, items: rows });
  } catch (err) {
    next(err);
  }
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 6: Validação e tratamento de erros

A API nunca deve confiar no navegador. Verifique cada entrada, devolva mensagens claras para os erros e converta erros do banco em respostas que o front end pode mostrar sem vazar detalhes internos.

Validar no servidor

As verificações no Angular melhoram a experiência, mas o servidor precisa verificar de novo, porque qualquer pessoa pode enviar uma requisição diretamente. Uma biblioteca de schema descreve os campos uma vez e relata todos os problemas em uma única resposta.

Um único formato de erro

Use um tratador de erros central que devolva o mesmo formato JSON para qualquer falha. Mapeie erros conhecidos do banco, como chaves duplicadas, para 409, registre o erro completo no servidor e envie ao cliente apenas uma mensagem segura.

Erros em rotas assíncronas

O Express 4 não captura automaticamente Promises rejeitadas pelos manipuladores de rota. Envolva as rotas assíncronas para que os erros cheguem ao tratador, ou use o Express 5, que faz isso por você.

Exemplo 39: Validar a entrada

Verifique os campos antes que cheguem ao banco de dados.

function validateProduct(body) {
  const errors = [];
  if (typeof body.name !== "string" || body.name.trim().length < 2) errors.push("name must be at least 2 characters");
  if (typeof body.price !== "number" || body.price < 0) errors.push("price must be a number >= 0");
  return errors;
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 40: Devolver 400 para entrada inválida

Explique cada erro de validação ao cliente.

app.post("/api/products", async (req, res, next) => {
  const errors = validateProduct(req.body);
  if (errors.length) return res.status(400).json({ errors });
  // ...insert as in example 34
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 41: Validar com um schema

Descreva os dados uma vez e reutilize as regras.

const { z } = require("zod");

const productSchema = z.object({
  name: z.string().min(2).max(120),
  price: z.number().nonnegative(),
  stock: z.number().int().nonnegative().default(0),
});

const parsed = productSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues });

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 42: Transformar erros do banco em respostas

Mapeie duplicatas e referências ausentes para códigos claros.

app.use((err, req, res, next) => {
  if (err.code === "ER_DUP_ENTRY") return res.status(409).json({ error: "Already exists" });
  if (err.code === "ER_NO_REFERENCED_ROW_2") return res.status(422).json({ error: "Related item not found" });
  next(err);
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 43: Uma classe de erro própria

Carregue o status HTTP junto com a mensagem de erro.

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

// usage
throw new HttpError(404, "Product not found");

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 44: Um wrapper de rotas async

Envie os erros das rotas async para o tratador.

const asyncRoute = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get("/api/products/:id", asyncRoute(async (req, res) => {
  const [rows] = await pool.execute("SELECT * FROM products WHERE id = ?", [req.params.id]);
  if (!rows.length) throw new HttpError(404, "Product not found");
  res.json(rows[0]);
}));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 45: CORS para o servidor de desenvolvimento do Angular

Permita que o navegador chame a API a partir de outra porta.

const cors = require("cors");

app.use(cors({ origin: "http://localhost:4200" }));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 46: Cabeçalhos de segurança

Defina cabeçalhos HTTP seguros com o helmet.

const helmet = require("helmet");

app.use(helmet());
app.disable("x-powered-by");

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 7: Autenticação e autorização

A autenticação comprova quem é o usuário. A autorização decide o que ele pode fazer. Guarde senhas como hashes, emita tokens de vida curta e verifique o papel em cada rota protegida.

Senhas com hash

O bcrypt transforma a senha em um hash de mão única com sal embutido e fator de custo. Guarde somente o hash, compare com bcrypt.compare no login, e nunca registre senhas nem as devolva em respostas.

Tokens e sua validade

Um JSON Web Token carrega o id e o papel do usuário, assinado com um segredo. Mantenha a validade curta, por exemplo uma hora, e use um refresh token revogável para sessões mais longas. Guarde o refresh token como hash no banco.

Papéis e proteção de rotas

Verifique o token em um middleware e, depois, o papel para ações exclusivas de administradores. Lembre-se de que o front end pode esconder um botão, mas só o servidor consegue aplicar uma regra.

Exemplo 47: Criptografar senhas

Guarde o hash bcrypt, nunca a senha.

const bcrypt = require("bcrypt");

async function hashPassword(plain) {
  return bcrypt.hash(plain, 12);
}

async function checkPassword(plain, hash) {
  return bcrypt.compare(plain, hash);
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 48: Uma tabela de usuários

Guarde e-mails únicos, hashes de senha e papéis.

CREATE TABLE users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(190) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  role ENUM('customer', 'admin') NOT NULL DEFAULT 'customer',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 49: Registrar um usuário

Crie uma conta com a senha em hash.

app.post("/api/auth/register", asyncRoute(async (req, res) => {
  const { email, password } = req.body;
  const hash = await hashPassword(password);
  const [result] = await pool.execute(
    "INSERT INTO users (email, password_hash) VALUES (?, ?)",
    [email.toLowerCase(), hash]
  );
  res.status(201).json({ id: result.insertId, email });
})));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 50: Entrar e emitir um token

Devolva um JWT assinado após uma senha correta.

const jwt = require("jsonwebtoken");

app.post("/api/auth/login", asyncRoute(async (req, res) => {
  const [rows] = await pool.execute("SELECT id, password_hash, role FROM users WHERE email = ?", [req.body.email]);
  if (!rows.length || !(await checkPassword(req.body.password, rows[0].password_hash))) {
    throw new HttpError(401, "Wrong email or password");
  }
  const token = jwt.sign({ sub: rows[0].id, role: rows[0].role }, process.env.JWT_SECRET, { expiresIn: "1h" });
  res.json({ token });
})));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 51: Verificar o JWT em um middleware

Leia o token Bearer e recuse tokens inválidos ou expirados.

const jwt = require("jsonwebtoken");

function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: "Missing token" });
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: "Invalid or expired token" });
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 52: Permitir apenas alguns papéis

Devolva 403 quando o usuário não tiver o papel exigido.

function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ error: "Forbidden" });
    }
    next();
  };
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 53: Proteger rotas

Exija login para pedidos e restrinja a exclusão ao administrador.

app.get("/api/orders", requireAuth, asyncRoute(async (req, res) => {
  const [rows] = await pool.execute("SELECT id, product_id, quantity FROM orders WHERE user_id = ?", [req.user.sub]);
  res.json(rows);
})));

app.delete("/api/products/:id", requireAuth, requireRole("admin"), asyncRoute(async (req, res) => {
  await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
  res.status(204).end();
}));

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 54: Guardar refresh tokens

Guarde o hash de cada refresh token para poder revogá-lo.

CREATE TABLE refresh_tokens (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id INT UNSIGNED NOT NULL,
  token_hash CHAR(64) NOT NULL,
  expires_at DATETIME NOT NULL,
  revoked TINYINT(1) NOT NULL DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 8: Componentes e templates do Angular

O Angular constrói a interface a partir de componentes. Cada componente tem uma classe com a lógica, um template com a marcação e estilos. Componentes standalone, fluxo de controle e bindings são a forma moderna de escrevê-los.

Componentes standalone

Um componente standalone declara seus próprios imports e não precisa de NgModule. Gere-o com ng generate component e use o seletor em outro template para exibi-lo.

Bindings

A interpolação mostra valores entre chaves duplas. O property binding define uma propriedade do DOM entre colchetes, e o event binding chama um método entre parênteses. O binding bidirecional também é possível, mas o fluxo de dados em um único sentido é mais fácil de depurar.

Fluxo de controle

Use @if para exibir conteúdo condicionalmente, @for com uma expressão track para repetir itens, e @switch para escolher entre várias visões. Rastrear por id mantém a página eficiente quando a lista muda.

Exemplo 55: Um componente Angular standalone

Um componente que não precisa de NgModule.

import { Component } from "@angular/core";

@Component({
  selector: "app-hello",
  standalone: true,
  template: `<h1>Welcome to the shop</h1>`,
})
export class HelloComponent {}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 56: Data binding no template

Mostre valores, vincule atributos e trate cliques.

<h2>{{ product.name }}</h2>
<p>Price: {{ product.price | currency:'THB' }}</p>
<button [disabled]="product.stock === 0" (click)="addToCart(product)">Add to cart</button>

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 57: Controle de fluxo com @if e @for

Mostre listas e estados vazios no template.

@if (products.length > 0) {
  <ul>
    @for (p of products; track p.id) {
      <li>{{ p.name }} ({{ p.stock }} left)</li>
    }
  </ul>
} @else {
  <p>No products yet.</p>
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 58: Entradas e saídas

Envie dados a um componente filho e devolva eventos ao pai.

import { Component, Input, Output, EventEmitter } from "@angular/core";

@Component({ selector: "app-product-card", standalone: true, template: "" })
export class ProductCardComponent {
  @Input({ required: true }) product!: { id: number; name: string; price: number };
  @Output() added = new EventEmitter<number>();

  add() {
    this.added.emit(this.product.id);
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 59: Iniciar uma aplicação standalone

Forneça o roteador e o HttpClient na inicialização.

import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideHttpClient()],
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 60: Gerar e executar com o CLI

Crie componentes e serviços e depois sirva a aplicação.

cd web
ng generate component products/product-list
ng generate service products/product
ng serve --open

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 9: Serviços, HTTP e roteamento

Os serviços guardam a lógica e conversam com a API, para que os componentes permaneçam pequenos. O roteador associa URLs a componentes, guards protegem as páginas, e o carregamento sob demanda mantém pequeno o primeiro download.

Serviços injetáveis e HttpClient

Marque o serviço com providedIn root para que o Angular crie uma única instância para o aplicativo. Injete o HttpClient e devolva Observables tipados com a interface da resposta, para que o compilador verifique os campos que você usa.

Rotas e parâmetros

Defina as rotas como um array de pares caminho e componente. Leia os parâmetros de ActivatedRoute e adicione no fim uma rota curinga que mostra uma página de não encontrado.

Guards e carregamento sob demanda

Um guard CanActivate devolve true ou um redirecionamento. Rotas preguiçosas usam loadComponent com um import dinâmico, então o código de administração só é baixado quando o usuário o abre.

Exemplo 61: Um serviço injetável

Compartilhe estado entre componentes com uma única instância.

import { Injectable } from "@angular/core";

@Injectable({ providedIn: "root" })
export class CartService {
  private items: { id: number; qty: number }[] = [];

  add(id: number) {
    const found = this.items.find((i) => i.id === id);
    if (found) found.qty++;
    else this.items.push({ id, qty: 1 });
  }

  count() {
    return this.items.reduce((sum, i) => sum + i.qty, 0);
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 62: Chamar a API com HttpClient

Requisições tipadas que devolvem Observables.

import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

export interface Product { id: number; name: string; price: number; stock: number; }

@Injectable({ providedIn: "root" })
export class ProductService {
  private http = inject(HttpClient);

  list(): Observable<Product[]> {
    return this.http.get<Product[]>("/api/products");
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 63: Definir rotas

Associe URLs a componentes, com uma página de reserva.

import { Routes } from "@angular/router";

export const routes: Routes = [
  { path: "", pathMatch: "full", redirectTo: "products" },
  { path: "products", component: ProductListComponent },
  { path: "products/:id", component: ProductDetailComponent },
  { path: "**", component: NotFoundComponent },
];

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 64: Um guard de rota

Envie usuários anônimos para a página de login.

import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "./auth.service";

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isLoggedIn() || inject(Router).createUrlTree(["/login"]);
};

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 65: Ler um parâmetro de rota

Carregue um produto a partir da URL.

import { Component, inject } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

@Component({ selector: "app-product-detail", standalone: true, template: "" })
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  readonly id = Number(this.route.snapshot.paramMap.get("id"));
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 66: Carregamento sob demanda de uma funcionalidade

Baixe o código de administração só quando for necessário.

export const routes: Routes = [
  {
    path: "admin",
    canActivate: [authGuard],
    loadComponent: () => import("./admin/admin.component").then((m) => m.AdminComponent),
  },
];

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 10: Formulários e validação

Os formulários coletam os dados que a API armazena. Os formulários reativos mantêm o modelo em TypeScript, o que torna a validação e os testes diretos e deixa o template simples.

Formulários reativos

Monte o formulário com FormBuilder, descreva cada campo com seu valor padrão e seus validadores, e vincule o template com formGroup e formControlName. O valor do formulário é tipado, então o código de envio é verificado pelo compilador.

Validadores personalizados e assíncronos

Um validador é uma função que devolve null quando o valor é válido, ou um objeto que descreve o erro. Escreva um para regras que os validadores prontos não cobrem, como valores formados apenas por espaços.

Exibir erros e enviar dados

Mostre mensagens somente depois que o usuário tocar em um campo, para que o formulário não comece em estado de erro. Quando o servidor devolver erros de validação, exiba-os junto ao formulário, e não em um alerta genérico.

Exemplo 67: Um formulário reativo

Monte o formulário em TypeScript com validadores.

import { Component, inject } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";

@Component({ selector: "app-product-form", standalone: true, imports: [ReactiveFormsModule], templateUrl: "./product-form.html" })
export class ProductFormComponent {
  private fb = inject(FormBuilder);
  form = this.fb.nonNullable.group({
    name: ["", [Validators.required, Validators.minLength(2)]],
    price: [0, [Validators.required, Validators.min(0)]],
    stock: [0, [Validators.min(0)]],
  });
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 68: O template do formulário

Vincule os campos ao modelo do formulário.

<form [formGroup]="form" (ngSubmit)="save()">
  <label>Name <input formControlName="name"></label>
  <label>Price <input type="number" formControlName="price"></label>
  <label>Stock <input type="number" formControlName="stock"></label>
  <button type="submit" [disabled]="form.invalid">Save</button>
</form>

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 69: Um validador personalizado

Recuse valores que contenham só espaços.

import { AbstractControl, ValidationErrors } from "@angular/forms";

export function noSpacesOnly(control: AbstractControl): ValidationErrors | null {
  const value = String(control.value ?? "");
  return value.trim().length === 0 && value.length > 0 ? { spacesOnly: true } : null;
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 70: Mostrar mensagens de validação

Exiba os erros depois que o usuário toca no campo.

@if (form.controls.name.touched && form.controls.name.invalid) {
  <p class="error">
    @if (form.controls.name.errors?.['required']) { Name is required. }
    @if (form.controls.name.errors?.['minlength']) { Name must be at least 2 characters. }
  </p>
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 71: Enviar o formulário para a API

Envie os dados e navegue ao concluir com sucesso.

save() {
  if (this.form.invalid) return;
  this.http.post("/api/products", this.form.getRawValue()).subscribe({
    next: () => this.router.navigate(["/products"]),
    error: (err) => (this.serverError = err.error?.errors ?? ["Could not save"]),
  });
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 72: Mostrar erros do servidor

Exiba as mensagens de validação devolvidas pela API.

// After a 400 response, the API sends { errors: ["price must be a number >= 0"] }
serverError: string[] = [];

// in the template
// @for (e of serverError; track e) { <p class="error">{{ e }}</p> }

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 11: Estado, Observables e interceptadores

Os dados mudam com o tempo, e muitas requisições precisam ser coordenadas. Observables e signals descrevem essa mudança, e os interceptadores acrescentam comportamentos transversais, como tokens e registro de erros, a todas as requisições.

Observables e o async pipe

Um Observable emite valores ao longo do tempo. O async pipe assina no template e cancela a assinatura automaticamente, evitando vazamentos de memória e código de assinatura manual.

Signals para estado local

Um signal guarda um valor que avisa o template quando muda. computed deriva valores a partir de signals, e effect executa código quando eles mudam. Os signals servem bem para estados simples, como um contador ou o total do carrinho.

Interceptadores

Um interceptador HTTP enxerga cada requisição e cada resposta. Use um para acrescentar o cabeçalho Authorization e outro para registrar falhas ou redirecionar para a página de login quando o token expirar.

Exemplo 73: Observables e o async pipe

Renderize uma lista sem assinaturas manuais.

@Component({
  selector: "app-products",
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @for (p of products$ | async; track p.id) {
      <div>{{ p.name }}</div>
    }`,
})
export class ProductsComponent {
  products$ = inject(ProductService).list();
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 74: Um interceptor que adiciona o token

Anexe o cabeçalho Authorization a cada requisição.

import { HttpInterceptorFn } from "@angular/common/http";
import { inject } from "@angular/core";
import { AuthService } from "./auth.service";

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 75: Um interceptor de erros global

Registre as falhas da API em um só lugar.

import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http";
import { catchError, throwError } from "rxjs";

export const errorInterceptor: HttpInterceptorFn = (req, next) =>
  next(req).pipe(
    catchError((err: HttpErrorResponse) => {
      console.error("API error", err.status, req.url);
      return throwError(() => err);
    })
  );

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 76: Signals para estado local

Acompanhe um valor e derive outro a partir dele.

import { Component, computed, signal } from "@angular/core";

@Component({ selector: "app-counter", standalone: true, template: "" })
export class CounterComponent {
  count = signal(0);
  double = computed(() => this.count() * 2);

  increment() {
    this.count.update((n) => n + 1);
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 77: Uma pequena store com signals

Guarde o estado do carrinho em um único serviço.

import { Injectable, computed, signal } from "@angular/core";

@Injectable({ providedIn: "root" })
export class CartStore {
  private readonly lines = signal<{ id: number; qty: number }[]>([]);
  readonly totalQty = computed(() => this.lines().reduce((s, l) => s + l.qty, 0));

  add(id: number) {
    this.lines.update((ls) => [...ls, { id, qty: 1 }]);
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 78: Cancelar a assinatura automaticamente

Pare o fluxo quando o componente for destruído.

import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

export class ClockComponent {
  constructor() {
    interval(1000).pipe(takeUntilDestroyed()).subscribe(() => this.tick());
  }
  tick() {}
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 12: Integração full-stack

Conectar as partes é onde muitos projetos quebram. Um proxy em desenvolvimento, arquivos de ambiente para cada implantação e tipos compartilhados mantêm o front end e a API sincronizados.

Proxy de desenvolvimento

Encaminhe /api do servidor de desenvolvimento do Angular para o servidor Node.js, assim o navegador enxerga uma única origem e nenhuma configuração de CORS é necessária durante o desenvolvimento. Reinicie o ng serve depois de alterar o arquivo do proxy.

Arquivos de ambiente

Os arquivos de ambiente do Angular dão a cada build as suas próprias configurações, como o endereço da API. Mantenha o endereço de produção fora do código-fonte quando ele for sigiloso, e leia-o da configuração de build.

Tipos compartilhados

Descreva o formato de Product uma vez em uma pasta compartilhada e importe-o tanto na API quanto no front end. Um campo renomeado de um lado aparece como erro de compilação do outro, em vez de bug em tempo de execução.

Exemplo 79: Encaminhar chamadas da API em desenvolvimento

Redirecione /api para o servidor Node.js.

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "changeOrigin": true
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 80: Arquivos de ambiente

Use outro endereço de API em produção.

// src/environments/environment.ts
export const environment = {
  production: false,
  apiBaseUrl: "/api",
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: "https://api.shop.example.com",
};

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 81: Montar a URL da API

Combine o endereço base e o caminho em um só lugar.

import { environment } from "../environments/environment";

export const api = (path: string) => `${environment.apiBaseUrl}${path}`;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 82: Executar a API e o front end

Inicie os dois servidores em dois terminais.

# terminal 1
cd api && npm run dev
# terminal 2
cd web && ng serve --proxy-config proxy.conf.json

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 83: Compartilhar tipos entre front end e back end

Mantenha o formato de Product igual nos dois lados.

// shared/product.ts (used by both web and api through a path alias)
export interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

export type CreateProduct = Omit<Product, "id">;

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 84: Verificar a saúde da API a partir do Angular

Mostre se o back end está acessível.

export class AppComponent {
  private http = inject(HttpClient);
  apiOk = signal<boolean | null>(null);

  constructor() {
    this.http.get<{ status: string }>("/api/health").subscribe({
      next: (r) => this.apiOk.set(r.status === "ok"),
      error: () => this.apiOk.set(false),
    });
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 13: Testando cada camada

Os testes pegam erros antes que os usuários os encontrem. Teste funções puras, rotas da API, componentes e serviços do Angular e, por fim, o sistema inteiro com um banco de dados separado.

Testes da API

O Supertest envia requisições ao aplicativo Express sem abrir uma porta real, e o Jest ou o node:test verifica o resultado. Teste o caminho de sucesso e pelo menos uma falha, como um token ausente ou um preço inválido.

Testes do Angular

O TestBed cria o componente com suas dependências. O HttpTestingController responde às chamadas HTTP com dados falsos, então o componente pode ser testado sem uma API em execução.

Integração e cobertura

Execute os testes de integração em um banco de testes separado, reiniciado antes de cada execução, para que os testes nunca alterem dados reais. A cobertura mostra quais trechos de código nenhum teste alcança; é um ponto de partida, não uma garantia.

Exemplo 85: Testar uma rota do Express

Chame a rota com Supertest e verifique a resposta.

const request = require("supertest");
const app = require("../app");

test("GET /api/health returns ok", async () => {
  const res = await request(app).get("/api/health");
  expect(res.status).toBe(200);
  expect(res.body.status).toBe("ok");
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 86: Testar uma função pura

Use o executor de testes node:test, já incluído no Node.js.

const { test } = require("node:test");
const assert = require("node:assert/strict");
const { validateProduct } = require("../validate");

test("rejects a short name", () => {
  assert.equal(validateProduct({ name: "A", price: 10 }).length, 1);
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 87: Testar um componente

Crie o componente com TestBed.

import { TestBed } from "@angular/core/testing";
import { ProductListComponent } from "./product-list.component";

it("creates the component", () => {
  TestBed.configureTestingModule({ imports: [ProductListComponent] });
  const fixture = TestBed.createComponent(ProductListComponent);
  expect(fixture.componentInstance).toBeTruthy();
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 88: Simular HTTP com HttpTestingController

Responda às requisições sem um servidor real.

import { TestBed } from "@angular/core/testing";
import { provideHttpClient } from "@angular/common/http";
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
import { ProductService } from "./product.service";

it("loads products", () => {
  TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
  const service = TestBed.inject(ProductService);
  const http = TestBed.inject(HttpTestingController);
  service.list().subscribe((items) => expect(items.length).toBe(1));
  http.expectOne("/api/products").flush([{ id: 1, name: "Mouse", price: 350, stock: 3 }]);
  http.verify();
});

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 89: Teste de integração com um banco de testes

Execute a API contra um banco de dados separado.

# create a separate test database
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS shop_test"
DB_NAME=shop_test npm test -- --runInBand

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 90: Relatórios de cobertura

Meça quanto código os testes alcançam.

cd api && npx jest --coverage
cd ../web && ng test --code-coverage --watch=false

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 14: Docker e implantação

Os contêineres empacotam cada parte com as dependências exatas. O Docker Compose inicia o banco de dados, a API e o servidor web juntos, e as mesmas imagens podem rodar em um notebook, em um servidor de testes ou em um serviço em nuvem.

Imagens da API e do aplicativo web

Use builds em vários estágios: instale e compile em um estágio, depois copie apenas o que a imagem final precisa. A imagem da API roda com um usuário sem privilégios de root, e a imagem do Angular contém somente os arquivos compilados servidos pelo Nginx.

Compose e inicialização do banco

O Compose inicia o MySQL primeiro e espera a verificação de saúde antes de iniciar a API. Coloque os scripts de schema em uma pasta init, que o MySQL só executa quando o volume de dados está vazio.

Proxy reverso

O Nginx serve os arquivos do Angular, encaminha /api para o contêiner da API e aplica a regra try_files, que faz as rotas do cliente continuarem funcionando depois de atualizar a página.

Exemplo 91: Dockerfile para a API

Uma imagem de produção pequena que roda com usuário não root.

FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 92: Dockerfile para o aplicativo Angular

Gere o build e sirva-o com o Nginx.

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx ng build --configuration production

FROM nginx:1.27-alpine
COPY --from=build /app/dist/web/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 93: Compose com MySQL, API e web

Suba a pilha inteira com um comando.

services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: shop
      MYSQL_USER: shop
      MYSQL_PASSWORD: <strong-password>
      MYSQL_ROOT_PASSWORD: <root-password>
    volumes:
      - db-data:/var/lib/mysql
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 20
  api:
    build: ./api
    env_file: ./api/.env
    depends_on:
      db:
        condition: service_healthy
  web:
    build: ./web
    ports:
      - "80:80"
    depends_on:
      - api
volumes:
  db-data:

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 94: Executar o schema na primeira inicialização

Crie as tabelas automaticamente em um volume novo.

# db/init/01-schema.sql is run once when the data volume is empty
services:
  db:
    volumes:
      - ./db/init:/docker-entrypoint-initdb.d:ro

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Tópico 15: Operação em produção

Um lançamento não termina quando roda uma vez. A produção exige configuração segura, logs estruturados, backups, um pipeline de integração contínua e uma checklist que impeça lançamentos ruins.

Configuração segura

Recuse iniciar quando a chave de assinatura estiver ausente, confie no cabeçalho do proxy somente quando houver um proxy na frente, e desative os cabeçalhos do framework que revelam o software do servidor.

Logs e backups

Escreva logs JSON estruturados com um id de requisição, para que um problema possa ser acompanhado por todo o sistema. Faça backup do banco com mysqldump e --single-transaction, e teste a restauração em outra máquina.

Pipeline e checklist de lançamento

Um fluxo de trabalho de CI executa os testes da API, os testes do Angular e os builds de produção a cada envio. Um script de lançamento verifica o endpoint de saúde e os contêineres antes de anunciar a nova versão.

Exemplo 95: Configurações de produção na API

Recuse iniciar sem uma chave secreta.

const isProd = process.env.NODE_ENV === "production";

if (isProd && !process.env.JWT_SECRET) {
  throw new Error("JWT_SECRET must be set in production");
}
app.set("trust proxy", 1);

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 96: Um proxy reverso com Nginx

Sirva a aplicação e encaminhe /api para a API.

server {
  listen 80;
  root /usr/share/nginx/html;
  location /api/ {
    proxy_pass http://api:3000;
    proxy_set_header Host $host;
  }
  location / {
    try_files $uri /index.html;
  }
}

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 97: Logs estruturados com o pino

Grave logs em JSON que as ferramentas consigam pesquisar.

const pino = require("pino");
const logger = pino({ level: process.env.LOG_LEVEL || "info" });

app.use(require("pino-http")({ logger }));
logger.info({ port: 3000 }, "API started");

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 98: Fazer backup do banco MySQL

Gere um dump consistente e comprima-o.

mysqldump -h db -u shop -p shop --single-transaction > backup-$(date +%F).sql
gzip backup-$(date +%F).sql

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 99: Um workflow de CI no GitHub Actions

Teste e gere o build a cada envio para main.

name: ci
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci --prefix api
      - run: npm test --prefix api
      - run: npm ci --prefix web
      - run: npx ng test --watch=false --prefix web
      - run: npx ng build --configuration production --prefix web

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Exemplo 100: Um script de checklist de lançamento

Interrompa o lançamento se uma verificação falhar.

#!/usr/bin/env bash
set -euo pipefail
curl -fsS https://shop.example.com/api/health | grep -q '"status":"ok"'
docker compose ps --format json | grep -q '"Health":"healthy"'
gzip -t backup-latest.sql.gz
echo "release checks passed"

*Este exemplo não foi executado aqui, porque o MySQL e o Angular CLI não estavam instalados no computador que preparou este artigo.*

Laboratório: construir, testar e lançar uma pequena loja

Reúna o artigo em um único projeto: uma API com login e rotas de produtos, um schema MySQL com uma camada de consultas segura, uma página de produtos no Angular com um formulário reativo, testes para cada camada e uma pilha Docker Compose.

1. Crie as pastas api e web, instale Express, mysql2, dotenv e bcrypt na API e gere o aplicativo Angular com roteamento.

2. Crie as tabelas products e users no MySQL com um usuário de permissões limitadas e insira alguns produtos.

3. Escreva as rotas de produtos com placeholders, validação e um tratador de erros central, e verifique-as com curl ou Supertest.

4. Adicione cadastro e login com senhas em hash e um JWT, depois proteja a rota de pedidos com o middleware.

5. Crie a lista de produtos no Angular com HttpClient, uma rota para os detalhes do produto e um formulário reativo para adicionar um produto.

6. Adicione o arquivo de proxy, execute a API e o aplicativo web juntos e teste o fluxo completo no navegador.

7. Escreva um teste de API e um teste de componente, execute-os, depois suba a pilha inteira com docker compose up e execute a checklist de lançamento do exemplo 100.

Próximos passos

Depois do laboratório, leia a documentação oficial do Angular, do Express e do MySQL, acrescente paginação e busca à loja, e construa uma segunda funcionalidade do início ao fim sem copiar código. Guarde os exemplos em um repositório e execute os testes de novo depois de cada atualização de dependências, porque os frameworks mudam entre versões principais.

Resumo

Uma aplicação full-stack conecta três partes que precisam concordar entre si. Você aprendeu a configurar o projeto e as ferramentas, escrever código em Node.js e Express, modelar tabelas no MySQL e consultas seguras, conectar a API com pools e transações, validar entradas e tratar erros, proteger rotas com senhas em hash e tokens, criar componentes, serviços, rotas, formulários e interceptadores no Angular, ligar o front end à API, testar cada camada, empacotar o sistema com Docker e prepará-lo para produção.