Skip to content
Fase 6Persistência

Bases de Dados e SQL

Progresso da fase0%

A carregar…

Tudo o que guardaste até agora desaparecia assim que o programa terminava. Nesta fase vais aprender a guardar dados de forma permanente numa base de dados relacional, a estrutura de tabelas e chaves, e a linguagem SQL para ler e escrever nessa base de dados — depois ligas tudo a um pequeno servidor Express para teres uma API real a persistir dados num ficheiro.

Prompt recomendado para esta fase

Estou na Fase 6 a aprender bases de dados relacionais e SQL (SELECT, INSERT, UPDATE, DELETE), e a ligar isso a um servidor Express com SQLite. Ajuda-me a pensar primeiro na estrutura das tabelas (colunas, tipos, chaves) antes de escrevermos qualquer consulta. Se eu tiver uma query que não devolve o esperado, ajuda-me a lê-la parte a parte em vez de a reescreveres logo.

Conteúdos teóricos

5 temas

1. O que é uma base de dados?

Um sistema organizado para guardar, procurar e relacionar dados de forma permanente.

Até agora, todos os dados que criaste — arrays, objetos, variáveis — existiam só enquanto o programa estava a correr. Assim que o node ficheiro.js terminava, tudo desaparecia. Uma base de dados resolve isto: guarda dados num ficheiro (ou num serviço externo), de forma organizada, para que continuem a existir depois do programa terminar e possam ser partilhados por vários programas ao mesmo tempo.

Bases de dados relacionais

Existem vários tipos de base de dados; nesta plataforma vais focar-te nas relacionais, que organizam dados em tabelas — parecidas com folhas de cálculo, mas com regras mais rígidas e formas poderosas de as relacionar entre si. É o tipo mais usado no mundo, e a linguagem para as interrogar chama-se SQL (Structured Query Language).

Porquê SQLite nesta plataforma

Vamos usar o SQLite, uma base de dados que vive num único ficheiro (.db) no teu computador — sem precisares de instalar nem configurar um servidor de base de dados separado. É exatamente a mesma linguagem SQL que vais usar depois com bases de dados maiores (PostgreSQL, MySQL) em projetos reais; muda o motor por trás, não a forma como escreves as consultas.

Uma pequena parte do vocabulário, antes de avançar

  • Tabela — uma coleção de registos do mesmo tipo (ex: uma tabela utilizadores).
  • Linha (row) — um registo individual dentro da tabela (ex: um utilizador específico).
  • Coluna — um campo que toda a linha da tabela tem (ex: nome, email).
  • Consulta (query) — uma instrução escrita em SQL que pedes à base de dados para executar.

Vais ver estes quatro termos constantemente a partir daqui.

2. Estrutura: tabelas, colunas e chaves

Como desenhar a "forma" dos teus dados antes de guardares seja o que for.

Antes de guardares qualquer dado, tens de definir a estrutura da tabela — que colunas existem, e que tipo de dado cada uma aceita.

sql
CREATE TABLE utilizadores (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nome TEXT NOT NULL,
  email TEXT NOT NULL,
  idade INTEGER
);

Chave primária (PRIMARY KEY)

Toda a tabela deve ter uma coluna que identifica de forma única cada linha — a chave primária. Aqui é a coluna id, marcada como INTEGER PRIMARY KEY AUTOINCREMENT: um número inteiro que a própria base de dados gera automaticamente e aumenta a cada nova linha, garantindo que nunca há dois registos com o mesmo id.

Restrições comuns

  • NOT NULL — esta coluna é obrigatória; a base de dados recusa guardar uma linha sem valor aqui.
  • UNIQUE — não pode haver dois registos com o mesmo valor nesta coluna (ex: dois utilizadores com o mesmo email).
  • DEFAULT valor — se não fornecer um valor, usa este por defeito.

Chave estrangeira (FOREIGN KEY): relacionar tabelas

O nome "base de dados relacional" vem daqui: uma tabela pode referenciar uma linha de outra tabela através de uma chave estrangeira.

sql
CREATE TABLE tarefas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  utilizador_id INTEGER,
  FOREIGN KEY (utilizador_id) REFERENCES utilizadores (id)
);

A coluna utilizador_id em tarefas guarda o id de um registo em utilizadores — é assim que dizes "esta tarefa pertence a este utilizador", sem teres de copiar todos os dados do utilizador para dentro de cada tarefa. Este tipo de relação (uma tarefa pertence a um utilizador) chama-se um-para-muitos, e é o padrão mais comum em aplicações reais.

3. SQL de leitura: SELECT, WHERE, ORDER BY

A consulta que vais escrever mais vezes na vida: pedir dados de volta.

SELECT é o comando para ler dados de uma tabela:

