Back to blog

Generative AI For Developers: Tools and Workflows That Are Transforming Code

Hello HaWkers, generative artificial intelligence has gone from being a technological curiosity to becoming an essential tool in any developer's arsenal. In 2025, those who are not using AI in some way are falling behind.

Have you ever imagined having a pair programmer available 24 hours a day, capable of understanding your code, suggesting improvements, and even writing complete functions? This reality already exists, and in this article we will explore how to make the most of these tools.

The State of AI in Software Development in 2025

The landscape of AI tools for development has evolved drastically. According to recent surveys, more than 70% of professional developers already use some form of AI assistant in their daily workflow.

AI and Code

Main Available Tools

Code Assistants:

  • GitHub Copilot - Integrated directly in the IDE
  • Claude Code - Focus on quality code and explanations
  • Amazon CodeWhisperer - Optimized for AWS
  • Tabnine - Learning from your own code

Development Chatbots:

  • ChatGPT (GPT-4o) - Versatile and widely used
  • Claude - Excellent for complex reasoning
  • Gemini - Strong in long context

Specialized Tools:

  • Cursor - IDE with native AI
  • Codeium - Free alternative to Copilot
  • Sourcegraph Cody - Focused on large codebases

Practical Workflows with AI

The key to productivity with AI is not just using the tools, but integrating them intelligently into your workflow.

Workflow 1: Prompt-Driven Development

Instead of starting by writing code, start by describing what you need:

// Prompt: "Create a React hook to manage form state
// with validation, loading and error. Should support dynamic fields."

// Result generated by AI assistant:
import { useState, useCallback } from 'react';

interface FormField {
  value: string;
  error: string | null;
  touched: boolean;
}

interface FormState {
  [field: string]: FormField;
}

interface FieldValidator {
  (value: string): string | null;
}

interface FormConfig {
  [field: string]: {
    initialValue?: string;
    validator?: FieldValidator;
  };
}

export function useForm(config: FormConfig) {
  const [fields, setFields] = useState<FormState>(() => {
    const initialState: FormState = {};
    for (const [name, options] of Object.entries(config)) {
      initialState[name] = {
        value: options.initialValue || '',
        error: null,
        touched: false
      };
    }
    return initialState;
  });

  const [submitting, setSubmitting] = useState(false);
  const [generalError, setGeneralError] = useState<string | null>(null);

  const updateField = useCallback((name: string, value: string) => {
    setFields(prev => ({
      ...prev,
      [name]: {
        ...prev[name],
        value,
        error: config[name]?.validator?.(value) || null
      }
    }));
  }, [config]);

  const markTouched = useCallback((name: string) => {
    setFields(prev => ({
      ...prev,
      [name]: { ...prev[name], touched: true }
    }));
  }, []);

  const validateAll = useCallback(() => {
    let valid = true;
    const newFields = { ...fields };

    for (const [name, options] of Object.entries(config)) {
      const error = options.validator?.(fields[name].value) || null;
      newFields[name] = { ...newFields[name], error, touched: true };
      if (error) valid = false;
    }

    setFields(newFields);
    return valid;
  }, [fields, config]);

  const submit = useCallback(async (onSubmit: (data: Record<string, string>) => Promise<void>) => {
    if (!validateAll()) return;

    setSubmitting(true);
    setGeneralError(null);

    try {
      const data: Record<string, string> = {};
      for (const [name, field] of Object.entries(fields)) {
        data[name] = field.value;
      }
      await onSubmit(data);
    } catch (error) {
      setGeneralError(error instanceof Error ? error.message : 'Unknown error');
    } finally {
      setSubmitting(false);
    }
  }, [fields, validateAll]);

  const reset = useCallback(() => {
    const initialState: FormState = {};
    for (const [name, options] of Object.entries(config)) {
      initialState[name] = {
        value: options.initialValue || '',
        error: null,
        touched: false
      };
    }
    setFields(initialState);
    setGeneralError(null);
  }, [config]);

  return {
    fields,
    submitting,
    generalError,
    updateField,
    markTouched,
    validateAll,
    submit,
    reset
  };
}

