Let me start with a hard truth: one day your database may leak - a backup lands in the wrong hands, or a bug lets someone dump it. If passwords are stored in plain text that day, your users' accounts are finished - because most people use the same password everywhere. So passwords must never be stored in plain text. Never.
What hashing means, and how it differs from encryption
Hashing is one-way - you can make a hash from a password, but you cannot get the password back from the hash. At login, the password the user gives is hashed again and matched against the hash in the database. The reason for using bcrypt is that it is deliberately slow - when an attacker tries billions of guesses, their cost becomes enormous. Salt is added automatically.
import bcrypt from "bcrypt";
// At signup
const hash = await bcrypt.hash(plainPassword, 10);
await db.user.create({ data: { email, password: hash } });
// At login
const ok = await bcrypt.compare(plainPassword, user.password);
if (!ok) return res.status(401).json({ success: false, message: "Invalid credentials" });JWT - the proof of login
When login succeeds, the server gives a JWT (JSON Web Token). The token is split into three parts - header, payload, signature. The payload holds small information like the user's id and role, and the signature guarantees nobody changed the token in the middle - because changing it needs the server's secret, which only the server knows.
import jwt from "jsonwebtoken";
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);Remember two rules. One, never put passwords or personal information in the payload - anyone can decode and read a token, they just cannot change it. Two, keep the access token's lifetime short - 15 minutes to 1 hour. Because once a token is stolen there is no easy way to cancel it, and a short lifetime means small damage. The next chapter solves this short-lifetime problem with refresh tokens.
The mistake everyone makes
The two most dangerous mistakes: storing passwords in plain text or with just MD5/SHA (these are so fast that an attacker can guess billions per second), and hardcoding the JWT secret in code and pushing it to GitHub. The secret always lives in an environment variable.
Today's practice
Build signup and login endpoints. At signup, hash the password with bcrypt and save it; at login, compare it and on a match return a 15-minute access token. Try a wrong password yourself once and check that the correct 401 comes back.
How to say it in an interview
I store passwords hashed with bcrypt - it is slow and salted, so even if the database leaks, recovering passwords is practically impossible. After login I give a short-lived JWT access token - the payload holds only the user id and role, nothing sensitive, and the secret stays in an environment variable.