Hospedar site com banco de dados: como funciona

O navegador nunca acessa o banco direto. Um back-end (API ou servidor web) recebe a requisição, consulta o banco com credenciais guardadas em variável de ambiente e devolve a resposta.

Atualizado em 5 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Entender o caminho de uma requisição entre navegador, back-end e banco
  • Montar um back-end Node.js que lê e grava no PostgreSQL com consultas parametrizadas
  • Configurar a conexão por variável de ambiente e verificar que funciona
Nesta página

Quase todo site que guarda algo (usuários, pedidos, comentários, posts) tem um banco de dados atrás. A dúvida de quem começa é onde o banco "se encaixa": ele não faz parte da página, e o navegador não consegue (nem deve) falar com ele.

Entre os dois existe um back-end: um programa que roda num servidor, recebe requisições HTTP, consulta o banco e devolve o resultado. Este guia mostra esse desenho e um exemplo funcional com Node.js e PostgreSQL.

NavegadorGET / POSTJSON / HTMLBack-endDATABASE_URLvariável deambientepoolconsulta parametrizadaBanco dedadossem acessodireto
O navegador fala só com o back-end; ele guarda a senha no ambiente e consulta o banco por uma conexão do pool.

Como um site conversa com o banco de dados?#

O caminho de uma requisição tem três pontos:

  1. Navegador: pede uma página ou chama uma rota, por exemplo GET /api/posts.
  2. Back-end: valida o pedido, pega uma conexão do pool e executa uma consulta SQL (ou o equivalente no MongoDB).
  3. Banco de dados: devolve as linhas, o back-end monta o JSON ou o HTML e responde.

O navegador só enxerga o resultado final. A senha do banco existe apenas no servidor, em uma variável de ambiente. Se ela estivesse no JavaScript da página, qualquer visitante a leria nas ferramentas do desenvolvedor.

Um site estático pode usar banco de dados?#

Um site estático é só arquivos (HTML, CSS, JS) servidos como estão. Ele não executa código no servidor, então não consulta banco. Para ter dados dinâmicos, ele chama uma API em outro lugar, que faz a consulta.

Frameworks que renderizam no servidor (como o Next.js) juntam as duas partes num projeto só. A diferença entre renderizar no build ou a cada requisição está em SSR, SSG e site estático. Em qualquer caso, quem fala com o banco é código que roda no servidor.

O que preciso antes de começar?#

  • Uma versão recente do Node.js, compatível com o Express 5, e o npm.
  • Um banco PostgreSQL acessível, local ou hospedado, e a string de conexão no formato postgresql://usuario:senha@host:5432/banco. Veja Como hospedar um banco de dados se ainda não tem um.
  • Uma tabela simples para testar:
sql
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Como escrever o back-end com Node.js e PostgreSQL?#

Instale as dependências:

bash
npm install express pg

Crie o index.js. O arquivo usa ES modules, então adicione "type": "module" ao package.json:

js
import express from "express";
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});
// sem este listener, um erro em uma conexão ociosa derruba o processo
pool.on("error", (err) => console.error("erro no pool do PostgreSQL", err));

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

app.get("/api/posts", async (req, res) => {
  const { rows } = await pool.query(
    "SELECT id, title, created_at FROM posts ORDER BY id DESC LIMIT 20"
  );
  res.json(rows);
});

app.post("/api/posts", async (req, res) => {
  const { title } = req.body ?? {};
  if (typeof title !== "string" || title.trim() === "") {
    return res.status(400).json({ error: "title é obrigatório" });
  }
  const { rows } = await pool.query(
    "INSERT INTO posts (title) VALUES ($1) RETURNING id, title, created_at",
    [title.trim()]
  );
  res.status(201).json(rows[0]);
});

app.listen(process.env.PORT || 3000);

Três pontos importam aqui:

  • Pool: mantém conexões abertas e as reaproveita. Abrir uma conexão nova por requisição é lento e pode esgotar o limite de conexões do banco. A documentação do pg diz que normalmente basta um único pool por aplicação. O pool.on("error") trata erros de conexões ociosas, como o banco reiniciando; sem ele, o processo cai.
  • $1 e o array: é uma consulta parametrizada. O valor viaja separado do SQL, então texto malicioso não vira comando. É o que a OWASP lista como a primeira defesa contra SQL injection.
  • async sem try/catch: no Express 5, um erro lançado ou uma promise rejeitada em um handler é encaminhado ao tratador de erros automaticamente. No Express 4 era preciso chamar next(err) à mão.

