How to Connect to MySQL from Node.js
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
| Client | Package | Status | Version (as of June 2026) |
|---|---|---|---|
| mysql2 | mysql2 | Actively maintained, the default choice | 3.22.5 |
| node-mysql (legacy) | mysql | Last release Jan 2020 | 2.18.1 |
| MariaDB connector | mariadb | Maintained by MariaDB Corp, also speaks MySQL protocol | 3.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 mysql2Import 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
| Error | Cause | Fix |
|---|---|---|
ECONNREFUSED | Nothing listening at host:port | Check host/port, server running, firewall |
ER_ACCESS_DENIED_ERROR | Wrong user/password, or user not allowed from this host | Check credentials; MySQL accounts are user@host pairs |
ER_NOT_SUPPORTED_AUTH_MODE | Legacy mysql package vs MySQL 8+ auth | Use mysql2 |
ER_BAD_DB_ERROR | Database doesn't exist | Check database name |
ETIMEDOUT | Host unreachable (security group, VPC, wrong host) | Network path, not credentials |
Connection lost | Server closed an idle connection | Use 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.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.