sql
SELECT nome, email FROM utilizadores;

Isto devolve as colunas nome e email de todas as linhas da tabela utilizadores. Para pedir todas as colunas de uma vez, usa SELECT * FROM utilizadores; — o asterisco significa "todas".

Filtrar com WHERE

WHERE restringe quais as linhas devolvidas, com uma condição — a mesma ideia dos if que já conheces, mas aplicada a dados guardados:

sql
SELECT * FROM utilizadores WHERE idade >= 18;

SELECT * FROM utilizadores WHERE nome = 'Ana';

SELECT * FROM tarefas WHERE utilizador_id = 3 AND concluida = 0;

Repara que AND e OR funcionam em SQL tal como && e || funcionam em JavaScript — combinam várias condições da mesma forma lógica.

Ordenar com ORDER BY

ORDER BY decide a ordem das linhas devolvidas:

sql
SELECT * FROM utilizadores ORDER BY idade DESC;

DESC ordena do maior para o menor (descendente); ASC (o padrão, podes omitir) ordena do menor para o maior.

Limitar resultados com LIMIT

Quando só precisas das primeiras N linhas (ex: as 5 tarefas mais recentes):

sql
SELECT * FROM tarefas ORDER BY id DESC LIMIT 5;

Combinar WHERE, ORDER BY e LIMIT na mesma consulta é extremamente comum — é assim que a maioria dos ecrãs de listagem de uma aplicação real (produtos, mensagens, publicações) pede dados à base de dados.

4. SQL de escrita: INSERT, UPDATE, DELETE

Adicionar, alterar e remover dados — os outros três verbos além do SELECT.

INSERT: adicionar uma nova linha

sql
INSERT INTO utilizadores (nome, email, idade)
VALUES ('Marta', 'marta@email.com', 25);

Não precisas de indicar a coluna id — como definiste AUTOINCREMENT no tema 2, a base de dados atribui-o automaticamente.

UPDATE: alterar linhas existentes

sql
UPDATE utilizadores SET idade = 26 WHERE nome = 'Marta';

O WHERE aqui não é opcional na prática. Um UPDATE sem WHERE altera todas as linhas da tabela — é um dos erros mais caros que um programador pode cometer, e infelizmente já aconteceu a gente experiente em bases de dados de produção. O hábito certo: escreve sempre primeiro o SELECT equivalente com o mesmo WHERE, confirma que devolve exatamente as linhas que queres alterar, e só depois troca SELECT * por UPDATE ... SET ....

DELETE: remover linhas

sql
DELETE FROM utilizadores WHERE id = 7;

A mesma regra do UPDATE aplica-se, ainda com mais força: DELETE FROM utilizadores; sem WHERE apaga toda a tabela, sem aviso e sem confirmação. Testa sempre o WHERE com um SELECT primeiro.

Resumo dos quatro verbos (CRUD)

Estes quatro comandos formam o que se chama CRUD — Create, Read, Update, Delete — as quatro operações básicas que qualquer aplicação com dados persistentes precisa de suportar. Repara na correspondência direta com os métodos HTTP que viste na Fase 5: POST costuma disparar um INSERT, GET um SELECT, PUT/PATCH um UPDATE, DELETE um... DELETE.

5. Ligar SQLite a um servidor Express

Da consulta SQL isolada a uma API real que lê e escreve num ficheiro de base de dados.

Para uma aplicação web usar a tua base de dados, precisas de um servidor que receba pedidos HTTP (Fase 5) e responda executando consultas SQL. Em Node.js, a combinação mais comum para começar é o Express (para o servidor) com better-sqlite3 (para falar com o ficheiro .db).

javascript
const express = require('express');
const Database = require('better-sqlite3');

const app = express();
app.use(express.json());

const db = new Database('dados.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS tarefas (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titulo TEXT NOT NULL,
    concluida INTEGER DEFAULT 0
  )