Nunca monte o SQL concatenando entrada do usuário, como "... WHERE title = '" + title + "'".

Como configurar as credenciais com segurança?#

Defina DATABASE_URL no ambiente, nunca no código nem no repositório:

bash
export DATABASE_URL="postgresql://usuario:senha@localhost:5432/meubanco"
node index.js

Se a senha tiver símbolos com significado na URL, como @, /, # ou %, aplique percent-encoding (p@ss vira p%40ss, pa/ss vira pa%2Fss). Em desenvolvimento, um arquivo .env fora do Git resolve; em produção, use o painel de variáveis de ambiente do seu provedor. Mais detalhes em O que são variáveis de ambiente.

Como verificar que funcionou?#

Com o servidor rodando, grave e leia:

bash
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Primeiro post"}'

curl http://localhost:3000/api/posts

A primeira resposta traz o registro criado, com id e created_at. A segunda lista os posts. Para o site usar isso, o front-end servido pelo mesmo back-end chama fetch("/api/posts") e renderiza o JSON.

Quais erros são comuns?#

  • connect ECONNREFUSED: nada aceitou a conexão naquele host e porta. O banco está desligado ou o host ou a porta estão errados. Um firewall que descarta pacotes costuma dar timeout em vez disso.
  • password authentication failed for user "...": usuário ou senha errados.
  • Invalid URL: a string de conexão está malformada, muitas vezes por um símbolo da senha sem percent-encoding.
  • sorry, too many clients already: o banco atingiu o limite de conexões. Use um único Pool compartilhado, criado uma vez no início do programa. Veja mais em too many connections.
  • Banco e site em lugares distantes: cada consulta paga a latência da rede, e uma página com várias consultas fica lenta. Mantenha os dois no mesmo datacenter.
  • Senha no front-end ou no repositório: gere uma nova senha no banco e mova-a para variável de ambiente.

Como fazer isso na Square Cloud#

Na Square Cloud, o back-end e o banco ficam na mesma conta. Você faz o deploy do site (Node.js, Python, PHP, Go e outros runtimes) e cria pelo painel um banco PostgreSQL, MySQL, MongoDB ou Redis, com latência média de cerca de 1 ms entre aplicação e banco, porque ficam no mesmo datacenter. O painel entrega a URL de conexão completa e o certificado.

O TLS é obrigatório: o cliente precisa confiar na CA baixada e, no PostgreSQL, apresentar também o .pem baixado, então passe esses arquivos na opção ssl do pg.Pool. Cada banco aceita até 512 conexões, sem pooler na frente, por isso mantenha o max do pool baixo. A hospedagem de bancos começa no plano Standard; o Hobby não inclui bancos.

Coloque a URL de conexão como variável de ambiente DATABASE_URL da aplicação, pelo painel, API ou CLI. A mudança de variável vale no próximo restart, sem rebuild. Toda aplicação web recebe um subdomínio *.squareweb.app com HTTPS, e o domínio personalizado está disponível a partir do Standard. Aplicações web usam no mínimo 512 MB de RAM.

Veja os planos e como publicar seu site em Hospedagem de sites na Square Cloud.

Hospedagem de sites

Sites estáticos e SSR com TLS automático e CDN global

Saiba mais

Perguntas frequentes

Um site estático pode usar banco de dados?

O HTML estático em si não consulta o banco. Para ler ou gravar dados, ele precisa chamar um back-end (uma API) que faz a consulta. O JavaScript do navegador nunca deve conter a senha do banco.

Posso conectar o JavaScript do navegador direto ao banco?

Não. Qualquer coisa enviada ao navegador é pública, inclusive a string de conexão. Quem se conecta ao banco é sempre o servidor.

Preciso de um ORM?

Não. Um driver como o pg, com consultas parametrizadas, já resolve. ORMs ajudam em esquemas grandes e migrações, mas são opcionais.

Como evito SQL injection?

Use consultas parametrizadas, com marcadores como $1, em vez de concatenar texto do usuário no SQL. A OWASP lista prepared statements como a primeira defesa.

Próximo artigoErro 404 ao recarregar uma SPA React/Vue depois do deployUma SPA React ou Vue dá 404 ao recarregar porque o servidor procura um arquivo na URL. Resolva com fallback para o index.html no nginx, Express, Apache ou Netlify.