# Best Practices for Using a Database with Node.js in New Express Projects

> Learn best practices for using databases with Node.js in new Express projects. Secure your application with connection pooling, parameterized queries, and environment config.

- Repository: [expressjs/express](https://github.com/expressjs/express)
- Tags: best-practices
- Published: 2026-02-20

---

**Use connection pooling, parameterized queries, and environment-driven configuration to safely integrate any SQL or NoSQL database with Node.js while preventing connection leaks and injection attacks.**

When building a new project with the Express framework, integrating a database with Node.js requires careful architectural decisions to prevent performance bottlenecks and security vulnerabilities. The expressjs/express repository demonstrates that while the framework itself remains agnostic to database choices, production-ready applications demand robust connection management and async patterns.

## Choose the Right Database Driver for Node.js

Selecting the appropriate driver or ORM depends on your data model and performance requirements.

### SQL and ORM Options

For relational data with complex queries, use **Sequelize**, **Knex**, or **TypeORM**. These provide full-featured query building, migrations, and transaction support. For lightweight access or legacy code, use **node-postgres (pg)** or **mysql2**, which expose native protocols and allow fine-tuned query optimization.

### NoSQL and Cache Drivers

For document-oriented stores, **Mongoose** provides schema enforcement, middleware hooks, and built-in validation for MongoDB. For high-throughput caching, **ioredis** or the native **redis** client offer native commands and connection pooling.

> **Note:** Express does not ship with a database layer. The framework focuses on request/response handling while you plug in the database of choice. The repository contains illustrative examples, such as the MVC example that wires a MySQL pool in [`examples/mvc/db.js`](https://github.com/expressjs/express/blob/main/examples/mvc/db.js).

## Implement Connection Pooling in Express

Creating a new connection for every request exhausts database resources and increases latency. Use a connection pool that lives for the lifetime of the Node process.

```js
// mysql2 pool (ESM)
import mysql from 'mysql2/promise';
export const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,           // adjust to your load
  queueLimit: 0
});

```

Connection pooling reduces latency by reusing connections and guarantees a maximum number of concurrent DB sessions, protecting the database from overload.

## Write Asynchronous Database Code with async/await

Express middleware can be async functions. Wrap database calls with `try / catch` and forward errors to `next(err)` so the central error handler responds consistently.

```js
import { pool } from './db.js';

export async function listUsers(req, res, next) {
  try {
    const [rows] = await pool.query('SELECT * FROM users');
    res.json(rows);
  } catch (err) {
    next(err);                     // Express error middleware
  }
}

```

This pattern avoids callback hell and keeps the request pipeline readable.

## Secure Database Credentials with Environment Variables

Never hard-code passwords or connection strings. Load them from environment variables or a secret manager and validate them at startup.

```js
if (!process.env.DB_URL) {
  throw new Error('Missing DB_URL env var');
}

```

The repository's examples demonstrate this approach in multiple places, such as the Redis session example [`examples/session/redis.js`](https://github.com/expressjs/express/blob/main/examples/session/redis.js).

## Prevent SQL Injection with Parameterized Queries

SQL injection remains a critical vulnerability. Never concatenate user input into query strings. Rely on driver-provided placeholders or ORM sanitization.

```js
await pool.execute('SELECT * FROM users WHERE email = ?', [req.body.email]);

```

For MongoDB, Mongoose automatically escapes values, but you still need to validate schemas.

## Handle Database Transactions Safely

For multi-step operations that must be atomic, wrap them in a transaction.

```js
const conn = await pool.getConnection();
await conn.beginTransaction();
try {
  await conn.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
  await conn.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);
  await conn.commit();
} catch (e) {
  await conn.rollback();
  throw e;
} finally {
  conn.release();
}

```

## Ensure Graceful Shutdown and Connection Cleanup

When the process receives SIGTERM (e.g., in containers), close the pool to let pending queries finish.

```js
process.on('SIGTERM', async () => {
  await pool.end();   // close all idle connections
  process.exit(0);
});

```

## Monitor Database Performance and Metrics

Instrument query latency, error rates, and pool usage. Tools like Prometheus (via `express-prometheus-middleware`) or New Relic can scrape these metrics.

```js
app.use(require('express-prometheus-middleware')({
  metricsPath: '/metrics',
  collectDefaultMetrics: true,
}));

```

## Implement Pagination for Large Result Sets

Never send an entire table in a single response. Use `LIMIT/OFFSET` (or cursor-based pagination) and, for huge blobs, stream results.

```js
const [rows] = await pool.query('SELECT * FROM logs ORDER BY id LIMIT ? OFFSET ?', [pageSize, offset]);
res.json(rows);

```

## Test with In-Memory or Mock Databases

Unit tests should not depend on a real DB. Use SQLite in-memory for SQL or `mongo-memory-server` for MongoDB. The repository's test suite shows how to mock request/response without touching a DB, keeping the core framework lightweight.

## Common Challenges When Using a Database with Node.js

| Challenge | Typical Symptom | Mitigation |
|-----------|------------------|------------|
| **Connection leaks** | Process hangs after many requests, "Too many connections" DB error | Always `release()` or `end()` connections in a `finally` block; use pool-provided `query` shortcuts that auto-release. |
| **Blocking the event loop** | High CPU, request latency spikes | Avoid synchronous DB driver methods; never use `await` inside a tight loop without yielding; consider batching or streaming. |
| **ORM performance overhead** | Queries slower than raw SQL | Profile critical paths; use raw queries for hot paths; configure ORM caching; keep model definitions lean. |
| **Schema migrations drift** | Production DB out of sync with code | Adopt a migration tool (e.g., **sequelize-cli**, **knex migrations**, **db-migrate**) and run migrations as part of deployment. |
| **Error handling inconsistency** | Some failures return 500, others 200 with error body | Centralise error handling through Express error-middleware; wrap DB errors in a custom `DatabaseError` class. |
| **Environment-specific credentials** | Wrong DB used on staging vs prod | Use `.env` files per environment or a secret management service; never commit production credentials. |
| **Scaling out (multiple Node instances)** | Duplicate connection pools overwhelm DB | Size the pool per instance wisely (`connectionLimit = totalDBConnections / instanceCount`). Use a load balancer or connection-pooling proxy if needed. |
| **Transaction deadlocks** | Requests hang or abort unexpectedly | Keep transaction scope minimal; order tables consistently; retry on deadlock errors. |

## Key Files in the Express Repository

| File | Role |
|------|------|
| [`examples/mvc/db.js`](https://github.com/expressjs/express/blob/main/examples/mvc/db.js) | Demonstrates a reusable MySQL connection pool for an MVC-style app. |
| [`examples/session/redis.js`](https://github.com/expressjs/express/blob/main/examples/session/redis.js) | Illustrates configuring Redis for session storage using environment variables. |
| [`lib/express.js`](https://github.com/expressjs/express/blob/main/lib/express.js) | Core Express constructor showing how middlewares are attached. |
| [`index.js`](https://github.com/expressjs/express/blob/main/index.js) | Entry point that re-exports the Express API. |

## Summary

- **Choose drivers wisely**: Match SQL complexity with ORM capabilities or use lightweight drivers like `mysql2` and `pg` for fine control.
- **Pool connections**: Create a single pool instance at startup and reuse it across requests to prevent connection leaks and resource exhaustion.
- **Secure credentials**: Load connection strings from environment variables, as demonstrated in [`examples/session/redis.js`](https://github.com/expressjs/express/blob/main/examples/session/redis.js), and never commit secrets.
- **Prevent injection**: Use parameterized queries or ORM sanitization exclusively; never concatenate user input into SQL strings.
- **Handle transactions**: Wrap multi-step operations in explicit transactions with proper rollback and release logic.
- **Monitor and paginate**: Instrument query latency, implement pagination with `LIMIT/OFFSET`, and gracefully shut down pools on SIGTERM.

## Frequently Asked Questions

### How do I prevent connection leaks in a Node.js Express application?

Always release connections in a `finally` block or use pool methods like `pool.query()` that automatically return connections to the pool. In the Express MVC example ([`examples/mvc/db.js`](https://github.com/expressjs/express/blob/main/examples/mvc/db.js)), the shared pool pattern ensures controllers never hold connections longer than necessary, preventing the "Too many connections" errors that plague leaky applications.

### Should I use an ORM or raw SQL drivers for my Express project?

Use an ORM like Sequelize or TypeORM when you need migrations, complex relationships, and type safety across your database with Node.js. Use raw drivers like `mysql2` or `pg` when you need maximum performance and fine-grained query control, as shown in the repository's MVC example that uses a raw MySQL pool for lightweight data access.

### How do I handle database transactions in Express middleware?

Acquire a connection from the pool, begin a transaction, execute your queries, and commit only if all operations succeed. Always wrap transactions in `try/catch/finally` blocks to ensure `rollback()` and `release()` are called, preventing partial data updates and connection leaks that can occur when requests fail mid-transaction.

### What is the best way to manage database credentials securely in Node.js?

Never hard-code credentials; instead, load them from environment variables at startup and validate their presence before initializing the pool. The Express repository demonstrates this pattern in [`examples/session/redis.js`](https://github.com/expressjs/express/blob/main/examples/session/redis.js), where the Redis connection string is configured via environment variables rather than committed to source control, ensuring staging and production environments use distinct credentials without code changes.