Conectando Node.js ao PostgreSQL com pg

1. Introdução ao pacote pg e configuração inicial

O pacote pg é o driver oficial para conectar aplicações Node.js ao banco de dados PostgreSQL. Ele é amplamente utilizado por sua performance, suporte a conexões poolizadas e compatibilidade com os padrões mais recentes do PostgreSQL. Para projetos que utilizam React no frontend e Node.js no backend, o pg oferece uma base sólida para construir APIs REST escaláveis.

Para instalar o pacote, utilize o npm:

npm install pg

A configuração pode ser feita de duas formas principais: usando uma Connection String ou um objeto de configuração. A Connection String é mais compacta, enquanto o objeto oferece maior clareza:

// Usando Connection String
const { Pool } = require('pg');
const pool = new Pool({
  connectionString: 'postgresql://usuario:senha@localhost:5432/meubanco'
});

// Usando objeto de configuração
const pool = new Pool({
  user: 'usuario',
  host: 'localhost',
  database: 'meubanco',
  password: 'senha',
  port: 5432,
});

2. Estabelecendo a conexão com o PostgreSQL

O pg oferece duas classes principais para conexão: Client e Pool. O Client estabelece uma conexão única e é ideal para scripts simples ou tarefas pontuais. Já o Pool gerencia um conjunto de conexões reutilizáveis, sendo a escolha recomendada para aplicações web que precisam lidar com múltiplas requisições simultâneas.

Exemplo de conexão com Pool:

const { Pool } = require('pg');

const pool = new Pool({
  host: 'localhost',
  database: 'minhaloja',
  user: 'admin',
  password: 'admin123',
  port: 5432,
  max: 20, // máximo de conexões no pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

pool.on('error', (err, client) => {
  console.error('Erro inesperado no pool:', err);
  process.exit(-1);
});

// Testando a conexão
async function testarConexao() {
  try {
    const resultado = await pool.query('SELECT NOW()');
    console.log('Conectado ao PostgreSQL em:', resultado.rows[0].now);
  } catch (erro) {
    console.error('Falha na conexão:', erro);
  }
}

testarConexao();

3. Executando consultas SQL com pg

O método query() é o coração das operações com o pg. Ele retorna um objeto result contendo rows (linhas retornadas), rowCount (número de linhas afetadas) e fields (metadados das colunas).

Para evitar SQL injection, utilize placeholders numerados ($1, $2, ...):

// SELECT com parâmetros
async function buscarUsuarioPorEmail(email) {
  const resultado = await pool.query(
    'SELECT * FROM usuarios WHERE email = $1',
    [email]
  );
  return resultado.rows[0];
}

// INSERT
async function criarUsuario(nome, email, idade) {
  const resultado = await pool.query(
    'INSERT INTO usuarios (nome, email, idade) VALUES ($1, $2, $3) RETURNING *',
    [nome, email, idade]
  );
  return resultado.rows[0];
}

// UPDATE
async function atualizarUsuario(id, nome) {
  const resultado = await pool.query(
    'UPDATE usuarios SET nome = $1 WHERE id = $2 RETURNING *',
    [nome, id]
  );
  return resultado.rows[0];
}

// DELETE
async function deletarUsuario(id) {
  const resultado = await pool.query(
    'DELETE FROM usuarios WHERE id = $1 RETURNING *',
    [id]
  );
  return resultado.rows[0];
}

4. Gerenciamento de transações

Transações são essenciais para operações que exigem atomicidade. Com o pg, você controla manualmente o início, commit e rollback:

async function transferenciaBancaria(origemId, destinoId, valor) {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');

    // Deduzir da conta de origem
    await client.query(
      'UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1',
      [valor, origemId]
    );

    // Adicionar à conta de destino
    await client.query(
      'UPDATE contas SET saldo = saldo + $1 WHERE id = $2',
      [valor, destinoId]
    );

    await client.query('COMMIT');
    console.log('Transferência realizada com sucesso!');
  } catch (erro) {
    await client.query('ROLLBACK');
    console.error('Transferência falhou, transação revertida:', erro);
    throw erro;
  } finally {
    client.release();
  }
}

5. Integração com Express.js (API REST)

A combinação de Express.js com pg permite criar APIs REST completas. Veja um exemplo de configuração:

const express = require('express');
const { Pool } = require('pg');

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

const pool = new Pool({ /* configuração */ });

// Rota GET para listar usuários
app.get('/api/usuarios', async (req, res) => {
  try {
    const resultado = await pool.query('SELECT * FROM usuarios ORDER BY id');
    res.json(resultado.rows);
  } catch (erro) {
    res.status(500).json({ erro: 'Erro ao buscar usuários' });
  }
});

// Rota POST para criar usuário
app.post('/api/usuarios', async (req, res) => {
  const { nome, email, idade } = req.body;

  if (!nome || !email) {
    return res.status(400).json({ erro: 'Nome e email são obrigatórios' });
  }

  try {
    const resultado = await pool.query(
      'INSERT INTO usuarios (nome, email, idade) VALUES ($1, $2, $3) RETURNING *',
      [nome, email, idade]
    );
    res.status(201).json(resultado.rows[0]);
  } catch (erro) {
    if (erro.code === '23505') { // violação de unique
      res.status(409).json({ erro: 'Email já cadastrado' });
    } else {
      res.status(500).json({ erro: 'Erro ao criar usuário' });
    }
  }
});

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

6. Boas práticas e segurança

Variáveis de ambiente com dotenv

// Instalação: npm install dotenv
require('dotenv').config();

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});

Encerramento gracioso do pool

process.on('SIGINT', async () => {
  console.log('Encerrando conexões...');
  await pool.end();
  process.exit(0);
});

process.on('SIGTERM', async () => {
  console.log('Sinal de término recebido...');
  await pool.end();
  process.exit(0);
});

7. Exemplo completo: API de usuários com React no frontend

Backend (Node.js + Express + pg)

const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');

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

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

// Rotas CRUD
app.get('/api/usuarios', async (req, res) => {
  const result = await pool.query('SELECT * FROM usuarios');
  res.json(result.rows);
});

app.post('/api/usuarios', async (req, res) => {
  const { nome, email } = req.body;
  const result = await pool.query(
    'INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING *',
    [nome, email]
  );
  res.status(201).json(result.rows[0]);
});

app.listen(3001, () => console.log('Backend rodando na porta 3001'));

Frontend (React)

import React, { useState, useEffect } from 'react';

function App() {
  const [usuarios, setUsuarios] = useState([]);
  const [nome, setNome] = useState('');
  const [email, setEmail] = useState('');

  useEffect(() => {
    fetch('http://localhost:3001/api/usuarios')
      .then(res => res.json())
      .then(data => setUsuarios(data));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const response = await fetch('http://localhost:3001/api/usuarios', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ nome, email })
    });
    const novoUsuario = await response.json();
    setUsuarios([...usuarios, novoUsuario]);
    setNome('');
    setEmail('');
  };

  return (
    <div>
      <h1>Usuários</h1>
      <form onSubmit={handleSubmit}>
        <input value={nome} onChange={e => setNome(e.target.value)} placeholder="Nome" />
        <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
        <button type="submit">Adicionar</button>
      </form>
      <ul>
        {usuarios.map(user => (
          <li key={user.id}>{user.nome} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Referências