Ir al contenido
  1. Posts/

Cómo adoptar la IA en tu ciclo de desarrollo

Adrián Díaz Cervera
Autor
Adrián Díaz Cervera

La IA ha dejado de ser un hype. Hemos pasado de que la usaban cuatro valientes a que hoy en día prácticamente nadie en el sector se queda al margen. Se habla mucho de que seremos la última generación de desarrolladores que hemos escrito código de verdad, de que la IA lo puede hacer todo… pero ¿cuál es la realidad hoy? ¿Cómo la podemos ir integrando de forma sensata? En esta serie de posts voy a ir contando cómo lo uso yo en mi día a día con proyectos .NET.

El primer contacto
#

La primera vez que te enfrentas a una herramienta de IA como Claude, reconozcámoslo, el caso de uso más habitual es tirarle un error de compilación o una duda puntual. Le preguntas, te propone algo, encaja, y piensas: “esto mola”. Otra funcionalidad que engancha rápido es el autocompletado predictivo: escribes function Fibonacci y ya te tiene el cuerpo entero listo.

Una vez que ves que te complementa, vas cogiendo confianza y empiezas a pedirle cosas más grandes: “añádeme un nuevo endpoint en nuestra API de pedidos”. Aquí es donde te puedes encontrar con dos escenarios muy diferentes:

  1. El agente analiza tu proyecto, entiende qué tienes y genera un endpoint siguiendo tus estándares.
  2. El agente entra en “modo imaginación” y te entrega algo que tiene buena pinta pero que claramente no lo ha escrito nadie del equipo: usa Controllers en lugar de Minimal APIs, cambia la forma de definir las clases, introduce record donde nunca los usáis…

¿Por qué ocurre esto? Porque al agente le pasa exactamente lo mismo que a cualquier ingeniero que entra nuevo en un proyecto: no sabe qué guidelines usa el equipo, qué arquitectura se sigue, qué librerías están aprobadas. Si no se lo cuentas, improvisa, igual que haría una persona.

¿Cómo adoptamos la IA en el equipo?
#

Para mí hay una diferencia enorme entre integrar la IA en un proyecto existente y arrancar uno nuevo con ella desde el principio. Si tienes un proyecto en curso, tienes que pensar bien el proceso. Si arrancas desde cero, tienes la ventaja de establecer las bases desde el primer commit.

Si el proyecto ya existe, el primer movimiento es ejecutar /init desde Claude Code en la raíz del repositorio. El comando escanea el repo y genera un CLAUDE.md automático con lo que infiere: estructura de carpetas, tecnologías detectadas, comandos del proyecto. Es un punto de partida útil, pero casi siempre necesita corrección: puede malinterpretar la arquitectura, incluir carpetas irrelevantes como bin/ u obj/, o asumir patrones que no usáis. Tómatelo como un borrador que hay que revisar con el equipo antes de hacer commit.

Otro error habitual al arrancar en un repo grande es querer dar todo el contexto de golpe. Lo que funciona mejor es ir por capas: empieza con el CLAUDE.md básico (stack y estructura), comprueba que el agente genera algo coherente con una tarea pequeña, y ve añadiendo guidelines a medida que detectas que se desvía. Si el agente comete el mismo error dos veces, eso es una señal clara de que falta esa regla en el fichero de contexto.

Paso 1: Qué incluir en el fichero de contexto
#

Un buen CLAUDE.md no es un resumen del README: es una guía de trabajo. La pregunta que tienes que responder es ¿qué necesitaría saber un ingeniero nuevo el primer día para no romper nada y encajar en el equipo? Eso mismo es lo que necesita el agente.

Para un proyecto .NET, como mínimo debería cubrir:

## Stack
- .NET 10, ASP.NET Core Minimal APIs con MinApiLib
- CQRS sin MediatR: handlers inyectados directamente vía [FromServices]
- FluentValidation para validaciones (devuelve 422, no 400)
- xUnit v3 + WebApplicationFactory para tests de integración