The secret is providing enough context in the prompt to get quality code.

Workflow 2: AI-Assisted Refactoring

Use AI to identify and fix problems in existing code:

// Original code - problematic
function processOrders(orders) {
  let result = [];
  for (let i = 0; i < orders.length; i++) {
    if (orders[i].status == 'active') {
      let total = 0;
      for (let j = 0; j < orders[i].items.length; j++) {
        total = total + orders[i].items[j].price * orders[i].items[j].quantity;
      }
      orders[i].total = total;
      if (total > 100) {
        orders[i].discount = total * 0.1;
      }
      result.push(orders[i]);
    }
  }
  return result;
}

// Prompt: "Refactor this code following best practices:
// TypeScript typing, functional programming, immutability"

// Refactored code by AI:
interface OrderItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface Order {
  id: number;
  status: 'active' | 'cancelled' | 'completed';
  items: OrderItem[];
  total?: number;
  discount?: number;
}

interface ProcessedOrder extends Order {
  total: number;
  discount: number;
}

const calculateOrderTotal = (items: OrderItem[]): number =>
  items.reduce((total, item) => total + item.price * item.quantity, 0);

const calculateDiscount = (total: number, discountThreshold = 100, percentage = 0.1): number =>
  total > discountThreshold ? total * percentage : 0;

const processOrder = (order: Order): ProcessedOrder => {
  const total = calculateOrderTotal(order.items);
  const discount = calculateDiscount(total);

  return {
    ...order,
    total,
    discount
  };
};

const processOrders = (orders: Order[]): ProcessedOrder[] =>
  orders
    .filter(order => order.status === 'active')
    .map(processOrder);

Workflow 3: Test Generation

AI can drastically speed up test creation:

// Prompt: "Generate complete unit tests for processOrders function
// using Vitest, covering success cases, edge cases and errors"

import { describe, it, expect } from 'vitest';
import { processOrders, calculateOrderTotal, calculateDiscount } from './orders';

describe('processOrders', () => {
  describe('calculateOrderTotal', () => {
    it('should calculate total correctly for multiple items', () => {
      const items = [
        { id: 1, name: 'Product A', price: 50, quantity: 2 },
        { id: 2, name: 'Product B', price: 30, quantity: 1 }
      ];

      expect(calculateOrderTotal(items)).toBe(130);
    });

    it('should return 0 for empty list', () => {
      expect(calculateOrderTotal([])).toBe(0);
    });

    it('should handle decimal quantities', () => {
      const items = [
        { id: 1, name: 'Product', price: 10.5, quantity: 3 }
      ];

      expect(calculateOrderTotal(items)).toBeCloseTo(31.5);
    });
  });

  describe('calculateDiscount', () => {
    it('should apply 10% discount for totals above 100', () => {
      expect(calculateDiscount(150)).toBe(15);
    });

    it('should return 0 for totals below or equal to threshold', () => {
      expect(calculateDiscount(100)).toBe(0);
      expect(calculateDiscount(50)).toBe(0);
    });

    it('should accept custom thresholds and percentages', () => {
      expect(calculateDiscount(200, 150, 0.2)).toBe(40);
    });
  });

  describe('processOrders', () => {
    it('should filter only active orders', () => {
      const orders = [
        { id: 1, status: 'active', items: [] },
        { id: 2, status: 'cancelled', items: [] },
        { id: 3, status: 'active', items: [] }
      ];

      const result = processOrders(orders);

      expect(result).toHaveLength(2);
      expect(result.every(o => o.status === 'active')).toBe(true);
    });

    it('should calculate total and discount for each order', () => {
      const orders = [
        {
          id: 1,
          status: 'active',
          items: [{ id: 1, name: 'Item', price: 60, quantity: 2 }]
        }
      ];

      const result = processOrders(orders);

      expect(result[0].total).toBe(120);
      expect(result[0].discount).toBe(12);
    });

    it('should return empty array when no active orders', () => {
      const orders = [
        { id: 1, status: 'cancelled', items: [] }
      ];

      expect(processOrders(orders)).toHaveLength(0);
    });

    it('should maintain immutability of original orders', () => {
      const originalOrder = {
        id: 1,
        status: 'active' as const,
        items: [{ id: 1, name: 'Item', price: 50, quantity: 1 }]
      };
      const orders = [originalOrder];

      processOrders(orders);

      expect(originalOrder).not.toHaveProperty('total');
      expect(originalOrder).not.toHaveProperty('discount');
    });
  });
});

