DEV Community

Cover image for AI Pair Programming: How Intelligent Code Assistants Transform Modern Web Development
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

AI Pair Programming: How Intelligent Code Assistants Transform Modern Web Development

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I remember the first time I tried an AI pair programmer. I was stuck on a tricky React component. I typed a frustrated comment into my editor: “Need to sort this list by date, then group by user, but handle timezone offsets.” A second later, a complete function appeared. It wasn't just syntax; it was the logic I needed, with clear variable names and a comment about UTC conversion. I didn't have to search Stack Overflow or dig through old code. The tool understood my intent and gave me a working solution. It felt less like using a tool and more like having a conversation with a very knowledgeable, patient colleague.

That’s the shift happening right now. These AI systems have moved far beyond predicting your next word. They are active participants in the building process. They live inside your code editor, watching your work, ready to help. They can write a function from a description, explain a block of complex legacy code, spot a security hole, or draft a set of tests. For anyone building for the web today, this changes everything. It makes the process faster, less frustrating, and often, more creative.

Let me show you what this looks like in practice. Imagine you’re building a user dashboard and need a data table with sorting and filtering. Instead of building it from scratch, you describe it.

You might write a comment like this: // Create a table component for user data. Columns: name, email, signup date, status. Make it sortable by any column and filterable by status.

The AI can then generate the skeleton of a proper component.

import { useState, useMemo } from 'react';

