TL;DR
The #1 input validation best practice is to never trust user input. Validate on both client (for UX) and server (for security). Use schema validation libraries like Zod, check data types, enforce length limits, and sanitize output. This prevents 79% of injection and data corruption vulnerabilities.
"All input is evil until proven otherwise. Validate everything, trust nothing."
The Cardinal Rule: Never Trust User Input
Every piece of data from users, URL parameters, cookies, and headers can be malicious:
// All of these need validation:
req.body // Form data, JSON payloads
req.params // URL parameters (/users/:id)
req.query // Query strings (?page=1)
req.headers // Headers (authorization, content-type)
req.cookies // Cookies
formData.get() // FormData from forms
event.target.value // User input in forms
Best Practice 1: Use Schema Validation 3 min
Schema validation ensures data matches expected structure and types:
import { z } from 'zod';
// Define the expected shape
const userSchema = z.object({
email: z.string().email('Invalid email format'),
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(100, 'Name must be under 100 characters')
.regex(/^[a-zA-Z\s'-]+$/, 'Name contains invalid characters'),
age: z.number()
.int('Age must be a whole number')
.min(13, 'Must be at least 13 years old')
.max(120, 'Invalid age'),
website: z.string().url().optional(),
});
// Validate input
function validateUser(input: unknown) {
const result = userSchema.safeParse(input);
if (!result.success) {
return {
valid: false,
errors: result.error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
};
}
return { valid: true, data: result.data };
}
// Usage in API route
app.post('/api/users', (req, res) => {
const validation = validateUser(req.body);
if (!validation.valid) {
return res.status(400).json({ errors: validation.errors });
}
// validation.data is now typed and safe to use
createUser(validation.data);
});
Best Practice 2: Validate on Client AND Server 2 min
Both are necessary for different reasons:
| Validation Location | Purpose | Can Be Bypassed? |
|---|---|---|
| Client-side | User experience (instant feedback) | Yes (easily) |
| Server-side | Security (actual protection) | No |
Never rely on client-side validation alone. Attackers can easily bypass JavaScript validation by sending requests directly to your API. Server-side validation is required for security.
Best Practice 3: Validate Specific Types 3 min
Email Validation
const emailSchema = z.string()
.email('Invalid email format')
.max(254, 'Email too long')
.toLowerCase()
.trim();
// Or with custom regex for stricter validation
const strictEmailSchema = z.string()
.regex(
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
'Invalid email format'
);
URL Validation
const urlSchema = z.string()
.url('Invalid URL format')
.refine(
(url) => {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
},
'Only HTTP and HTTPS URLs allowed'
);
ID Validation
// UUID validation
const uuidSchema = z.string().uuid('Invalid ID format');
// Numeric ID (for SQL databases)
const numericIdSchema = z.coerce.number()
.int('ID must be an integer')
.positive('ID must be positive');
// Slug validation
const slugSchema = z.string()
.regex(/^[a-z0-9-]+$/, 'Invalid slug format')
.min(1)
.max(100);
Best Practice 4: Sanitize for Output Context 2 min
Different contexts require different sanitization:
import DOMPurify from 'dompurify';
// HTML context: escape or sanitize
function sanitizeForHtml(input: string) {
// Option 1: Escape (safest, no HTML allowed)
return input
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// Option 2: Sanitize (allow some HTML)
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
});
}
// SQL context: use parameterized queries (not string escaping)
// See database best practices
// URL context: encode
function sanitizeForUrl(input: string) {
return encodeURIComponent(input);
}
// JSON context: stringify handles escaping
JSON.stringify({ userInput: input });
Best Practice 5: Enforce Length Limits 1 min
Prevent denial of service and database issues:
const commentSchema = z.object({
content: z.string()
.min(1, 'Comment cannot be empty')
.max(10000, 'Comment too long (max 10,000 characters)'),
});
const fileUploadSchema = z.object({
name: z.string().max(255, 'Filename too long'),
size: z.number().max(10 * 1024 * 1024, 'File too large (max 10MB)'),
});
// Also limit array lengths
const tagsSchema = z.array(z.string().max(50))
.max(20, 'Too many tags (max 20)');
Best Practice 6: Validate File Uploads 2 min
Files need special validation:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateFile(file: File) {
const errors = [];
// Check MIME type
if (!ALLOWED_TYPES.includes(file.type)) {
errors.push('Invalid file type. Allowed: JPEG, PNG, WebP');
}
// Check file size
if (file.size > MAX_SIZE) {
errors.push('File too large. Maximum size: 5MB');
}
// Check file extension matches MIME type
const ext = file.name.split('.').pop()?.toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png', 'webp'];
if (!ext || !validExtensions.includes(ext)) {
errors.push('Invalid file extension');
}
return {
valid: errors.length === 0,
errors,
};
}
Common Input Validation Mistakes
| Mistake | Risk | Prevention |
|---|---|---|
| Client-only validation | Complete bypass | Always validate server-side |
| Type coercion issues | Unexpected behavior | Explicit type checking |
| Missing length limits | DoS, buffer issues | Set reasonable maximums |
| Trusting file extensions | Malicious uploads | Check MIME type too |
| Accepting javascript: URLs | XSS attacks | Whitelist allowed protocols |
External Resources: For comprehensive input validation guidelines, see the OWASP Input Validation Cheat Sheet and the OWASP Validation Regex Repository.
Which validation library should I use?
Zod is the most popular choice for TypeScript projects due to its excellent type inference. Yup is also good, especially with Formik. For simple cases, you can use built-in methods, but libraries provide better error messages and composability.
Should I sanitize input or output?
Validate and sanitize input, but also sanitize output based on context. Input validation catches obvious issues early. Output encoding/sanitization prevents injection in the specific context (HTML, SQL, URL) where the data is used.
How strict should validation be?
Be as strict as your use case allows without hurting legitimate users. For security-critical fields (passwords, IDs), be very strict. For user content, balance security with usability. Allow international characters in names, for example.
Is TypeScript enough for validation?
No. TypeScript types exist only at compile time. Runtime data (API requests, form submissions) is not type-checked by TypeScript. You need runtime validation like Zod to ensure data matches expected types.
Verify Your Input Validation
Scan your application for input validation vulnerabilities.
Start Free Scan