## Estructura del proyecto
- src/Api          → endpoints y configuración de la app (record : Post/Get con MinApiLib)
- src/Application  → handlers, commands, queries (vertical slices)
- src/Domain       → entidades e interfaces de repositorio
- tests/           → tests unitarios e integración

## Comandos habituales
dotnet build
dotnet test
dotnet run --project src/Api

## Convenciones importantes
- Los endpoints son records que heredan de Post/Get/etc. de MinApiLib
- El binding del body se hace con [FromBody] en HandleAsync, nunca en el constructor
- ValidationProblem debe pasar statusCode: StatusCodes.Status422UnprocessableEntity
- No añadir librerías nuevas sin consultarlo primero

Con esto el agente ya sabe con qué trabaja, dónde vive cada cosa y cómo ejecutar el proyecto. Es suficiente para empezar. El resto de las reglas las irás añadiendo a medida que detectes que se desvía.

Paso 2: Documentar las guidelines del equipo
#

Con el contexto general no basta. El siguiente paso es crear una carpeta docs/ en el repositorio con al menos estos ficheros:

  • architecture.md: qué arquitectura usamos (por ejemplo, Vertical Slice Architecture), qué patrones están prohibidos, cómo se organizan las features.
  • guidelines.md: nomenclatura de clases, cómo se nombran los endpoints, estilo de código, reglas de EF Core (¿usamos migraciones automáticas o manuales?), etc.
  • decisions/: una carpeta con ADRs (Architecture Decision Records). Cada fichero explica una decisión técnica y el motivo: por qué usamos Minimal APIs en lugar de Controllers, por qué elegimos MediatR, etc. Esto es especialmente valioso para la IA, porque le da el por qué, no solo el qué.

Cuanto más contexto le des, más coherente será el código que genere.

Paso 3: Definir qué acciones queremos que haga la IA
#

Una vez que el agente tiene contexto del proyecto y de las guidelines, el siguiente paso es establecer para qué tareas lo vamos a usar de forma recurrente. Las que a mí más valor me aportan en proyectos .NET son:

  • Revisión de PRs: que compruebe si el código que se quiere mergear cumple con las guidelines, si hay problemas de seguridad evidentes o si falta cobertura de tests.
  • Generación de tests: dado un handler o un endpoint, que genere los tests unitarios y de integración correspondientes siguiendo la estructura de tests/.
  • Revisión de seguridad: detección de problemas como inyecciones SQL, exposición de datos sensibles en logs, endpoints sin autorización, etc.
  • Refactor guiado: cuando detectas un problema de deuda técnica, pedirle que proponga el refactor respetando la arquitectura del proyecto.

Paso 4: Establecer reglas de comportamiento en Claude Code
#

Claude Code permite definir reglas persistentes que el agente aplica en todas las conversaciones del proyecto, dentro del propio CLAUDE.md. Por ejemplo:

## Reglas para el agente

- Usa siempre Minimal APIs, nunca Controllers.
- Los tests de integración deben usar Testcontainers, nunca mocks de base de datos.
- Aplica el patrón Vertical Slice: cada feature va en su propia carpeta dentro de Application/.
- No añadas librerías nuevas sin consultarlo primero.
- Si detectas código sin tests, indícalo pero no los generes salvo que se te pida explícitamente.

Estas reglas hacen que el agente trabaje dentro de los límites que el equipo ha establecido, reduciendo drásticamente el ruido en las revisiones.

Paso 5: Mantener el contexto vivo
#

El CLAUDE.md tiene el mismo problema que cualquier documentación: si nadie lo cuida, queda obsoleto. Y un contexto obsoleto es casi peor que no tener ninguno, porque el agente genera código confiado en información incorrecta.

Algunas prácticas que funcionan bien:

  • Trátalo como código: el CLAUDE.md vive en el repositorio y pasa por revisión en la PR igual que cualquier otro fichero. Si se cambia la arquitectura, la PR que introduce ese cambio también actualiza el contexto.
  • Úsalo como señal: cada vez que el agente comete un error por falta de contexto, es una tarea pendiente en el fichero. No lo corrijas solo en el chat; escríbelo.
  • No lo dejes crecer sin control: un CLAUDE.md de 500 líneas es tan inútil como uno vacío. Si crece demasiado, mueve el detalle a docs/ y deja en el fichero principal solo lo esencial. El agente necesita contexto accionable, no una novela.

