Martin Lefebvre's blog

The Singleton

Handling MySQL Connections in TypeScript: The Good, the Bad, and the “Too Many Connections” Error

When building a TypeScript application that talks to MySQL, how you manage database connections matters more than it might initially appear. A seemingly harmless pattern can quietly work its way into production and, under load, bring your application down with the dreaded:

Error: Too many connections

In this post, we will look at a bad but common approach to handling MySQL connections, why it causes problems, and then contrast it with a simple, effective approach that leverages ES modules and the Singleton pattern—without classes.


The Bad Way: Creating a New Connection Per Request

A very common mistake is to create a new MySQL connection every time the database is accessed, without checking whether a connection already exists.

At first glance, this approach looks straightforward and works fine during development or low traffic. The issue only appears once your application sees real usage.

Example: A New Connection Every Time

// db.ts
import mysql from "mysql2/promise";

export async function getConnection() {
  return mysql.createConnection({
    host: "localhost",
    user: "app_user",
    password: "secret",
    database: "app_db",
  });
}
// userRepository.ts
import { getConnection } from "./db";

export async function getUsers() {
  const connection = await getConnection();
  const [rows] = await connection.query("SELECT * FROM users");
  return rows;
}

Why This Is a Problem

Each call to getConnection() opens a brand new TCP connection to MySQL. In a real application:

MySQL has a finite number of allowed connections, and once they are used up, new queries will fail until existing connections are released.

This approach scales poorly and fails in exactly the situations where you need reliability the most.


The Good Way: A Singleton Connection via ES Modules

A better approach is to open the connection once and reuse it.

In Node.js, ES modules are cached after their first import. This gives us a natural and elegant way to implement a Singleton—without classes, frameworks, or complexity.

Example: A Singleton MySQL Connection

// db.ts
import mysql from "mysql2/promise";

let connection: mysql.Connection | null = null;

export async function getConnection() {
  if (!connection) {
    connection = await mysql.createConnection({
      host: "localhost",
      user: "app_user",
      password: "secret",
      database: "app_db",
    });
  }

  return connection;
}
// userRepository.ts
import { getConnection } from "./db";

export async function getUsers() {
  const connection = await getConnection();
  const [rows] = await connection.query("SELECT * FROM users");
  return rows;
}

Why This Works

This pattern dramatically reduces connection churn and is suitable for many small-to-medium applications.


A Quick Note on Connection Pools

In higher-throughput systems, you will typically want a connection pool rather than a single connection. The same Singleton principle applies: create the pool once and reuse it everywhere.

The key takeaway remains the same:

Never open a new MySQL connection for every request without a clear lifecycle strategy.


Final Thoughts

Connection management is one of those details that feels minor—until it isn’t. The “bad” approach often slips into codebases because it is easy and works early on. The “good” approach takes only a few extra lines of code and saves you from production outages later.

By leveraging ES module caching and a simple Singleton pattern, you can keep your TypeScript applications efficient, stable, and friendly to your MySQL server.

If you are seeing “too many connections” errors, this is one of the first places worth revisiting.

Tags: