The Night the Database Cried
It was 3 AM when my phone buzzed violently. Our production database had become a graveyard of empty user profiles. A registration endpoint had silently accepted null
values for months. "How?" I asked my sleep-deprived self. The answer? We’d forgotten to validate required fields and check user existence properly.
This is why field validation and user existence checks aren’t just checkboxes — they’re your first line of defense against data chaos. Let me show you how to implement these safeguards in Node.js, learned through years of firefighting production issues.
Step 1: Setting the Stage: Project Directory
To get started, here’s our folder structure:
cd src && mkdir utils
cd utils && touch validateRequiredFields.ts checkUserExists.ts
We’ll create two essential modules:
- validateRequiredFields.ts for managing fields validation.
- checkUserExists.ts for handling user exists validation.
Step 2: Validating Required Fields — Your Data Bouncer
Problem Solved: Prevents API endpoints from processing incomplete/invalid requests that can corrupt your data or crash services.
Implementation:
Inside of validateRequiredFields.ts
:
// utils/validateRequiredFields.ts
interface ValidationResult {
isValid: boolean;
error?: string;
}
/**
* Validates required fields in a request
* @param fields - Object containing field names and their values
* @returns ValidationResult indicating if all required fields are present
* @example
* const validation = validateRequiredFields({ email: 'user@example.com', password: '' });
* if (!validation.isValid) {
* // Handle missing fields
* }
*/
export const validateRequiredFields = (fields: Record<string, any>): ValidationResult => {
const missingFields = Object.entries(fields)
.filter(([_, value]) => !value?.toString().trim())
.map(([key]) => key);
if (missingFields.length > 0) {
return {
isValid: false,
error: `Missing required fields: ${missingFields.join(', ')}`,
};
}
return { isValid: true };
};
Pro Tip: Always combine this with schema validation (like Zod or Joi) for complex validation rules. I learned this the hard way when someone submitted a password field containing just spaces!
Usage in Express Route:
// routes/auth.ts
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Validate fields
const validation = validateRequiredFields({ email, password });
if (!validation.isValid) {
return res.status(400).json({ error: validation.error });
}
// Continue with registration
});
Step 3: User Existence Checks — The Gatekeeper
Problem Solved: Prevents duplicate accounts and ensures operations only affect existing users.
Implementation:
Inside of checkUserExists.ts
:
// utils/checkUserExists.ts
import pool from '../db/db';
interface CheckResult {
exists: boolean;
userData?: any;
}
/**
* Checks if a user exists in the database
* @param email - User's email address
* @param expectUserToExist - If true, throws error when user doesn't exist; if false, throws when user does exist
* @returns Object containing existence status and optional user data
* @throws Error when user existence doesn't match expectation
*/
export const checkUserExists = async (
email: string,
shouldExist: boolean = true
): Promise<CheckResult> => {
const userExists = await pool.query(
'SELECT * FROM users WHERE email = $1 LIMIT 1',
[email.toLowerCase().trim()]
);
const exists = rows.length > 0;
if (shouldExist && !exists) throw new Error('User not found');
if (!shouldExist && exists) throw new Error('Email already registered');
return { exists, userData: exists ? userExists.rows[0] : undefined };
};
Critical Insight: Always normalize emails to lowercase and trim whitespace. I once spent 4 hours debugging a “user doesn’t exist” error that turned out to be “USER@DOMAIN.COM” vs “user@domain.com”.
Usage Flow:
// Registration endpoint
app.post('/register', async (req, res) => {
try {
await checkUserExists(email, false); // Expect no existing user
// Create new user
} catch (error) {
return res.status(409).json({ error: error.message });
}
});
// Login endpoint
app.post('/login', async (req, res) => {
try {
const { userData } = await checkUserExists(email, true);
if (!userData.is_verified) {
return res.status(403).json({ error: 'Verify your email first' });
}
// Continue login
} catch (error) {
return res.status(404).json({ error: error.message });
}
});
Why This Matters: The Three-Layer Defense
- Client-Side Validation: Basic checks in the UI
- Field Validation: Your API’s first reality check
- Database Checks: The final authority
Without this trifecta, you’re vulnerable to:
- Duplicate accounts wasting storage
- Ghost users cluttering analytics
- Security holes from unverified operations
The Production-Proven Approach
After implementing these patterns across 12+ services, here’s my battle-tested advice:
- Centralize Validation: Create reusable modules like these
- Standardize Errors: Use consistent error formats
- Log Validation Failures: They’re early warning signs
- Test Boundary Cases: Empty strings, nulls, whitespace-only values
// Test case that saved me last week
test('rejects password with only spaces', () => {
const result = validateRequiredFields({
password: ' '
});
expect(result.isValid).toBe(false);
});
Your Challenge This Week:
Audit one authentication endpoint. How many of these checks are missing? Implement these utilities and watch your error logs shrink like magic.
Remember: In the world of Node.js APIs, good validation isn’t just code — it’s a love letter to your future self.
Before You Go… 🚀
Thanks for sticking around!
Top comments (0)