`);

app.get('/tarefas', (req, res) => {
  const tarefas = db.prepare('SELECT * FROM tarefas').all();
  res.json(tarefas);
});

app.post('/tarefas', (req, res) => {
  const { titulo } = req.body;
  const resultado = db.prepare('INSERT INTO tarefas (titulo) VALUES (?)').run(titulo);
  res.status(201).json({ id: resultado.lastInsertRowid, titulo });
});

app.listen(3000, () => console.log('Servidor a correr na porta 3000'));

O ponto de interrogação: consultas parametrizadas

Reparaste no ? dentro de VALUES (?)? Isto chama-se consulta parametrizada — em vez de "colares" o valor diretamente na string SQL, passas-lhe o valor separadamente através de .run(valor). Isto não é opcional por estilo: colar valores vindos do utilizador diretamente numa string SQL abre a porta a um ataque chamado SQL injection, onde alguém malicioso escreve, no lugar de um nome, código SQL que apaga ou rouba dados. Consultas parametrizadas eliminam esse risco por completo — usa-as sempre que um valor vier de fora do teu código (de um formulário, de um pedido HTTP).

O ciclo completo

  1. O navegador (ou uma ferramenta como o Postman) envia um pedido HTTP.
  2. O Express recebe o pedido na rota correta (app.get, app.post, etc.).
  3. O código da rota executa uma consulta SQL na base de dados.
  4. O resultado é devolvido como resposta HTTP, normalmente em JSON.

Isto fecha o círculo entre tudo o que aprendeste desde a Fase 5: HTTP, rotas, e agora persistência real.

Exercícios práticos

4 exercícios
Fácil

Exercício Fácil — Desenhar e Criar uma Tabela de Livros

Desenha e cria, em SQL puro, a tabela de uma pequena biblioteca pessoal.

FerramentasVS CodeTerminalNode.jsbetter-sqlite3

Objetivo: praticar CREATE TABLE, tipos de coluna e a chave primária, sem ainda ligar nada a Node.js.

Passos a realizar

  1. Instala o Node.js com npm install better-sqlite3 numa pasta nova chamada biblioteca dentro de fase6.
  2. Cria criar-tabela.js que abre (ou cria) um ficheiro biblioteca.db.
  3. Escreve um CREATE TABLE IF NOT EXISTS para uma tabela livros, com colunas: id (chave primária autoincrementada), titulo (texto, obrigatório), autor (texto, obrigatório), ano (número), lido (número, com valor por defeito 0).
  4. Corre o ficheiro com node e confirma, sem erros, que o ficheiro biblioteca.db foi criado na pasta.
  5. Insere manualmente três livros com três instruções INSERT separadas, dentro do mesmo ficheiro.
  6. No fim, faz um SELECT * simples e imprime o resultado com console.log para confirmares que os três livros lá estão.

Dicas úteis

  • db.exec() corre SQL sem parâmetros (bom para CREATE TABLE); db.prepare(...).run() ou .all() é para consultas com dados a passar.
  • Se correres o ficheiro duas vezes, o IF NOT EXISTS evita um erro por a tabela já existir — mas os INSERTs vão duplicar os livros, o que é normal nesta fase de testes.

Erros comuns

  • Esquecer NOT NULL nas colunas obrigatórias, permitindo criar livros sem título.
  • Não guardar o resultado do npm install antes de tentar correr o ficheiro, causando um erro de módulo não encontrado.

As minhas dúvidas

Médio

Exercício Médio — Consultas com WHERE, ORDER BY e LIMIT

A partir da tabela de livros do exercício anterior, escreve seis consultas diferentes.

FerramentasVS CodeTerminalNode.jsbetter-sqlite3

Objetivo: ganhar fluência a combinar SELECT, WHERE, ORDER BY e LIMIT — a base de qualquer ecrã de listagem numa aplicação real.

Passos a realizar

  1. No mesmo projeto biblioteca, insere pelo menos 8 livros (com anos e estados de "lido" variados), para teres dados suficientes para filtrar.
  2. Cria consultas.js e escreve, cada uma numa secção separada com um console.log a identificar qual é:
    • Todos os livros ainda não lidos.
    • Todos os livros publicados depois de um certo ano, à tua escolha.
    • Os 3 livros mais recentes (maior ano), ordenados do mais recente para o mais antigo.
    • Todos os livros de um autor específico, ordenados alfabeticamente pelo título.
  3. Para cada consulta, imprime o número de resultados encontrados antes de imprimires os próprios livros.

Fluxo de funcionamento

  1. 1SELECT * FROM livros
  2. 2WHERE filtra linhas
  3. 3ORDER BY define a ordem
  4. 4LIMIT corta o tamanho

Dicas úteis

  • better-sqlite3 usa .all() para consultas que devolvem várias linhas, e .get() quando só esperas uma.
  • Testa cada consulta isoladamente antes de a combinares com outras condições — mais fácil de depurar uma peça de cada vez.

Erros comuns

  • Esquecer as aspas à volta de valores de texto dentro do WHERE (ex: WHERE autor = Tolkien em vez de WHERE autor = 'Tolkien').
  • Confundir ASC com DESC, obtendo a ordem invertida da pretendida.

As minhas dúvidas

Médio

Exercício Médio — API de Produtos com Express

Constrói um pequeno servidor Express com rotas GET e POST ligadas a uma tabela SQLite.

FerramentasVS CodeTerminalNode.jsExpressbetter-sqlite3

Objetivo: dar o salto de scripts isolados para uma API real que aceita pedidos HTTP e persiste dados.

Passos a realizar

  1. Numa pasta nova api-produtos dentro de fase6, corre npm install express better-sqlite3 cors.
  2. Cria servidor.js: inicializa o Express, ativa cors() e express.json(), e cria (se não existir) uma tabela produtos com id, nome, preco.
  3. Cria a rota GET /produtos que devolve todos os produtos em JSON.
  4. Cria a rota POST /produtos que lê nome e preco do corpo do pedido (req.body), valida que ambos existem e que preco é maior que 0, e insere um novo produto — devolvendo o produto criado com status 201.
  5. Se a validação falhar, devolve status 400 com uma mensagem de erro clara.
  6. Corre o servidor e testa as duas rotas com o navegador (para o GET) e com uma ferramenta como o Postman, o Insomnia ou o curl (para o POST).

Fluxo de funcionamento

  1. 1Pedido POST /produtos
  2. 2Express valida req.body
  3. 3INSERT parametrizado
  4. 4Resposta 201 com o produto

Código de exemplo

servidor.js
const express = require('express');
const cors = require('cors');
const Database = require('better-sqlite3');

const app = express();
app.use(cors());
app.use(express.json());

const db = new Database('produtos.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS produtos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nome TEXT NOT NULL,
    preco REAL NOT NULL
  )
`);

