Database errors to status codes
ValidationError, duplicate keys and CastError: turn each into the right 4xx, and never leak the password.
When the database says no, it throws. Your job is to translate that into a status code the client understands, instead of a blanket 500.
The translation table
| What happened | How to spot it | Status |
|---|---|---|
| broke a schema rule (required, minlength, enum) | err.name === "ValidationError" |
400 |
| unique field already taken | err.code === 11000 |
409 |
malformed id passed to findById |
err.name === "CastError" |
400 |
| query found nothing | result is null |
404 |
| anything else | 500 |
try {
const user = await User.create({ email, password });
res.status(201).json({ id: user._id, email: user.email });
} catch (err) {
if (err.name === "ValidationError") return res.status(400).json({ error: err.message });
if (err.code === 11000) return res.status(409).json({ error: "Email already registered" });
res.status(500).json({ error: "Something went wrong" });
}
Unique means unique… after normalising
Ada@Example.com and ada@example.com are the same person. lowercase: true and trim: true on the schema make the unique index catch that.
Never send the password back
res.json(user) sends every field, password included. Pick the fields you return, or strip sensitive ones. Tests check for this with not.toHaveProperty("password").
Don't trust the body
User.create(req.body) saves whatever the client sends, including role: "admin". Copy the fields you expect: const { email, password } = req.body.
Turn database errors into status codes
POST /users creates a user (201). A validation failure is 400, a duplicate email is 409, and the password never comes back in the response.