Skip to content

Database Integration

Express doesn't care what database you use. It passes the same req/res to every handler whether you're reading from PostgreSQL , MongoDB , or a CSV file
What matters is how you manage connections , handle transactions , and prevent injection. Connection pooling keeps your database alive under load. Transaction middleware keeps your data consistent. Parameterized queries keep attackers out. Get these right and your database layer is solid. Get them wrong and you're debugging production outages at 3AM

PostgreSQL with pg

npm install pg
const { Pool } = require('pg')

// connection pool - never create individual clients
const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME || 'myapp',
  user: process.env.DB_USER || 'postgres',
  password: process.env.DB_PASSWORD,
  max: 20,                           // max connections in pool
  idleTimeoutMillis: 30000,          // close idle connections after 30s
  connectionTimeoutMillis: 5000      // fail fast if DB is down
})

// query helper
async function query(text , params) {
  const start = Date.now()
  const result = await pool.query(text , params)
  const duration = Date.now() - start
  console.log('Query:', { text: text.slice(0, 80), duration, rows: result.rowCount })
  return result
}

// route
app.get('/users' , async (req , res) => {
  const result = await query('SELECT id , email , name FROM users ORDER BY id')
  res.json(result.rows)
})

app.get('/users/:id' , async (req , res) => {
  const { id } = req.params
  const result = await query('SELECT * FROM users WHERE id = $1' , [id])
  if (result.rows.length === 0) {
    return res.status(404).json({ error: 'User not found' })
  }
  res.json(result.rows[0])
})

MySQL with mysql2

npm install mysql2
const mysql = require('mysql2/promise')

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 0
})

// route
app.get('/users' , async (req , res) => {
  const [rows] = await pool.execute(
    'SELECT id , email , name FROM users ORDER BY id'
  )
  res.json(rows)
})

MongoDB with Mongoose

npm install mongoose
const mongoose = require('mongoose')

mongoose.connect(process.env.MONGO_URI , {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000
})

// schema
const userSchema = new mongoose.Schema({
  email: { type: String , required: true , unique: true , lowercase: true },
  name: { type: String , required: true , trim: true },
  passwordHash: { type: String , required: true },
  role: { type: String , enum: ['user' , 'admin'] , default: 'user' },
  createdAt: { type: Date , default: Date.now }
})

// don't return password hash in JSON
userSchema.methods.toJSON = function() {
  const obj = this.toObject()
  delete obj.passwordHash
  delete obj.__v
  return obj
}

const User = mongoose.model('User' , userSchema)

// route
app.get('/users' , async (req , res) => {
  const users = await User.find().select('-passwordHash')
  res.json(users)
})

connection pooling pattern

Every database call shares a pool. Never create connections per request

// src/config/database.js
const { Pool } = require('pg')

let pool

function getPool() {
  if (!pool) {
    pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 20,
      idleTimeoutMillis: 30000
    })

    pool.on('error' , (err) => {
      console.error('Unexpected pool error:', err)
      // pool will auto-recover but log for alerting
    })
  }
  return pool
}

module.exports = { getPool }
// routes/users.js
const { getPool } = require('../config/database')

router.get('/' , async (req , res) => {
  const pool = getPool()
  const result = await pool.query('SELECT * FROM users')
  res.json(result.rows)
})

transaction middleware

Transactions ensure atomicity - all operations succeed or none do

async function withTransaction(callback) {
  const pool = getPool()
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await callback(client)
    await client.query('COMMIT')
    return result
  } catch (err) {
    await client.query('ROLLBACK')
    throw err
  } finally {
    client.release()
  }
}

// usage
app.post('/transfer' , async (req , res) => {
  const { fromId , toId , amount } = req.body

  const result = await withTransaction(async (client) => {
    // deduct from sender
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1',
      [amount , fromId]
    )

    // credit receiver
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount , toId]
    )

    // log transaction
    await client.query(
      'INSERT INTO transfers (from_id , to_id , amount) VALUES ($1 , $2 , $3)',
      [fromId , toId , amount]
    )
  })

  res.json({ success: true })
})

database security patterns

// 1. NEVER log query parameters with sensitive data
// BAD: console.log('Query:', text, params)
// GOOD: console.log('Query:', text) - params might have passwords

// 2. Always use parameterized queries - NEVER string interpolation
// BAD: `SELECT * FROM users WHERE id = ${req.params.id}`
// GOOD: 'SELECT * FROM users WHERE id = $1', [req.params.id]

// 3. Limit returned columns
// BAD: SELECT * FROM users  (returns passwordHash to Node.js)
// GOOD: SELECT id , email , name , role FROM users

// 4. Set statement timeout
await pool.query('SET statement_timeout = 5000')  // kill queries over 5s

// 5. Connection string from env , never hardcoded
// process.env.DATABASE_URL - not a literal string in code

prerequisites

express_13_validation.md - express-validator , Joi , Zod , sanitization


next → express_15_rest_api.md