DEV Community

Cover image for 5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)
Dharanidharan
Dharanidharan

Posted on

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Most of us have shipped something that "worked on my machine" only to watch it fall apart in production. The frustrating part? Beginner projects tend to fail in the same areas: mobile UX, performance, accessibility, and security. These mistakes are predictable which means they're fixable.

This post walks through five critical pitfalls I see constantly, with real code examples and actionable fixes you can apply today.


Pitfall #1: Breaking Your Site on Mobile

Over 60% of web traffic comes from mobile devices, yet most beginners test exclusively on a large monitor with DevTools occasionally set to "iPhone" mode. The result: horizontal scrolling, cramped spacing, and buttons too small 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;
}
Enter fullscreen mode Exit fullscreen mode

Checklist

  • Test on real devices, not just DevTools
  • Use clamp() for responsive spacing
  • All touch targets should be minimum 44×44px
  • Avoid fixed widths use max-width instead
  • Check your layout at 320px, 768px, and 1440px

Pitfall #2: Shipping Slow Sites (Core Web Vitals Failures)

The three metrics that matter:

  • LCP (Largest Contentful Paint): under 2.5s
  • INP (Interaction to Next Paint): under 200ms
  • CLS (Cumulative Layout Shift): under 0.1

Images without dimensions are a classic CLS killer, and blocking scripts tank LCP.

The Fix

Prevent layout shift with explicit dimensions:

<!-- ✅ 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"
>
Enter fullscreen mode Exit fullscreen mode

Load non-critical scripts only when needed:

<!-- ✅ Load chat widget on first user interaction -->
<script>
  const loadChatWidget = () => {
    const script = document.createElement('script');
    script.src = 'chat-widget.js';
    script.defer = true;
    document.body.appendChild(script);
  };
  document.addEventListener('mousemove', loadChatWidget, { once: true });
</script>
Enter fullscreen mode Exit fullscreen mode

Stop importing entire libraries:

// ❌ Imports everything
import _ from 'lodash';
import moment from 'moment';

// ✅ Tree-shakeable
import { sum } from 'lodash-es';

// ✅ Use native APIs
const formatted = new Intl.DateTimeFormat('en-US').format(new Date());
Enter fullscreen mode Exit fullscreen mode

Checklist

  • Run Lighthouse before every deployment
  • Always specify image dimensions
  • Defer or async all non-critical scripts
  • Code split large JavaScript bundles
  • Monitor Core Web Vitals in Google Search Console

Last year I worked on a client project where the homepage CLS was 0.32 due to missing image dimensions. Fixing just three images dropped it to 0.05 and improved mobile engagement immediately.


Pitfall #3: Locking Out Users with Disabilities

Accessibility lawsuits are rising, but beyond legal risk you're genuinely locking out real users if your site isn't keyboard or screen reader friendly.

The Fix

Use semantic HTML with proper ARIA attributes:

<!-- ✅ Accessible form input -->
<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>
Enter fullscreen mode Exit fullscreen mode

Verify color contrast meets WCAG AA (4.5:1 ratio for body text):

/* ❌ Insufficient contrast (2.5:1) */
.text { color: #767676; background: #ffffff; }

/* ✅ WCAG AA compliant (4.6:1) */
.text { color: #595959; background: #ffffff; }
Enter fullscreen mode Exit fullscreen mode

Make dropdowns keyboard-navigable:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Checklist

  • Use semantic HTML (<button>, <nav>, <main>, <article>)
  • Every form input needs an associated <label>
  • Test with keyboard-only navigation
  • Install eslint-plugin-jsx-a11y to catch issues early
  • Test with NVDA (Windows) or VoiceOver (Mac)

Pitfall #4: Building Insecure APIs

The most common API vulnerability is BOLA Broken Object Level Authorization. It happens when an endpoint doesn't verify that the authenticated user actually owns the resource they're requesting.

// ❌ Anyone can access ANY order by changing the ID in the URL
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.orders.findById(req.params.orderId);
  res.json(order); // No ownership check!
});
Enter fullscreen mode Exit fullscreen mode

The Fix

Always verify resource ownership:

// ✅ Ownership check
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.orders.findOne({
    id: req.params.orderId,
    userId: req.user.id // Critical
  });

  if (!order) return res.status(404).json({ error: 'Order not found' });

  res.json(order);
});
Enter fullscreen mode Exit fullscreen mode

Add rate limiting to prevent brute force:

import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many login attempts, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/api/login', loginLimiter, async (req, res) => { ... });
Enter fullscreen mode Exit fullscreen mode

Use short-lived tokens with secure storage:

// ✅ Short-lived access token + httpOnly refresh token
const accessToken = jwt.sign({ userId: user.id }, ACCESS_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });

res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
});

res.json({ accessToken });
Enter fullscreen mode Exit fullscreen mode

Checklist

  • Verify resource ownership in every API endpoint
  • Rate limit all public endpoints
  • Use short-lived JWTs (15 minutes max)
  • Store refresh tokens in httpOnly cookies
  • Validate and sanitize all user inputs

Pitfall #5: Blindly Trusting AI-Generated Code

AI tools like Copilot and ChatGPT are genuinely useful but they generate code that looks correct while hiding security holes and edge-case bugs. Here's a real example:

// ❌ AI-generated file upload looks fine, has a 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 });
});
Enter fullscreen mode Exit fullscreen mode

An attacker uploads a file named ../../../etc/passwd and you're in trouble.

The Fix

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

app.post('/api/upload', async (req, res) => {
  const file = req.files?.upload;

  if (!file) return res.status(400).json({ error: 'No file uploaded' });
  if (file.size > MAX_FILE_SIZE) return res.status(400).json({ error: 'File too large' });

  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 });
});
Enter fullscreen mode Exit fullscreen mode

Checklist

  • Review AI-generated code line-by-line
  • Test edge cases AI tends to miss
  • Never accept code you don't fully understand
  • Run security linters (eslint-plugin-security)
  • Treat AI as an assistant, not a replacement for thinking

Your Action Plan for This Week

  1. Run a Lighthouse audit on your main pages fix anything below 90
  2. Install eslint-plugin-jsx-a11y and resolve violations
  3. Audit your API endpoints for missing authorization checks
  4. Review any AI-generated code from the past month
  5. Test your site on a real mobile device, not just DevTools

Wrapping Up

These pitfalls affect developers at every level. The difference is that experienced developers have systems to catch them before they reach production automated Lighthouse CI, security scanning in PRs, accessibility linting in the editor, 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.

If you want more content like this, the original and more posts are on my blog: Dharanidharan's Solopreneur Blog.


What's the worst web dev pitfall you've run into? Drop it in the comments I'd love to hear how you fixed it.

Top comments (0)