Ejemplo real: añadir un endpoint en la API de DragonBall
#

Vamos a ver la diferencia en la práctica. El código completo está disponible en GitHub. Tenemos una API de personajes de DragonBall con esta estructura:

src/
├── Api/
│   ├── Program.cs
│   └── Characters/
│       └── GetCharactersEndpoint.cs
├── Application/
│   └── Characters/
│       └── GetCharacters/
│           ├── GetCharactersQuery.cs
│           └── GetCharactersHandler.cs
├── Domain/
│   └── Characters/
│       └── Character.cs
tests/
└── DragonBall.Tests/
    └── Integration/
        └── GetCharactersEndpointTests.cs

Le pedimos a Claude: “Añade el endpoint POST /characters para crear un personaje”.

Sin contexto: lo que hace Claude por defecto
#

Sin CLAUDE.md ni documentación de guidelines, Claude genera algo así:

// CharactersController.cs — ¡Controllers! Nadie del equipo usa esto
[ApiController]
[Route("api/[controller]")]
public class CharactersController : ControllerBase
{
    private readonly AppDbContext _db;

    public CharactersController(AppDbContext db)
    {
        _db = db;
    }

    [HttpPost]
    public async Task<IActionResult> CreateCharacter([FromBody] CreateCharacterDto dto)
    {
        var character = new Character { Name = dto.Name, Race = dto.Race };
        _db.Characters.Add(character);
        await _db.SaveChangesAsync();
        return Ok(character);
    }
}

Problemas evidentes: Controller en lugar de Minimal API, sin MinApiLib, sin FluentValidation, sin handler independiente, sin tests. Un revisor lo rechazaría en la PR.

Con contexto: lo que hace Claude conociendo el proyecto
#

Con el CLAUDE.md en su lugar, Claude genera la feature completa siguiendo la estructura Vertical Slice y las convenciones de MinApiLib que ya existen en el proyecto:

src/Api/Characters/CreateCharacterEndpoint.cs

using Application.Characters.CreateCharacter;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using MinApiLib.Endpoints;

namespace Api.Characters;

public record CreateCharacter() : Post("/characters")
{
    public async Task<IResult> HandleAsync(
        [FromBody] CreateCharacterRequest body,
        [FromServices] CreateCharacterHandler handler,
        CancellationToken cancellationToken)
    {
        try
        {
            var character = await handler.Handle(
                new CreateCharacterCommand(body.Name, body.Race, body.PowerLevel, body.Affiliation),
                cancellationToken);

            return Results.Created($"/characters/{character.Id}", character);
        }
        catch (ValidationException ex)
        {
            var errors = ex.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(e => e.ErrorMessage).ToArray());

            return Results.ValidationProblem(errors, statusCode: StatusCodes.Status422UnprocessableEntity);
        }
    }
}

public sealed record CreateCharacterRequest(
    string Name,
    string Race,
    int PowerLevel,
    string Affiliation);

src/Application/Characters/CreateCharacter/CreateCharacterCommand.cs

namespace Application.Characters.CreateCharacter;

public sealed record CreateCharacterCommand(
    string Name,
    string Race,
    int PowerLevel,
    string Affiliation);

src/Application/Characters/CreateCharacter/CreateCharacterHandler.cs

using Domain.Characters;
using FluentValidation;

namespace Application.Characters.CreateCharacter;

public sealed class CreateCharacterHandler(
    ICharacterRepository repository,
    IValidator<CreateCharacterCommand> validator)
{
    public async Task<Character> Handle(CreateCharacterCommand command, CancellationToken cancellationToken)
    {
        await validator.ValidateAndThrowAsync(command, cancellationToken);

        var character = new Character
        {
            Name = command.Name,
            Race = command.Race,
            PowerLevel = command.PowerLevel,
            Affiliation = command.Affiliation
        };

        return repository.Add(character);
    }
}