app.get('/produtos', (req, res) => {
  const produtos = db.prepare('SELECT * FROM produtos').all();
  res.json(produtos);
});

app.post('/produtos', (req, res) => {
  // completa aqui: validar req.body e inserir com consulta parametrizada
});

app.listen(3000, () => console.log('A correr em http://localhost:3000'));
Estrutura de partida

Dicas úteis

  • Nunca confies em dados vindos de req.body sem os validar primeiro — é a mesma disciplina do IMC da Fase 2, agora aplicada a dados de fora do teu controlo.
  • Usa sempre consultas parametrizadas (com ?) ao inserir dados vindos de req.body — nunca concatenes esses valores diretamente na string SQL.

Erros comuns

  • Esquecer app.use(express.json()), o que faz req.body chegar como undefined.
  • Devolver status 200 em vez de 201 quando algo é criado com sucesso — um pormenor pequeno, mas que segue a convenção HTTP correta.

As minhas dúvidas

Difícil

Exercício Difícil — App de Notas Persistente com SQLite

Constrói o CRUD completo (GET, POST, DELETE) para uma app de notas, com frontend simples a consumi-lo.

FerramentasVS CodeTerminalNode.jsExpressbetter-sqlite3

Este exercício junta a Fase 5 e a Fase 6: um pequeno frontend em HTML/JS que fala, via fetch, com uma API Express ligada a SQLite — o mesmo padrão usado em aplicações reais.

Passos a realizar

  1. Cria uma pasta app-notas dentro de fase6, com um subdiretório backend (servidor.js) e um subdiretório frontend (index.html, script.js).
  2. No backend, cria a tabela notas (id, titulo, conteudo, criado_em com DEFAULT CURRENT_TIMESTAMP).
  3. Implementa três rotas: GET /api/notas (todas as notas, ordenadas da mais recente para a mais antiga), POST /api/notas (cria uma nota nova), DELETE /api/notas/:id (remove uma nota pelo id, usando o parâmetro da rota).
  4. No frontend, usa fetch para: carregar e mostrar todas as notas ao abrir a página, adicionar uma nota nova através de um formulário, e remover uma nota ao clicar num botão "Apagar" junto a cada uma.
  5. Depois de qualquer criação ou remoção, volta a pedir a lista completa ao servidor (em vez de tentares atualizar o array local à mão) — mantém o frontend sempre sincronizado com a base de dados real.
  6. Testa o ciclo completo: adicionar três notas, apagar a do meio, e confirmar que as outras duas continuam lá depois de recarregares a página no navegador (prova de que os dados ficaram mesmo persistidos em disco).

Fluxo de funcionamento

  1. 1Frontend: fetch GET /api/notas
  2. 2Express + SQLite devolvem JSON
  3. 3JS desenha as notas no DOM
  4. 4Recarrega ao criar/apagar

Dicas úteis

  • req.params.id (não req.body) é onde vais encontrar o valor de :id numa rota como /api/notas/:id.
  • O teste do "recarregar a página e os dados continuarem lá" é o que prova que a persistência está mesmo a funcionar — sem ele, podes estar só a atualizar uma cópia em memória que desaparece ao reiniciar o servidor.

Erros comuns

  • Esquecer de recarregar a lista de notas do servidor depois de um POST ou DELETE, deixando o ecrã dessincronizado com a base de dados real.
  • Comparar req.params.id (que chega como texto) diretamente com um id numérico sem o converter, fazendo o DELETE falhar silenciosamente.

As minhas dúvidas