How to Connect to MySQL from Node.js

7 min readMySQL

For new Node.js projects there is one answer: mysql2. The package it succeeded, mysql (felixge's node-mysql), hasn't shipped a release since January 2020 and cannot authenticate against MySQL 8's default auth plugin without a workaround. This guide shows working mysql2 code for connections, pools, and prepared statements, then covers the configuration and errors that actually trip people up.

The options

ClientPackageStatusVersion (as of June 2026)
mysql2mysql2Actively maintained, the default choice3.22.5
node-mysql (legacy)mysqlLast release Jan 20202.18.1
MariaDB connectormariadbMaintained by MariaDB Corp, also speaks MySQL protocol3.5.3

mysql2 is API-compatible with the legacy mysql package (it started as a drop-in replacement), adds a native Promise API, prepared statements, and support for MySQL 8+ authentication. Every major ORM and query builder (Prisma, Drizzle, Knex, Sequelize, TypeORM) uses or recommends it.

The mariadb connector is a reasonable pick if your server is actually MariaDB -- it supports MariaDB-specific features like INSERT ... RETURNING. Against a MySQL server, stick with mysql2.

Basic connection

npm install mysql2

Import from mysql2/promise for async/await. The callback API (require("mysql2")) exists for legacy compatibility, but there's no reason to use it in new code:

import mysql from "mysql2/promise";
 
const connection = await mysql.createConnection({
  host: "db.example.com",
  port: 3306,
  user: "app_user",
  password: "secret",
  database: "sales",
});
 
const [rows] = await connection.query(
  "SELECT id, name FROM customers WHERE region = ?",
  ["EMEA"]
);
console.log(rows);
 
await connection.end();

Two things to notice:

  • Placeholders are ?, positional. Never interpolate values into the SQL string.
  • Results come back as [rows, fields] -- a two-element array you destructure. Forgetting the destructure and treating the whole array as rows is a classic first-hour bug.

A connection URI works too: mysql.createConnection("mysql://app_user:secret@db.example.com:3306/sales").

Use a pool for anything serving requests

A single connection processes one query at a time and dies permanently if the server drops it. A pool manages multiple connections and replaces broken ones:

import mysql from "mysql2/promise";
 
const pool = mysql.createPool({
  host: "db.example.com",
  user: "app_user",
  password: "secret",
  database: "sales",
  connectionLimit: 10,     // default: 10
  maxIdle: 10,             // default: same as connectionLimit
  idleTimeout: 60000,      // default: idle connections closed after 60s
  queueLimit: 0,           // default: 0 = unlimited queued requests
});
 
const [rows] = await pool.query("SELECT * FROM orders WHERE status = ?", ["open"]);

pool.query() grabs a connection, runs the query, and releases it automatically. The defaults above were verified against mysql2 3.22.5.

Size connectionLimit against the server's max_connections (default 151 on MySQL 8), remembering that every worker process gets its own pool. Four Node processes with connectionLimit: 50 can exhaust a default server.

Transactions need a dedicated connection

pool.query() may run consecutive statements on different connections, which silently breaks transactions. Check a connection out explicitly:

const conn = await pool.getConnection();
try {
  await conn.beginTransaction();
  await conn.query("UPDATE accounts SET balance = balance - ? WHERE id = ?", [100, 1]);
  await conn.query("UPDATE accounts SET balance = balance + ? WHERE id = ?", [100, 2]);
  await conn.commit();
} catch (err) {
  await conn.rollback();
  throw err;
} finally {
  conn.release();   // forget this and the pool eventually deadlocks
}

query() vs execute()

mysql2 has two query methods. query() does client-side escaping and sends plain SQL text. execute() uses true server-side prepared statements: the statement is compiled once, parameters travel separately from the SQL, and mysql2 caches the prepared statement per connection:

// Prepared statement -- compiled once per connection, parameters sent separately
const [rows] = await pool.execute(
  "SELECT * FROM orders WHERE customer_id = ? AND status = ?",
  [42, "open"]
);

Use execute() for hot-path queries that run repeatedly. Two caveats: prepared statements can't parameterize identifiers (table or column names), and some statements (USE, certain DDL) can't be prepared at all -- use query() for those.

Named placeholders are available behind a flag, with :name syntax:

const pool = mysql.createPool({ /* ... */, namedPlaceholders: true });
const [rows] = await pool.execute(
  "SELECT * FROM orders WHERE customer_id = :id AND status = :status",
  { id: 42, status: "open" }
);

The auth error everyone hits: ER_NOT_SUPPORTED_AUTH_MODE

MySQL 8 changed the default authentication plugin from mysql_native_password to caching_sha2_password. The legacy mysql package never gained support for it, so connecting to a stock MySQL 8+ server fails with:

ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol
requested by server; consider upgrading MySQL client

The fix is to use mysql2, which supports caching_sha2_password natively (along with mysql_native_password and sha256_password). If you see this error with mysql2, the user was likely created with a plugin the server can't negotiate over an insecure channel -- caching_sha2_password requires either TLS or an RSA key exchange for the initial handshake, both of which mysql2 handles automatically against a normally configured server.

The wrong fix, still circulating in old Stack Overflow answers, is downgrading the user with ALTER USER ... IDENTIFIED WITH mysql_native_password. MySQL 9 removed mysql_native_password entirely, so that workaround now has an expiry date attached.

Other gotchas

"Connection lost: The server closed the connection." MySQL drops connections idle longer than wait_timeout (default 8 hours, often minutes on managed platforms). Single long-lived connections will eventually hit this; pools replace dead connections on checkout, which is the main practical reason to use one even at low traffic.

Charset and emoji. Modern mysql2 defaults to utf8mb4, which handles all of Unicode. But if your tables were created with legacy utf8 (a 3-byte-max encoding), inserting an emoji fails with Incorrect string value. Fix the table: ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4.

BIGINT precision. JavaScript numbers lose precision past 2^53. mysql2 returns BIGINT columns as JS numbers by default; for ID columns beyond that range, set supportBigNumbers: true and bigNumberStrings: true to get strings back instead of silently wrong numbers.

Dates and timezones. mysql2 converts DATETIME/TIMESTAMP to JS Date objects using the connection's timezone setting (default: local). For servers storing UTC, set timezone: "Z" -- or dateStrings: true to get raw strings and side-step Date entirely.

SSL. Off by default. For managed databases (PlanetScale, RDS, Cloud SQL over public IP), enable it: ssl: { rejectUnauthorized: true } with the provider's CA if needed. Don't ship rejectUnauthorized: false -- it disables certificate verification, leaving the connection encrypted but unauthenticated.

Error reference

ErrorCauseFix
ECONNREFUSEDNothing listening at host:portCheck host/port, server running, firewall
ER_ACCESS_DENIED_ERRORWrong user/password, or user not allowed from this hostCheck credentials; MySQL accounts are user@host pairs
ER_NOT_SUPPORTED_AUTH_MODELegacy mysql package vs MySQL 8+ authUse mysql2
ER_BAD_DB_ERRORDatabase doesn't existCheck database name
ETIMEDOUTHost unreachable (security group, VPC, wrong host)Network path, not credentials
Connection lostServer closed an idle connectionUse a pool

Connecting with a GUI instead

For exploring data, checking schemas, or debugging the queries you're about to embed in code, a client beats console.log. Mako connects to MySQL with AI-assisted SQL autocomplete; the same guide compares MySQL Workbench, DBeaver, TablePlus, and the rest of the field honestly.

For the SQL side of things once you're connected, see our MySQL guides on joins, transactions, and upserts. For loading data, see importing CSV to MySQL.


Mako connects to MySQL (and 8 other databases) with AI-powered autocomplete. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.