src/Application/Characters/CreateCharacter/CreateCharacterValidator.cs

using FluentValidation;

namespace Application.Characters.CreateCharacter;

public sealed class CreateCharacterValidator : AbstractValidator<CreateCharacterCommand>
{
    public CreateCharacterValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("El nombre es obligatorio.")
            .MaximumLength(100).WithMessage("El nombre no puede superar los 100 caracteres.");

        RuleFor(x => x.Race)
            .NotEmpty().WithMessage("La raza es obligatoria.")
            .MaximumLength(50).WithMessage("La raza no puede superar los 50 caracteres.");

        RuleFor(x => x.PowerLevel)
            .GreaterThan(0).WithMessage("El nivel de poder debe ser mayor que 0.");

        RuleFor(x => x.Affiliation)
            .NotEmpty().WithMessage("La afiliación es obligatoria.")
            .MaximumLength(100).WithMessage("La afiliación no puede superar los 100 caracteres.");
    }
}

tests/DragonBall.Tests/Integration/CreateCharacterEndpointTests.cs

using System.Net;
using System.Net.Http.Json;
using Domain.Characters;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;

namespace DragonBall.Tests.Integration;

public sealed class CreateCharacterEndpointTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async Task POST_characters_with_valid_body_returns_201()
    {
        var body = new { Name = "Bardock", Race = "Saiyan", PowerLevel = 10000, Affiliation = "None" };

        var response = await _client.PostAsJsonAsync("/characters", body);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
    }

    [Fact]
    public async Task POST_characters_returns_created_character_with_id()
    {
        var body = new { Name = "Bardock", Race = "Saiyan", PowerLevel = 10000, Affiliation = "None" };

        var response = await _client.PostAsJsonAsync("/characters", body);
        var character = await response.Content.ReadFromJsonAsync<Character>();

        character.Should().NotBeNull();
        character!.Id.Should().BeGreaterThan(0);
        character.Name.Should().Be("Bardock");
    }

    [Fact]
    public async Task POST_characters_with_empty_name_returns_422()
    {
        var body = new { Name = "", Race = "Saiyan", PowerLevel = 9000, Affiliation = "Z Fighters" };

        var response = await _client.PostAsJsonAsync("/characters", body);

        response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
    }

    [Fact]
    public async Task POST_characters_with_zero_power_level_returns_422()
    {
        var body = new { Name = "Goku", Race = "Saiyan", PowerLevel = 0, Affiliation = "Z Fighters" };

        var response = await _client.PostAsJsonAsync("/characters", body);

        response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
    }
}

La diferencia es brutal. El segundo resultado está listo para pasar la revisión de PR: usa MinApiLib con el patrón de record que ya tiene el proyecto, separa el handler y el validador en su propia carpeta de vertical slice, incluye validación con FluentValidation devolviendo 422 correctamente, y tiene los tests de integración con WebApplicationFactory apuntando a la app real.

Todo esto con el mismo prompt. La única diferencia es el contexto que le hemos dado al agente.

Dicho esto, el contexto reduce los errores pero no los elimina. El agente puede alucinar aunque tenga un CLAUDE.md perfecto: introducir un using que no existe, saltarse una convención que creías bien documentada o generar un test que compila pero no prueba lo que debería. Por eso los tests no son opcionales: son la red de seguridad que te permite delegar con confianza. Si el agente genera código y los tests pasan, tienes una garantía real. Si no hay tests, solo tienes fe.

Cuando el agente se equivoca de forma recurrente en lo mismo, no lo corrijas solo en el chat: añade esa corrección al CLAUDE.md o a los docs. Así el error ocurre una vez, no cien.

Conclusión
#

Adoptar la IA en un equipo de desarrollo no es instalar una extensión y olvidarse. Requiere el mismo esfuerzo que incorporar a un ingeniero nuevo: darle contexto, explicarle cómo trabajamos y definir qué se espera de él. La diferencia es que, una vez que tiene esa información, escala infinitamente mejor.

En el próximo post hablaré de cómo continuar con otras partes del ciclo de desarrollo donde realmente podamos ver el impacto de la IA.