Tutorials Logic, IN info@tutorialslogic.com

Node.js with MySQL: Pools, CRUD, Transactions, and Concurrency

Node.js with MySQL

MySQL is the world's most popular open-source relational database. Node.js can connect to MySQL using the mysql2 package - the modern, promise-based MySQL driver. This combination is widely used for building REST APIs, web applications, and backend services.

Step 1 - Install mysql2

Terminal

Terminal
npm install mysql2

Step 2 - Create a Connection

db.js

db.js
const mysql = require('mysql2/promise');

// Create a connection pool (recommended over single connection)
const pool = mysql.createPool({
  host:     'localhost',
  user:     'root',
  password: 'your_password',
  database: 'mydb',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

module.exports = pool;

Step 3 - CRUD Operations

CRUD with mysql2

CRUD with mysql2
const pool = require('./db');

async function main() {
  // SELECT - Read all users
  const [rows] = await pool.query('SELECT * FROM users');
  console.log('All users:', rows);

  // INSERT - Add a new user (use ? placeholders to prevent SQL injection)
  const [result] = await pool.query(
    'INSERT INTO users (name, email) VALUES (?, ?)',
    ['Alice', 'alice@example.com']
  );
  console.log('Inserted ID:', result.insertId);

  // UPDATE - Update a user
  await pool.query(
    'UPDATE users SET name = ? WHERE id = ?',
    ['Alice Smith', result.insertId]
  );

  // DELETE - Remove a user
  await pool.query('DELETE FROM users WHERE id = ?', [result.insertId]);

  console.log('Done!');
}

main().catch(console.error);

Step 4 - Express REST API with MySQL

REST API

REST API
const express = require('express');
const pool    = require('./db');
const app     = express();
app.use(express.json());

// GET all users
app.get('/users', async (req, res) => {
  const [rows] = await pool.query('SELECT * FROM users');
  res.json(rows);
});

// GET user by ID
app.get('/users/:id', async (req, res) => {
  const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
  if (!rows.length) return res.status(404).json({ error: 'Not found' });
  res.json(rows[0]);
});

// POST create user
app.post('/users', async (req, res) => {
  const { name, email } = req.body;
  const [result] = await pool.query(
    'INSERT INTO users (name, email) VALUES (?, ?)', [name, email]
  );
  res.status(201).json({ id: result.insertId, name, email });
});

// PUT update user
app.put('/users/:id', async (req, res) => {
  const { name, email } = req.body;
  await pool.query('UPDATE users SET name=?, email=? WHERE id=?', [name, email, req.params.id]);
  res.json({ message: 'Updated' });
});

// DELETE user
app.delete('/users/:id', async (req, res) => {
  await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]);
  res.json({ message: 'Deleted' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Pool Ownership

Create one MySQL pool during application startup and share it through the data-access layer. A pool reuses authenticated connections and limits database concurrency. Opening a connection inside every request adds handshake latency and can exhaust the server during a traffic spike; holding one checked-out connection forever defeats the pool.

Configure host, port, database, and credentials from validated environment values. Use a dedicated application account with only the required schema privileges. Set finite connection and acquisition limits from measured database capacity, not from the number of HTTP requests the server might receive.

Release a checked-out connection in a `finally` block. Handle startup and shutdown explicitly: reject readiness until a small database check succeeds, stop accepting new HTTP work during termination, let in-flight operations finish within a deadline, and close the pool. Log pool saturation without logging credentials or full queries containing private data.

  • Own one pool per process and share it through repositories.
  • Bound pool concurrency below the database connection budget.
  • Release checked-out connections on success and failure.
  • Close the pool during graceful shutdown.

Query Boundary

Use placeholders for every value supplied outside trusted source code. Parameter binding separates data from SQL syntax and blocks ordinary injection through names, emails, IDs, search values, and dates. Placeholders do not safely substitute table names, column names, sort directions, or SQL fragments; choose those from a small application-owned allowlist.

Validate and normalize request values before the repository call. Convert numeric IDs deliberately, bound text length, parse dates, reject unknown fields, and distinguish absent values from explicit null. Parameterization protects query structure, while validation protects domain rules and resource use.

Select named columns instead of `SELECT *`, limit result size, and map driver rows into a response DTO. The database schema may contain password hashes, internal flags, or audit fields that must never cross the HTTP boundary. Return safe domain errors while recording a query name, duration, request ID, and database error code internally.

  • Bind values and allowlist identifiers or sort choices.
  • Validate type, length, range, and nullability before querying.
  • Select and return only fields required by the caller.
  • Keep raw SQL and driver errors out of API responses.

Transaction Contract

Use a transaction when several statements represent one business change, such as creating an order and reserving inventory. Check out one connection, begin the transaction, run every related statement on that same connection, commit only after all invariants pass, and roll back on failure. Queries sent through the general pool may use different connections and do not join the transaction.

Keep transactions short. Do not wait for user input, call a slow external service, or perform large unrelated reads while locks are held. When an external message must follow the commit, store an outbox record in the same transaction and dispatch it afterward so a process crash cannot commit data but lose the event.

Concurrent updates need an explicit rule. Use affected-row checks, unique constraints, conditional updates, or a version column to detect stale writes. A successful SQL statement is not proof that the intended row existed or that another request did not change it first.

  • Run transactional statements on one checked-out connection.
  • Roll back in the error path and release in `finally`.
  • Keep network waits and reviewer pauses outside transactions.
  • Detect conflicts with constraints or optimistic concurrency.

CRUD HTTP Semantics

Separate HTTP parsing from database work. The route validates headers and body shape, the service applies authorization and business rules, and the repository owns SQL. This keeps transactions and queries testable without constructing Express request objects and prevents every route from inventing its own error mapping.

Return `201 Created` for a successful create, `200` or `204` for an update according to the response contract, `404` when the authorized resource does not exist, `409` for a uniqueness or version conflict, and a safe `500` for unexpected failure. Check `affectedRows`; an update of zero rows must not be reported as successful.

Use cursor or keyset pagination for large tables and a stable indexed order. Bound page size and filters. For deletion, decide whether the domain requires hard delete, soft delete, retention, or an audit record. An idempotent delete can return a consistent outcome when the resource is already absent, but the API contract should state that choice.

  • Keep route, service, and repository responsibilities distinct.
  • Map domain outcomes to deliberate HTTP statuses.
  • Verify affected rows and generated identifiers.
  • Bound pagination and choose stable indexed ordering.

Schema and Index Fit

Let the database enforce durable invariants with primary keys, foreign keys, unique constraints, nullability, and appropriate data types. Application validation improves feedback, but concurrent requests can pass the same pre-check; the constraint remains the final authority.

Create indexes from measured query shapes. A lookup by email needs a different index from a tenant-scoped recent-orders list. Inspect execution plans for slow queries and avoid functions or implicit conversions that prevent index use. More indexes increase write cost and storage, so every index needs a query it supports.

Apply schema migrations as versioned deployment artifacts. Prefer backward-compatible expand-and-contract changes during rolling releases: add the new shape, deploy code that supports both, backfill safely, switch reads, then remove the old shape later. Test migration and rollback or forward-fix behavior on representative data.

  • Enforce durable invariants in MySQL as well as Node.js.
  • Design indexes from filters, joins, and ordering.
  • Measure plans and query latency before tuning.
  • Keep migrations compatible with rolling application versions.

Database Verification

Test repositories against a real MySQL instance or an isolated database created for the suite. Cover empty results, duplicate keys, null values, transaction rollback, stale versions, connection loss, timeout, and pagination boundaries. A mock can verify service branching but cannot prove SQL syntax, collation, constraints, or driver conversion behavior.

In production, monitor query latency percentiles, error codes, transaction rollback, pool usage, acquisition wait, connection resets, deadlocks, and database CPU. Add request and operation identifiers so a slow HTTP call can be correlated with its repository operation without recording sensitive parameter values.

Set timeouts and cancellation expectations at every layer. A client disconnect does not guarantee MySQL stopped executing, so writes still need idempotency or conflict protection. Retry only failures known to be transient and only when repeating the operation is safe.

  • Run SQL and transaction tests against the real engine.
  • Observe pool pressure and query latency together.
  • Retry only classified transient and idempotent work.
  • Verify shutdown, migration, and database-outage behavior.

Transaction Failure and Concurrency

A transaction belongs to one checked-out connection. Begin, query, commit, and rollback through that same connection, then release it in `finally`. Running transaction statements through the pool can send them to different sessions and silently destroy the atomicity the code appears to promise. Keep the transaction short and never wait for unrelated network calls while locks are held.

Choose the isolation and locking behavior from the business invariant. A read followed by a write can lose an update when two requests race; use a conditional update, unique constraint, version column, or locking read as the case requires. Treat deadlocks and lock timeouts as expected concurrency outcomes. Retry only an idempotent transaction, with a limit and jitter, after rolling back the failed attempt.

Classify errors before mapping them to HTTP outcomes. Constraint violations may become a conflict or validation response, missing records may become not found, and connectivity failures should remain unavailable or internal failures. Do not expose SQL text, schema names, driver stacks, or credentials to clients. Preserve a safe cause and correlation ID for operations.

Test rollback after every write step, simultaneous updates to the same logical record, duplicate keys, pool exhaustion, a dropped connection, and process shutdown with borrowed connections. Verify the final database state, not just the response. An endpoint is transactionally correct only when its invariant survives those failure paths.

  • Keep every transaction statement on one checked-out connection.
  • Protect invariants with database constraints and concurrency controls.
  • Retry only bounded, idempotent transaction attempts.
  • Verify final state after rollback and concurrent execution.
Before you move on

Node.js MySQL Connect CRUD Mastery Check

1 checks
  • Connect Node.js request handling to MySQL safely with pools, parameters, and error boundaries.

Node JS Questions Learners Ask

A pool reuses database connections instead of opening a new TCP connection for every request. That reduces latency and prevents spikes from exhausting MySQL connection limits.

The SQL string contains placeholders, and user values are passed separately. The driver escapes and binds those values instead of letting raw input become SQL syntax.

Raw MySQL errors can reveal table names, column names, SQL fragments, or infrastructure details. The handler should log the detailed error internally with request context, then return a safe client message and status code.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.