DEV Community

Cover image for Stop Storing Passwords Like It's 1999: The Node.js + MySQL Reality Check
Vasu Ghanta
Vasu Ghanta

Posted on

Stop Storing Passwords Like It's 1999: The Node.js + MySQL Reality Check

If you're storing passwords in plain text in MySQL, we need to have an intervention. It's 2025. We have drones delivering tacos, but some devs treat passwords like grocery lists.

Opening a database and seeing "password123" just chilling there unprotected? That's leaving your door open with a neon "FREE STUFF" sign.

The Horror You're Creating

You just built your first Node.js auth system. Feeling brilliant. Then you write THIS:

app.post('/register', (req, res) => {
  const { username, password } = req.body;
  db.query('INSERT INTO users (username, password) VALUES (?, ?)', 
    [username, password]); // 🚨 DISASTER
});
Enter fullscreen mode Exit fullscreen mode

Congrats! When your database leaks (when, not if), every password sits there like free samples at Costco.

People reuse passwords everywhere. That "fluffy2023"? You just gave hackers access to Sarah's email, banking, and Instagram. Her cat Fluffy is judging you.

Solution: BCrypt Saves Everything

BCrypt blends passwords beyond recognition with no "undo" button. Hackers see gibberish like $2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW.

const bcrypt = require('bcrypt');

app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);

  db.query('INSERT INTO users (username, password) VALUES (?, ?)', 
    [username, hashedPassword]);
});
Enter fullscreen mode Exit fullscreen mode

The 10 is "salt rounds"—how many scramble cycles. Ten is perfect for 2025.

Mistake #2: Comparing Wrong

You hashed passwords? Don't ruin it with this:

// WRONG - Never works
if (results[0].password === password) {
  res.send('Logged in!');
}
Enter fullscreen mode Exit fullscreen mode

You can't compare hashed with plain text using ===. That's asking if a smoothie equals an apple.

The fix:

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  db.query('SELECT * FROM users WHERE username = ?', [username], 
    async (err, results) => {
      const match = await bcrypt.compare(password, results[0].password);

      if (match) {
        res.send('Welcome!');
      } else {
        res.send('Try again');
      }
  });
});
Enter fullscreen mode Exit fullscreen mode

BCrypt's compare() handles verification magically (it's math, but feels like magic).

Mistake #3: No Validation

Accepting ANY password is suicide. "1"? Welcome! Empty string? Sure!

function isPasswordValid(password) {
  const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
  return regex.test(password);
}

app.post('/register', async (req, res) => {
  if (!isPasswordValid(req.body.password)) {
    return res.status(400).send('Need 8+ chars, letters, numbers & symbols');
  }
  // continue with hashing...
});
Enter fullscreen mode Exit fullscreen mode

Mistake #4: SQL Injection

Building queries with template strings? Asking for disaster:

// NEVER
const query = `SELECT * FROM users WHERE username = '${username}'`;
Enter fullscreen mode Exit fullscreen mode

Type admin'-- and BAM—logged in as admin without a password.

Use parameterized queries ALWAYS:

db.query('SELECT * FROM users WHERE username = ?', [username]);
Enter fullscreen mode Exit fullscreen mode

Those ? placeholders treat input as VALUES, not code.

Complete Secure Example

const express = require('express');
const bcrypt = require('bcrypt');
const mysql = require('mysql2/promise');

const app = express();
app.use(express.json());

function isPasswordValid(password) {
  const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
  return regex.test(password);
}

app.post('/register', async (req, res) => {
  const { username, password } = req.body;

  if (!isPasswordValid(password)) {
    return res.status(400).json({ error: 'Weak password!' });
  }

  const hashedPassword = await bcrypt.hash(password, 10);
  await db.query('INSERT INTO users (username, password) VALUES (?, ?)',
    [username, hashedPassword]);

  res.json({ message: 'Account created!' });
});

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  const [results] = await db.query('SELECT * FROM users WHERE username = ?', 
    [username]);

  if (!results.length) {
    return res.status(401).send('Invalid credentials');
  }

  const match = await bcrypt.compare(password, results[0].password);

  if (match) {
    res.json({ message: 'Login successful!' });
  } else {
    res.status(401).send('Invalid credentials');
  }
});
Enter fullscreen mode Exit fullscreen mode

Wake Up Call

It's 2025. AI writes code and cars drive themselves. Zero excuse for MySpace-era password storage.

Hash with BCrypt, validate inputs, use parameterized queries, never store plain text. Your users trust you—don't be tomorrow's breach headline.

Go secure those passwords before the internet finds you. And trust me, it will.

Top comments (2)

Collapse
 
bhavin-allinonetools profile image
Bhavin Sheth

This should honestly be required reading for anyone building auth for the first time.
The “when, not if” database leak line hits hard — and the bcrypt examples are super clear.
Plain-text passwords still showing up in 2025 is scary, but posts like this help fix that. Great write-up 👍

Collapse
 
vasughanta09 profile image
Vasu Ghanta

Thanks so much, Bhavin! 🙌 Really happy to hear the article came across as clear and useful — especially the “when, not if” bit about database leaks. That’s exactly the mindset shift I was hoping to spark: secure password handling isn’t optional anymore. Appreciate the encouragement and glad you found the bcrypt examples approachable! 🚀