Best Practices for Using AI in Code

The effectiveness of AI depends a lot on how you interact with it.

Be Specific in Prompts

Weak prompt:
"Make a login function"

Strong prompt:
"Create an authentication function in TypeScript that:

  • Receives email and password as parameters
  • Validates the email format
  • Verifies minimum password of 8 characters
  • Returns an object with JWT token or typed error
  • Uses bcrypt for password comparison
  • Includes basic rate limiting"

Always Review Generated Code

AI can generate code with:

  • Security vulnerabilities
  • Incorrect logic for edge cases
  • Outdated dependencies
  • Patterns not suitable for your project

Review checklist:

  • Does the code do what was requested?
  • Are there adequate input validations?
  • Are errors handled correctly?
  • Does the code follow project patterns?
  • Are there tests for critical cases?

Use AI to Learn, Not Just Copy

// Instead of just copying, ask for explanations:

// Prompt: "Explain step by step how this debounce algorithm works
// and why each part is necessary"

function debounce<T extends (...args: any[]) => any>(
  func: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: NodeJS.Timeout | null = null;

  return function (...args: Parameters<T>) {
    // 1. If there is a pending timeout, we cancel it
    // This ensures only the last call will be executed
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // 2. We create a new timeout
    // The original function will only be called after 'delay' ms
    // without new calls
    timeoutId = setTimeout(() => {
      func(...args);
      timeoutId = null;
    }, delay);
  };
}

// Practical use: avoid multiple API calls when typing
const fetchSuggestions = debounce(async (term: string) => {
  const response = await fetch(`/api/suggestions?q=${term}`);
  return response.json();
}, 300);

Integrating AI in Team Workflow

AI adoption should be a team decision, not individual.

Defining Guidelines

When to use AI:

  • Boilerplate and repetitive code
  • Test generation
  • Documentation
  • Rapid prototyping
  • Debugging and troubleshooting

When to be careful:

  • Critical business logic
  • Security code
  • Sensitive integrations
  • Architectural decisions

Documenting AI Usage

Some teams adopt the practice of documenting when code was generated by AI:

/**
 * Calculates the distance between two geographic points using the Haversine formula.
 *
 * @ai-generated - Base code generated by Claude, reviewed and adapted by the team
 * @param lat1 Latitude of the first point
 * @param lon1 Longitude of the first point
 * @param lat2 Latitude of the second point
 * @param lon2 Longitude of the second point
 * @returns Distance in kilometers
 */
function calculateHaversineDistance(
  lat1: number,
  lon1: number,
  lat2: number,
  lon2: number
): number {
  const R = 6371; // Earth radius in km
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);

  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c;
}

function toRad(degrees: number): number {
  return degrees * (Math.PI / 180);
}

The Future of Development with AI

Trends point to even deeper integration:

Short term (2025-2026):

  • IDEs with fully integrated AI
  • Automated debugging
  • Assisted code review

Medium term (2026-2028):

  • Autonomous development agents
  • Automatic translation between languages
  • Automated performance optimization

Long term (2028+):

  • Specification-driven development
  • Proactive code maintenance
  • AI-suggested architecture

The key is to see AI as a tool that amplifies your capabilities, not as a replacement. The best developers will be those who know how to use AI strategically while maintaining solid fundamental skills.

If you want to understand more about how AI is impacting the job market, check out the article Soft Skills For Developers in 2025 where we discuss skills that complement the use of AI tools.

Let us go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments