CodeOath
← All posts
Node.js36 min total · 14 parts

Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs

Part 12 of 14 · ~2 min

Connecting to a Database

Why connection pooling, not one connection per request

Opening a database connection isn't free — it means a TCP handshake, often a TLS handshake on top of that, and an authentication round trip with the database server, all before a single query runs. Do that fresh for every incoming HTTP request, and you're paying that entire setup cost — typically tens of milliseconds — on top of every query's actual execution time, and you'll exhaust the database's own maximum-connections limit under any real concurrent load, since each one stays open only for the lifetime of a single request.

A connection pool solves this by opening a fixed, modest number of connections once, up front, and handing them out to whichever request needs one, returning each connection to the pool when the query finishes rather than closing it:

const { Pool } = require("pg"); // pg-style pool — the same shape appears across most Node DB drivers

const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20, // maximum simultaneous connections the pool will open
});

app.get("/orders/:id", async (req, res, next) => {
  try {
    // pool.query() checks out a connection, runs the query, and returns
    // the connection to the pool automatically when it's done
    const result = await pool.query("SELECT * FROM orders WHERE id = $1", [req.params.id]);
    if (result.rows.length === 0) return res.status(404).json({ error: "Order not found" });
    res.json(result.rows[0]);
  } catch (err) {
    next(err);
  }
});

The pool's max size is a genuine trade-off, not a "bigger is always better" dial: too small, and requests queue up waiting for a free connection under load even though your application server has spare capacity; too large, and you risk the database's own connection ceiling — which is often surprisingly low, and shared across every application instance connecting to it, not just this one process.

Where an ORM fits in

An ORM (object-relational mapper — Prisma, Sequelize, TypeORM, and others in the Node ecosystem) sits on top of exactly this same pooling mechanism, and lets you describe queries and schema changes in terms of your application's objects and models rather than hand-written SQL strings — migrations, relationship loading, and query building all become more declarative. The trade you're making is real, though: you give up some of the fine-grained control and predictability of the exact SQL that gets executed, in exchange for meaningfully less boilerplate and faster iteration on the common cases. Neither choice is universally correct — it's a call about how much you value raw query control versus development speed for a given project — but it's worth recognizing an ORM is still a client using a connection pool underneath, not a different transport mechanism.