mysql-next
v0.2.2
Published
🚀 A fast, feature-rich MySQL client for Node.js
Maintainers
Readme
- Fast full-featured MySQL client for Node.js
- 🏷️ ES6 Tagged Template
- 🎯 Simple surface API
- 🧩 Dynamic query support
- 🔌 TCP, pooling, TLS
- 🧠 Typed results
Installation
npm install mysql-nextQuick Start
// db.js
import mysql from 'mysql-next'
const sql = mysql('mysql://root:password@localhost:3306/app', {
max: 10,
idleTimeout: 30_000,
acquireTimeout: 30_000,
prepare: true,
maxStatements: 100
})
export default sql// users.js
import sql from './db.js'
export async function getUsersOver(age) {
const users = await sql`
select
id,
name,
age
from users
where age > ${age}
`
return users
}
export async function insertUser({ name, age }) {
const result = await sql`
insert into users ${sql({ name, age })}
`
return result.lastInsertId
}Connection
mysql([url], [options])
You can use either a mysql:// url connection string or an options object to define your connection properties. When both are provided, explicit options override values from the url.
const sql = mysql('mysql://username:password@host:3306/database', {
host: '127.0.0.1',
port: 3306,
socketPath: undefined,
user: 'root',
password: 'secret',
database: 'app',
ssl: false,
connectTimeout: 10_000,
max: 10,
idleTimeout: 30_000,
acquireTimeout: 30_000,
prepare: true,
maxStatements: 100
})Queries
await sql\...``
mysql-next uses tagged templates and sends interpolated values as bound parameters.
const users = await sql`
select
id,
name
from users
where age > ${60}
`const name = 'Mur'
const age = 60
const users = await sql`
select
name,
age
from users
where name like ${name + '%'}
and age > ${age}
`Do not wrap interpolated values in quotes like '${name}'. The driver already turns them into bound parameters.
Building queries
Insert and update helpers
const user = {
name: 'Murray',
age: 68
}
await sql`insert into users ${sql(user)}`
// insert into users (`name`, `age`) values (?, ?)
await sql`
update users
set ${sql({ name: user.name, age: user.age }, 'update')}
where id = ${user.id}
`
// update users set `name` = ?, `age` = ? where id = ?Lists and partial fragments
const ids = [1, 2, 3]
const users = await sql`
select
*
from users
where id in ${sql(ids)}
`
const olderThan = (x) => sql.fragment`and age > ${x}`
const filterAge = true
await sql`
select
*
from users
where name is not null
${filterAge ? olderThan(50) : sql.fragment``}
`SQL functions and raw fragments
const updatedAt = null
await sql`
update users
set updated_at = ${updatedAt || sql.unsafe('now()')}
`Use sql.unsafe() only when the SQL text is fully trusted.
Prefer bound parameters whenever possible. sql.unsafe() and low-level helpers such as escapeValue() are not appropriate for untrusted user input.
Table and column names
Dynamic identifiers are supported with sql.identifier().
const table = sql.identifier('users')
const column = sql.identifier('users.created_at')
await sql`
select ${column}
from ${table}
`Transactions
await sql.begin(fn)
Use sql.begin to start a transaction. A single connection is reserved for the callback and automatically committed or rolled back.
const user = await sql.begin(async (sql) => {
const result = await sql`
insert into users ${sql({ name: 'Murray', age: 68 })}
`
const users = await sql`
select *
from users
where id = ${result.lastInsertId}
`
return users[0]
})If anything throws, ROLLBACK is issued automatically.
Manual transactions
const tx = await sql.begin()
try {
await tx`insert into users ${tx({ name: 'Murray', age: 68 })}`
await tx.commit()
} catch (error) {
await tx.rollback()
throw error
}Savepoints
await sql.begin(async (sql) => {
await sql.savepoint('create_profile', async () => {
await sql`insert into profiles ${sql({ user_id: 1 })}`
})
})Result Array
SELECT queries return arrays with metadata attached.
const rows = await sql`
select id, name
from users
`
rows.length
rows.count
rows.columns
rows[0]?.idMutating queries return command metadata.
const result = await sql`
insert into users ${sql({ name: 'Murray' })}
`
result.command
result.count
result.affectedRows
result.lastInsertIdConnection details
Options
const sql = mysql('mysql://username:password@host:3306/database', {
host: '127.0.0.1',
port: 3306,
socketPath: '/var/run/mysqld/mysqld.sock',
database: 'app',
user: 'root',
password: 'secret',
ssl: false,
connectTimeout: 10_000,
max: 10,
idleTimeout: 30_000,
acquireTimeout: 30_000,
prepare: true,
maxStatements: 100,
resetOnRelease: false,
types: {
decimal: (raw) => raw,
newdecimal: (raw) => raw
}
})| Option | Default | Description |
| --- | --- | --- |
| resetOnRelease | false | Sends COM_RESET_CONNECTION on every pool release, guaranteeing clean session state per borrow at the cost of one extra round trip (see Benchmarks). |
Dynamic passwords
If your password needs to be resolved at connection time, pass a function.
const sql = mysql('mysql://root@localhost:3306/app', {
password: async () => process.env.DB_TOKEN ?? ''
})SSL / TLS
mysql-next can upgrade the connection to TLS during the MySQL handshake.
const sql = mysql({
host: 'db.example.com',
user: 'app',
password: 'secret',
database: 'main',
ssl: true
})Custom TLS options are also supported.
const sql = mysql('mysql://app:[email protected]:3306/main', {
ssl: {
rejectUnauthorized: false
}
})Avoid rejectUnauthorized: false outside local development. It disables server certificate verification and weakens TLS protection.
Authentication plugins
mysql-next currently supports these MySQL authentication plugins:
mysql_native_passwordcaching_sha2_password
Other authentication plugins are rejected explicitly.
The connection pool
Connections are created lazily. Instantiating mysql() does not open any connection by itself.
const sql = mysql() // no connection yet
await sql`select 1` // first connection is opened
await sql`select 1` // pooled connection reused
await Promise.all([sql`select 1`, sql`select 1`])max controls the upper bound of concurrently active pooled connections.
By default, connections returned to the pool are handed back without a server round trip, keeping prepared statement caches warm across borrows (see Benchmarks). Set resetOnRelease: true to send COM_RESET_CONNECTION on every release instead, guaranteeing that session state never leaks between consumers at the cost of one extra round trip per release.
Prepared statements
Prepared statements are used automatically for parameterized queries when prepare is enabled.
const sql = mysql('mysql://root:password@localhost:3306/app', {
prepare: true,
maxStatements: 100
})prepare: falsedisables prepared statement cachingmaxStatementscontrols the per-connection statement cache size
Reserved and direct connections
await sql.reserve()
Reserve a pooled connection for isolated work.
const reserved = await sql.reserve()
await reserved`select * from users`
reserved.release()await sql.connect()
Create a dedicated connection outside the pool.
const connection = await sql.connect()
await connection`select * from users`
await connection.end()
await sql.end()Custom types
You can override MySQL type parsers with the types option.
import Decimal from 'decimal.js'
import mysql from 'mysql-next'
const sql = mysql('mysql://root:password@localhost:3306/app', {
types: {
decimal: (raw) => new Decimal(raw),
newdecimal: (raw) => new Decimal(raw)
}
})Supported parser hooks: types.decimal and types.newdecimal.
Observability
The root client emits query events for pooled and dedicated connections.
sql.on('query', (event) => {
console.log(event.sql)
console.log(event.protocol)
console.log(event.duration)
console.log(event.rowCount)
console.log(event.connectionId)
console.log(event.serverConnectionId)
console.log(event.errorCode)
console.log(event.sqlState)
})Pool lifecycle events are also available.
sql.on('connect', console.log)
sql.on('acquire', console.log)
sql.on('release', console.log)
sql.on('drain', console.log)
sql.on('error', console.error)Teardown / Cleanup
To ensure proper teardown on shutdown, call await sql.end().
await sql.end()This rejects new queries, waits for active work to finish, and closes underlying connections.
If you need to close everything immediately, use:
sql.destroy()Error handling
Errors are thrown to the related query and never emitted as a global catch-all.
Server errors are exposed as MySQLError with MySQL-specific metadata.
import { MySQLError } from 'mysql-next'
try {
await sql`select * from missing_table`
} catch (error) {
if (error instanceof MySQLError) {
console.log(error.code)
console.log(error.sqlState)
}
}Connection and runtime errors from Node.js are forwarded as regular Error instances.
TypeScript support
You can pass the expected row list type directly to a query.
interface User {
id: number
name: string
}
const users = await sql<User[]>`select * from users`
users[0]?.id
users[0]?.nameYou can also use exported result types.
import mysql, { type InsertResult, type SelectResult } from 'mysql-next'
const sql = mysql('mysql://root:password@localhost:3306/app')
const users = await sql<SelectResult<{ id: number; name: string }>>`
select id, name
from users
`
const insert = await sql<InsertResult>`
insert into users ${sql({ name: 'Murray' })}
`
console.log(insert.lastInsertId)Exported result types include QueryResult, QueryRows, SelectResult<T>, CommandResult, InsertResult, UpdateResult, and DeleteResult.
Migration tools
mysql-next does not ship with a migration system. Use your existing migration tool or execute SQL files during setup.
Benchmarks
Head-to-head against mysql2 and the classic mysql package on the same MySQL 8.0 server (Docker), sequential clients (max: 1), identical workloads and pool sizing. Measured with perf_hooks after warmup.
| Workload | mysql-next | mysql2 | mysql (classic) |
| --------------------------- | ---------: | ---------: | ---------------: |
| Text query — SELECT 1 | 9,978/s| 7,039/s | 6,171/s |
| Prepared statement + params | 10,394/s| 7,520/s | 6,690/s * |
| Resultset — 200 rows | 554/s | 535/s | 544/s |
| Pool acquire + release | 406,721/s| 231,554/s| 15,358/s |
Ratios versus the runner-up per workload: 1.42x, 1.38x, 1.04x and 1.76x.
* The classic mysql package has no server-side prepare; its parameterized workload interpolates values client-side before sending.
Numbers from a representative single run on Linux / Node 24; absolute values vary by machine and by run (the resultset workload is network/server-bound), but mysql-next leads or ties on every workload across repeated runs. Row decoding uses per-column compiled readers, keeping per-cell work allocation-free. The classic driver's low pool-cycling score comes from its connection-borrow handshake (COM_PING + auth check), which both modern drivers skip.
Reproducing
npm run benchRequires a running MySQL instance reachable through DATABASE_URL (or .env.local). The script creates the target database if missing and prints the full table above, including heap deltas and the opt-in safety-mode row described below.
The safety trade-off
By default the pool returns connections to the idle queue without a server round trip, which keeps prepared statement caches warm across borrows — this is what the numbers above measure, and it matches how mainstream drivers behave.
If your app shares pools across tenants or needs guaranteed clean session state (user variables, temp tables, locks), opt in:
const sql = mysql(url, { resetOnRelease: true })This sends COM_RESET_CONNECTION on every release: one extra round trip per release plus a re-prepare per statement. On the same machine, prepared statements in this mode run at ~4,500 ops/s versus ~10,400 ops/s in default mode. Correctness first when you need it, speed first when you don't.
Support
Enjoying this tool? Consider supporting the project.
