TL;DR: Even experienced developers fall into these traps. Learn how to avoid responsive design failures, performance bottlenecks, accessibility oversights, API security holes, and AI code dependency issues with real code fixes you can implement today.
Why This Matters (And Why You're Probably Making These Mistakes)
Let’s be honest: most of us have shipped something that “worked on my machine” and then broke in the real world.
The hard truth is that most beginner projects fail in the same areas: mobile UX, performance, accessibility, and security. The good news is these issues are predictable and fixable once you know what to watch for.
I've been there. We all have. You push code that works on your laptop, ship it to production, and wonder why users are complaining. The difference between junior and senior developers isn't avoiding mistakes it's knowing which mistakes to watch for.
Let's fix five critical pitfalls that are sabotaging your web projects right now.
Pitfall #1: Breaking Your Site on Mobile (The Responsive Design Trap)
The Problem
You test exclusively on your 27-inch monitor with Chrome DevTools occasionally set to "iPhone" mode. Meanwhile, over 60% of web traffic comes from mobile devices, and 74% of users won't return to non-mobile-friendly websites.
Here's what beginners typically do:
/* ❌ This breaks on mobile */
.container {
width: 100%;
max-width: 1200px;
padding: clamp(1rem, 5vw, 3rem);
margin: 0 auto;
}
.button,
a[role="button"],
input[type="submit"],
input[type="button"] {
padding: 12px 24px;
min-height: 44px;
min-width: 44px;
font-size: 1rem;
}
When a user views this on a 375px-wide iPhone, your container forces horizontal scrolling, your padding eats up precious screen space, and your button is impossible to tap accurately.
The Fix
Go mobile-first with fluid layouts and proper touch targets:
/* ✅ Mobile-friendly approach */
.container {
width: 100%;
max-width: 1200px;
padding: clamp(1rem, 5vw, 3rem); /* Scales between 16px and 48px */
margin: 0 auto;
}
.button {
padding: 12px 24px;
min-height: 44px; /* Apple HIG + WCAG 2.2 requirement */
min-width: 44px;
font-size: 1rem; /* Scales with user preferences */
}
Pro tip: WCAG 2.2 recommends minimum target sizes (and Apple recommends ~44×44px) so making buttons comfortably tappable is a safe default.
Prevention Checklist
- [ ] Test on real devices, not just DevTools
- [ ] Use
clamp()for responsive spacing - [ ] Ensure all touch targets are minimum 44×44px
- [ ] Avoid fixed widths use
max-widthinstead - [ ] Check your site at 320px, 768px, and 1440px viewports
Pitfall #2: Shipping Slow Websites (Core Web Vitals Failures)
The Problem
The three Core Web Vitals you should care about are:
- LCP (Largest Contentful Paint): Must be under 2.5 seconds
- INP (Interaction to Next Paint): Must be under 200 milliseconds
- CLS (Cumulative Layout Shift): Must be under 0.1
Most beginners ship sites that fail all three. Here's a common culprit:
<!-- ❌ Images without dimensions cause layout shift -->
<img src="hero.jpg" alt="Hero image">
<!-- ❌ Blocking scripts kill performance -->
<script src="analytics.js"></script>
<script src="chat-widget.js"></script>
<script src="ad-network.js"></script>
The Fix
Prevent layout shift with explicit dimensions and lazy loading:
<!-- ✅ Prevents CLS and optimizes loading -->
<img
src="hero.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="Hero image"
width="1200"
height="630"
style="aspect-ratio: 1200 / 630;"
loading="lazy"
decoding="async"
>
Defer non-critical JavaScript:
<!-- ✅ Non-blocking scripts -->
<link rel="preconnect" href="https://analytics.example.com">
<script src="analytics.js" defer></script>
<!-- ✅ Even better: Load on user interaction -->
<script>
const loadChatWidget = () => {
const script = document.createElement('script');
script.src = 'chat-widget.js';
script.defer = true;
document.body.appendChild(script);
};
// Only load when user shows intent to interact
document.addEventListener('mousemove', loadChatWidget, { once: true });
</script>
Code split your JavaScript bundles:
// ❌ Importing entire libraries
import _ from 'lodash';
import moment from 'moment';
// ✅ Tree-shakeable imports
import { sum } from 'lodash-es';
// ✅ Use native APIs when possible
const formatted = new Intl.DateTimeFormat('en-US').format(new Date());
// ✅ Code split with React lazy loading
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}
Prevention Checklist
- [ ] Run Lighthouse audits before every deployment
- [ ] Limit pages to 50 resources for mobile performance
- [ ] Always specify image dimensions
- [ ] Defer or async all non-critical scripts
- [ ] Implement code splitting for JavaScript bundles
- [ ] Monitor Core Web Vitals in Google Search Console
Pitfall #3: Locking Out Users with Disabilities (Accessibility Oversights)
The Problem
Accessibility lawsuits have been rising for years but even without the legal side, you’re locking out real users if your site isn’t usable with a keyboard or screen reader.
Here's what breaks accessibility:
<!-- ❌ No label, poor error handling, bad contrast -->
<div onclick="handleClick()">Click me</div>
<input type="email" placeholder="Email">
<span class="error" style="color: #767676;">Invalid</span>
The Fix
Use semantic HTML with proper ARIA attributes:
<!-- ✅ Semantic, accessible markup -->
<button onclick="handleClick()">Click me</button>
<div>
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert" style="color: #d32f2f;">
Please enter a valid email address in the format: name@example.com
</span>
</div>
Ensure proper color contrast:
/* ❌ Insufficient contrast (2.5:1) - WCAG failure */
.text {
color: #767676;
background: #ffffff;
}
/* ✅ WCAG AA compliant (4.6:1) */
.text {
color: #595959;
background: #ffffff;
}
/* ✅ Support user preferences */
@media (prefers-contrast: more) {
.text {
color: #000000;
background: #ffffff;
}
}
Build keyboard-navigable components:
// ✅ Accessible dropdown with keyboard support
function DropdownMenu() {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div onKeyDown={handleKeyDown}>
<button
aria-expanded={isOpen}
aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
{isOpen && (
<ul role="menu">
<li role="menuitem">
<a href="/profile">Profile</a>
</li>
<li role="menuitem">
<a href="/settings">Settings</a>
</li>
</ul>
)}
</div>
);
}
Prevention Checklist
- [ ] Use semantic HTML (
<button>,<nav>,<main>,<article>) - [ ] Ensure all form inputs have associated
<label>elements - [ ] Test with keyboard-only navigation (no mouse)
- [ ] Verify color contrast meets WCAG AA (4.5:1 for normal text)
- [ ] Install
eslint-plugin-jsx-a11yto catch issues early - [ ] Test with screen readers (NVDA on Windows, VoiceOver on Mac)
Pitfall #4: Building Insecure APIs (The Authentication Nightmare)
The Problem
99% of enterprises experienced an API security incident in the past year, with 95% of attacks coming from authenticated sessions. The most common vulnerability? Broken Object Level Authorization (BOLA).
Here's the dangerous pattern:
// ❌ Anyone can access ANY order by changing the ID
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await db.orders.findById(req.params.orderId);
res.json(order); // Oops - no ownership check!
});
An attacker simply changes the URL from /api/orders/123 to /api/orders/456 and sees someone else's order.
The Fix
Always verify resource ownership:
// ✅ Verify the user owns this resource
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await db.orders.findOne({
id: req.params.orderId,
userId: req.user.id // Critical ownership check
});
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});
Implement rate limiting to prevent brute force attacks:
import rateLimit from 'express-rate-limit';
// ✅ Limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/login', loginLimiter, async (req, res) => {
const user = await authenticate(req.body);
res.json({ token: generateToken(user) });
});
Use short-lived tokens with proper storage:
// ❌ Long-lived token in localStorage
const token = jwt.sign({ userId: user.id }, SECRET);
localStorage.setItem('token', token); // Vulnerable to XSS
// ✅ Short-lived access token + secure refresh token
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' } // Expires quickly
);
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in httpOnly cookie (protected from XSS)
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken }); // Access token in response body
Prevention Checklist
- [ ] Always verify resource ownership in API endpoints
- [ ] Implement rate limiting on all public endpoints
- [ ] Use short-lived JWT tokens (15 minutes max)
- [ ] Store refresh tokens in httpOnly cookies
- [ ] Never expose sensitive fields in API responses
- [ ] Validate and sanitize all user inputs
- [ ] Use parameterized queries to prevent SQL injection
Pitfall #5: Blindly Trusting AI-Generated Code (The Copilot Trap)
The Problem
AI tools like Copilot and ChatGPT can speed you up massively but they can also generate code that looks correct while hiding security, performance, or edge-case bugs. The danger isn’t using AI it’s trusting it blindly.
Here's a typical AI-generated security nightmare:
// ❌ AI-generated code (looks fine, has critical vulnerability)
app.post('/api/upload', (req, res) => {
const file = req.files.upload;
file.mv(`./uploads/${file.name}`); // Path traversal attack!
res.json({ success: true });
});
An attacker uploads a file named ../../../etc/passwd and overwrites critical system files.
The Fix
Always review and secure AI-generated code:
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
// ✅ Security-reviewed upload handler
app.post('/api/upload', async (req, res) => {
const file = req.files?.upload;
// Validate file exists
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file size
if (file.size > MAX_FILE_SIZE) {
return res.status(400).json({ error: 'File too large' });
}
// Validate extension
const ext = path.extname(file.name).toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return res.status(400).json({ error: 'Invalid file type' });
}
// Generate safe filename (prevents path traversal)
const safeFilename = `${uuidv4()}${ext}`;
const uploadPath = path.join(__dirname, 'uploads', safeFilename);
await file.mv(uploadPath);
res.json({ filename: safeFilename });
});
Understand before accepting:
// AI might generate this (subtle floating-point bug)
function calculateDiscount(price, percentage) {
return price - (price * percentage / 100);
}
// Issues: No validation, floating-point precision errors, no bounds
// ✅ Reviewed and improved version
function calculateDiscount(price, percentage) {
// Validate inputs
if (typeof price !== 'number' || typeof percentage !== 'number') {
throw new TypeError('Price and percentage must be numbers');
}
if (price < 0 || percentage < 0 || percentage > 100) {
throw new RangeError('Invalid price or percentage');
}
// Handle floating-point precision
const discount = Math.round(price * percentage) / 100;
const finalPrice = Math.round((price - discount) * 100) / 100;
return finalPrice;
}
Prevention Checklist
- [ ] Always review AI-generated code line-by-line
- [ ] Test edge cases that AI might miss
- [ ] Run security linters (ESLint with security plugins)
- [ ] Never accept code you don't fully understand
- [ ] Use AI as an assistant, not a replacement for thinking
- [ ] Implement code review process for all AI-generated code
Your Action Plan for This Week
- Run a Lighthouse audit on your main pages fix anything below 90
- Install
eslint-plugin-jsx-a11yand fix accessibility violations - Audit your API endpoints for missing authorization checks
- Review any AI-generated code from the past month
- Test your site on a real mobile device not just DevTools
Wrapping Up
These five pitfalls affect developers at every skill level. The difference is that experienced developers have systems in place to catch them before they reach production:
- Automated Lighthouse CI checks
- Security scanning in pull requests
- Accessibility linting in the editor
- Comprehensive code review processes
- Real device testing in QA
You don't need years of experience to build secure, accessible, performant websites. You just need to know what to look for and now you do.
Struggling to fix these issues in your own projects?
🚀 Explore my custom web development solutions 📅 Let's connect to review your platform
I’m Dharanidharan, a full-stack developer dedicated to building fast, accessible, secure web applications using modern best practices.
What's the worst web dev pitfall you've encountered? Drop a comment below I'd love to hear your horror stories and how you fixed them! 👇
Found this helpful? Give it a ❤️ and bookmark for later. Follow me for more practical web development tutorials with real code examples!