function UserTable({ users }) {
  const [sortConfig, setSortConfig] = useState({ key: 'signupDate', direction: 'desc' });
  const [statusFilter, setStatusFilter] = useState('all');

  const filteredUsers = useMemo(() => {
    if (statusFilter === 'all') return users;
    return users.filter(user => user.status === statusFilter);
  }, [users, statusFilter]);

  const sortedUsers = useMemo(() => {
    const sortableItems = [...filteredUsers];
    if (sortConfig.key) {
      sortableItems.sort((a, b) => {
        const aVal = a[sortConfig.key];
        const bVal = b[sortConfig.key];

        if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
        if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
        return 0;
      });
    }
    return sortableItems;
  }, [filteredUsers, sortConfig]);

  const requestSort = (key) => {
    let direction = 'asc';
    if (sortConfig.key === key && sortConfig.direction === 'asc') {
      direction = 'desc';
    }
    setSortConfig({ key, direction });
  };

  return (
    <div className="user-table">
      <div className="filters">
        <label>
          Filter by status:
          <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
            <option value="all">All</option>
            <option value="active">Active</option>
            <option value="inactive">Inactive</option>
            <option value="pending">Pending</option>
          </select>
        </label>
      </div>
      <table>
        <thead>
          <tr>
            <th onClick={() => requestSort('name')}>Name</th>
            <th onClick={() => requestSort('email')}>Email</th>
            <th onClick={() => requestSort('signupDate')}>Signup Date</th>
            <th onClick={() => requestSort('status')}>Status</th>
          </tr>
        </thead>
        <tbody>
          {sortedUsers.map(user => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>{new Date(user.signupDate).toLocaleDateString()}</td>
              <td className={`status-${user.status}`}>{user.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is a solid starting point. It includes the core logic for sorting and filtering using React hooks, which is what you asked for. You might tweak the styling or the date format, but the heavy lifting is done. The AI didn't just write code; it applied a common React pattern for sortable tables based on its training.

One of the most time-consuming tasks for me has always been writing tests. It’s easy to put off. You know you should do it, but writing tests for a simple utility function can feel tedious. An AI pair programmer changes this. It can look at your function and propose a comprehensive test suite in seconds.

Say you have a function that formats a product price for display, considering currency, locale, and discounts.

function formatPrice(amount, currency = 'USD', locale = 'en-US', discount = 0) {
  if (typeof amount !== 'number' || isNaN(amount)) {
    throw new Error('Amount must be a valid number');
  }
  if (discount < 0 || discount > 1) {
    throw new Error('Discount must be between 0 and 1');
  }

  const finalAmount = amount * (1 - discount);

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(finalAmount);
}
Enter fullscreen mode Exit fullscreen mode

You can ask the AI to write tests for this. It will analyze the function, see the validation logic, the math, and the use of Intl.NumberFormat. It will then generate tests that cover the happy path, edge cases, and error conditions.

describe('formatPrice', () => {
  test('formats USD price correctly', () => {
    expect(formatPrice(29.99, 'USD')).toBe('$29.99');
    expect(formatPrice(1000, 'USD')).toBe('$1,000.00');
  });

  test('formats EUR price for German locale', () => {
    // Note: Output might vary slightly by Node.js version, so we check for the pattern
    const result = formatPrice(49.95, 'EUR', 'de-DE');
    expect(result).toMatch(/49,95\s*€/); // German uses comma as decimal separator
  });

  test('applies discount correctly', () => {
    expect(formatPrice(100, 'USD', 'en-US', 0.2)).toBe('$80.00');
    expect(formatPrice(75.50, 'USD', 'en-US', 0.1)).toBe('$67.95'); // 75.50 * 0.9 = 67.95
  });

  test('handles zero and full discount', () => {
    expect(formatPrice(50, 'USD', 'en-US', 0)).toBe('$50.00');
    expect(formatPrice(50, 'USD', 'en-US', 1)).toBe('$0.00');
  });

  test('throws error for invalid amount', () => {
    expect(() => formatPrice('not a number')).toThrow('Amount must be a valid number');
    expect(() => formatPrice(NaN)).toThrow('Amount must be a valid number');
  });

  test('throws error for invalid discount', () => {
    expect(() => formatPrice(100, 'USD', 'en-US', -0.1)).toThrow('Discount must be between 0 and 1');
    expect(() => formatPrice(100, 'USD', 'en-US', 1.5)).toThrow('Discount must be between 0 and 1');
  });

  test('handles different currencies', () => {
    const jpyResult = formatPrice(500, 'JPY', 'ja-JP');
    expect(jpyResult).toMatch(/¥500/); // Yen often has no fractional digits
  });
});
Enter fullscreen mode Exit fullscreen mode

This test suite is thorough. It checks basic formatting, locale-specific rules, discount math, and error handling. It gives you confidence in your function immediately. The AI considered edge cases I might have missed, like a 100% discount or invalid inputs. This is a massive boost to code quality and reliability.

When you join a new project or open an old codebase, understanding the existing code is a hurdle. You might see a complex function and spend minutes or hours figuring out what it does and why it’s written a certain way. AI can act as an instant guide.

You can highlight a confusing block of code and ask, “What does this do?” The AI will provide a plain-English summary.

Here’s a more complex example. Let’s say you find this function in a utilities file.

function normalizeAndAggregate(metrics, weights, windowSize = 5) {
  if (!Array.isArray(metrics) || metrics.some(m => typeof m !== 'number')) {
    return { error: 'Invalid metrics array' };
  }
  if (!weights || metrics.length !== weights.length) {
    return { error: 'Weights array mismatch' };
  }

  // Min-max normalization per metric
  const normalized = metrics.map((val, idx) => {
    const min = Math.min(...metrics);
    const max = Math.max(...metrics);
    return max === min ? 0.5 : (val - min) / (max - min);
  });

  // Weighted sum
  let weightedSum = 0;
  normalized.forEach((normVal, idx) => {
    weightedSum += normVal * (weights[idx] || 1);
  });

  // Simple moving average simulation
  const historical = JSON.parse(localStorage.getItem('metricHistory') || '[]');
  historical.push(weightedSum);
  if (historical.length > windowSize) historical.shift();
  localStorage.setItem('metricHistory', JSON.stringify(historical));

  const movingAvg = historical.reduce((a, b) => a + b, 0) / historical.length;

  return {
    raw: weightedSum,
    smoothed: movingAvg,
    trend: historical.length > 1 ? weightedSum > historical[historical.length - 2] : 'stable'
  };
}
Enter fullscreen mode Exit fullscreen mode

An AI explanation would be something like: “This function takes an array of numbers (metrics) and a corresponding weights array. First, it validates the inputs. It then normalizes each metric to a 0-1 scale based on the min and max values in the array. It calculates a weighted sum of these normalized values. Finally, it maintains a short history of these weighted sums in the browser’s local storage to compute a simple moving average and determine a trend (up, down, or stable). The return object contains the raw weighted sum, the smoothed average, and the trend.”

It might also point out potential issues: “There’s a potential performance issue with Math.min(...metrics) and Math.max(...metrics) being called inside a map loop, recalculating the same min/max for each element. The use of localStorage for calculation state is unusual and could cause problems in server-side rendering or if storage is full.”

This immediate context is powerful. It turns a confusing block of code into a clear concept, speeding up onboarding and debugging dramatically.

Documentation is another area where AI excels. It’s tedious to write, but AI can generate clear, consistent comments and API docs by analyzing your code structure. This keeps your codebase understandable for your future self and your team.

Take a function that fetches and caches data.

async function fetchWithCache(endpoint, options = {}, ttl = 300000) { // 5 minutes default
  const cacheKey = `cache_${endpoint}_${JSON.stringify(options)}`;
  const cached = sessionStorage.getItem(cacheKey);

  if (cached) {
    const { data, timestamp } = JSON.parse(cached);
    if (Date.now() - timestamp < ttl) {
      return data;
    }
  }

  try {
    const response = await fetch(endpoint, options);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();

    sessionStorage.setItem(cacheKey, JSON.stringify({
      data,
      timestamp: Date.now()
    }));

    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    // Optional: return stale cache if available and fresh enough?
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

The AI can generate JSDoc comments and an explanation.

/**
 * Fetches data from an endpoint with session-storage caching.
 * 
 * This function attempts to serve requests from a cache in `sessionStorage` 
 * to avoid redundant network calls. Cache entries are automatically invalidated 
 * after a specified time-to-live (TTL).
 *
 * @param {string} endpoint - The URL to fetch from.
 * @param {RequestInit} [options={}] - Fetch API options (method, headers, etc.).
 * @param {number} [ttl=300000] - Time-to-live for cache in milliseconds (default 5 minutes).
 * @returns {Promise<any>} The fetched or cached JSON data.
 * @throws {Error} If the network request fails and no valid cache exists.
 *
 * @example
 * // Fetch a list of posts, cached for 1 minute
 * const posts = await fetchWithCache('/api/posts', {}, 60000);
 *
 * @note
 * - Cache keys are generated from the endpoint and options, so identical calls are cached.
 * - Uses `sessionStorage`, which is tab-specific and cleared when the tab closes.
 * - Consider error handling for `sessionStorage` being full or disabled.
 */
Enter fullscreen mode Exit fullscreen mode

This is documentation I would actually read. It explains the purpose, the parameters, the return value, and even includes a useful note about storage limitations.

Security is a critical concern where human review is essential, but AI can be a powerful first line of defense. It can scan your code as you write it and flag patterns that are commonly associated with vulnerabilities.

For instance, you might be writing an Express.js route to update a user profile.

// A naive, vulnerable version
app.put('/api/user/:id', (req, res) => {
  const userId = req.params.id;
  const updates = req.body;

  // Dangerous: Directly merging user input into a database update
  db.run(`UPDATE users SET ${Object.keys(updates).map(k => `${k} = ?`).join(', ')} WHERE id = ?`, 
    [...Object.values(updates), userId]);

  res.json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

An AI tool would likely highlight this and suggest: “Warning: This dynamically builds an SQL query from user input, which is vulnerable to SQL injection. It also allows updating any column, including potentially sensitive ones like isAdmin. Consider using a parameterized query with an allowlist of updatable fields.”

Then, it could propose a safer alternative.

app.put('/api/user/:id', async (req, res) => {
  const userId = req.params.id;
  const updates = req.body;

  // Define allowed fields that a user can update
  const allowedFields = ['displayName', 'avatarUrl', 'bio'];
  const filteredUpdates = {};

  for (const key of allowedFields) {
    if (key in updates) {
      filteredUpdates[key] = updates[key];
    }
  }

  if (Object.keys(filteredUpdates).length === 0) {
    return res.status(400).json({ error: 'No valid fields to update' });
  }

  try {
    // Use parameterized query with explicit fields
    const setClause = Object.keys(filteredUpdates).map(key => `${key} = ?`).join(', ');
    const values = Object.values(filteredUpdates);
    values.push(userId); // For the WHERE clause

    await db.run(`UPDATE users SET ${setClause} WHERE id = ?`, values);

    res.json({ success: true, updatedFields: Object.keys(filteredUpdates) });
  } catch (error) {
    console.error('Update failed:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
Enter fullscreen mode Exit fullscreen mode

This suggestion fixes the injection vulnerability and implements a principle of least privilege by restricting which fields can be updated. It turns a security flaw into a learning moment and provides a corrected pattern.

Perhaps one of the most subtle benefits is refactoring assistance. As codebases grow, functions become bloated and responsibilities get mixed. AI can suggest ways to clean this up.

You might have a component that does too much.

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [cart, setCart] = useContext(CartContext);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const [prodRes, revRes] = await Promise.all([
          fetch(`/api/products/${productId}`),
          fetch(`/api/products/${productId}/reviews`)
        ]);

        if (!prodRes.ok || !revRes.ok) throw new Error('Failed to fetch');

        const prodData = await prodRes.json();
        const revData = await revRes.json();

        if (isMounted) {
          setProduct(prodData);
          setReviews(revData);
        }
      } catch (err) {
        if (isMounted) setError(err.message);
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchData();

    return () => { isMounted = false; };
  }, [productId]);

  const addToCart = (item) => {
    setCart(prev => [...prev, { ...item, id: Date.now() }]);
    // Show a temporary notification
    const notif = document.createElement('div');
    notif.className = 'cart-notification';
    notif.textContent = 'Added to cart!';
    document.body.appendChild(notif);
    setTimeout(() => notif.remove(), 2000);
  };

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!product) return <p>Product not found.</p>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <button onClick={() => addToCart(product)}>Add to Cart</button>
      <h2>Reviews</h2>
      <ReviewList reviews={reviews} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

An AI might suggest: “This component handles data fetching, state management, cart logic, and even direct DOM manipulation for notifications. This makes it hard to test and reuse. Consider extracting custom hooks for data fetching and cart notifications, and moving the notification logic into a proper UI context or component.”

It could then help you refactor by creating these separate pieces.

// Custom hook for fetching product data
function useProductData(productId) {
  const [state, setState] = useState({ product: null, reviews: [], loading: true, error: null });

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      setState(s => ({ ...s, loading: true, error: null }));
      try {
        const [prodRes, revRes] = await Promise.all([
          fetch(`/api/products/${productId}`),
          fetch(`/api/products/${productId}/reviews`)
        ]);

        if (!prodRes.ok || !revRes.ok) throw new Error('Failed to fetch');

        const prodData = await prodRes.json();
        const revData = await revRes.json();

        if (isMounted) {
          setState({ product: prodData, reviews: revData, loading: false, error: null });
        }
      } catch (err) {
        if (isMounted) setState(s => ({ ...s, loading: false, error: err.message }));
      }
    };

    fetchData();
    return () => { isMounted = false; };
  }, [productId]);

  return state;
}

// Refactored, focused component
function ProductPage({ productId }) {
  const { product, reviews, loading, error } = useProductData(productId);
  const { addToCartWithNotification } = useCart(); // Assume another custom hook

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!product) return <p>Product not found.</p>;

  return (
    <div>
      <ProductDetail product={product} onAddToCart={addToCartWithNotification} />
      <h2>Reviews</h2>
      <ReviewList reviews={reviews} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This refactored code is cleaner. The ProductPage component is now primarily responsible for rendering. The logic is distributed into reusable hooks (useProductData, useCart), which are easier to test and maintain. The AI helped identify the “code smells” and provided a path to a better structure.

The biggest change for me has been in how I learn. When I encounter a new library or a piece of syntax I don’t recognize, I can ask the AI directly in my editor. It’s like having a mentor looking over my shoulder, ready to explain anything.

For example, if I see resolvers in a GraphQL setup and I’m not sure how they work, I can ask. The AI can give me a concise explanation and show me an example relevant to my project’s context.

// AI Explanation:
// In GraphQL, a resolver is a function that fetches the data for a specific field in your schema.
// It's like a backend handler for each piece of data your frontend requests.

// You have a schema like:
// type Query {
//   user(id: ID!): User
// }
// type User {
//   name: String
//   email: String
// }

// The resolver for the `user` field would look up a user by ID.
const resolvers = {
  Query: {
    user: async (parent, args, context) => {
      // `args` contains { id: '123' }
      return await context.db.users.findUnique({ where: { id: args.id } });
    }
  },
  // You can also have resolvers for fields on the User type itself
  User: {
    // This resolver runs when someone asks for a User's 'name'
    name: (parent) => parent.fullName // You could transform data here
  }
};
Enter fullscreen mode Exit fullscreen mode

This contextual learning is incredibly efficient. Instead of switching to a browser, searching, and sifting through documentation, I get an immediate, practical explanation tied to what I’m working on.

It’s important to remember that these tools are partners, not replacements. They are incredibly good at generating code based on patterns they’ve seen, at explaining concepts, and at spotting common errors. But they lack true understanding of your project’s unique business rules, your team’s specific architecture decisions, or the nuanced “why” behind certain code.

The best results come from a collaborative workflow. I describe my intent, the AI provides a draft or a suggestion, and then I apply my judgment. I review the code. Does it align with our project’s patterns? Is it efficient? Does it handle edge cases correctly for our domain? I refine it. This cycle—human intent, AI draft, human refinement—is where the real power lies. It offloads the repetitive, boilerplate, and research-heavy aspects of coding, freeing me to focus on architecture, problem-solving, and the creative parts of building software.

This is how modern web development is being reshaped. The friction of translating thought into code is lowering. The barrier to exploring new technologies is shrinking. The consistency and security of our codebases are improving. It’s not about writing less code; it’s about writing better code, with more confidence, and spending our time and mental energy on the parts that truly require human creativity and insight. The AI pair programmer isn’t taking the keyboard; it’s sitting beside you, helping you drive faster and navigate more smoothly toward your destination.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)