De meeste demo’s liegen

Niet expres. Maar ze laten je zien hoe Claude Code een functie schrijft, een test genereert, klaar. Applaus.

Echt werk ziet er anders uit. Je hebt een ticket met vage acceptatiecriteria. Een bestaande codebase met patronen die je moet volgen. Een database die gemigreerd moet worden. Edge cases waar niemand aan gedacht heeft. En een PR die door je collega’s heen moet.

Dit is het verhaal van hoe ik een complete feature bouw met Claude Code. Van ticket tot productie. Elke stap, elke prompt, elke koerscorrectie.


Het ticket

Het issue is simpel genoeg:

USER-234: Notification Preferences API

Als gebruiker wil ik mijn notificatievoorkeuren kunnen beheren, zodat ik kan kiezen welke meldingen ik ontvang via e-mail, push of helemaal niet.

Acceptatiecriteria:

  • GET/PUT endpoints voor notificatievoorkeuren per gebruiker
  • Categorieën: marketing, security, updates, billing
  • Kanalen: email, push, none
  • Standaardwaarden bij nieuwe gebruikers
  • Validatie op ongeldige categorieën/kanalen

Niet rocket science. Maar wel het soort ticket waar je makkelijk twee uur aan kwijt bent — datamodel, migratie, endpoints, validatie, tests, PR. Precies het soort werk waar Claude Code schittert.


Fase 1: Begrijpen en plannen

Ik begin niet met “schrijf de code.” Ik begin met context.

Lees de codebase en begrijp hoe we features bouwen. 
Kijk specifiek naar:
- Hoe onze API-endpoints gestructureerd zijn
- Welke patronen we gebruiken voor EF Core entities
- Hoe validatie werkt
- De teststructuur

Stel daarna een plan voor om een Notification Preferences API te bouwen 
(GET/PUT per gebruiker, categorieën: marketing/security/updates/billing, 
kanalen: email/push/none).

Claude Code leest de projectstructuur, bekijkt bestaande endpoints, entities en tests. Na een halve minuut komt het terug met een plan in zes stappen.

Het plan is grotendeels goed, maar het stelt voor om een aparte NotificationService te maken. Wij gebruiken geen service-laag — onze handlers zitten direct in de endpoint-bestanden met MediatR. Eerste koerscorrectie:

Goed plan, maar we gebruiken geen aparte service classes. 
Alle logica gaat via MediatR handlers. Pas het plan aan.

Dit is precies waarom een goede CLAUDE.md cruciaal is. Als ik daar had vastgelegd “geen service-laag, gebruik MediatR handlers” had Claude Code het direct goed gedaan. Notitie voor mezelf: toevoegen na dit ticket.


Fase 2: Datamodel en migratie

Maak het EF Core datamodel voor NotificationPreference. 
Volg het patroon van onze bestaande entities. 
Voeg het toe aan de DbContext en maak een migratie aan.

Claude Code maakt de entity aan:

public class NotificationPreference
{
    public int Id { get; set; }
    public string UserId { get; set; } = string.Empty;
    public NotificationCategory Category { get; set; }
    public NotificationChannel Channel { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    public User User { get; set; } = null!;
}

public enum NotificationCategory
{
    Marketing,
    Security,
    Updates,
    Billing
}

public enum NotificationChannel
{
    Email,
    Push,
    None
}

Netjes. Het heeft naar onze bestaande entities gekeken en dezelfde conventies gevolgd: CreatedAt/UpdatedAt timestamps, navigation property, string.Empty als default.

De migratie draait het ook in één keer goed. Een unieke index op UserId + Category — zodat een gebruiker per categorie maar één voorkeur heeft. Dat had ik niet expliciet gevraagd, maar het is precies de juiste constraint.


Fase 3: API-endpoints

Maak de GET en PUT endpoints voor notification preferences. 
GET retourneert alle voorkeuren voor een gebruiker. 
PUT update een specifieke categorie. 
Gebruik ons standaard endpoint-patroon met MediatR.

Hier ging het niet helemaal goed de eerste keer. Claude Code maakte een PUT /api/users/{userId}/notifications/{category} endpoint, maar in ons project gebruiken we de user ID uit de JWT-claim — je stuurt die niet mee in de URL. Tweede koerscorrectie:

De userId komt uit de authenticated user (HttpContext.User), 
niet uit de URL. Pas beide endpoints aan.

Na de correctie ziet het er goed uit. De GET endpoint retourneert een lijst van alle voorkeuren, inclusief standaardwaarden voor categorieën die de gebruiker nog niet heeft ingesteld:

app.MapGet("/api/notifications/preferences", async (
    IMediator mediator,
    HttpContext context) =>
{
    var userId = context.User.GetUserId();
    var result = await mediator.Send(new GetNotificationPreferencesQuery(userId));
    return Results.Ok(result);
})
.RequireAuthorization()
.WithTags("Notifications");

De handler bevat de logica om ontbrekende categorieën aan te vullen met de standaardwaarde (Email). Dat is slim — zo krijgt de frontend altijd een compleet overzicht.


Fase 4: Tests

Schrijf xUnit tests voor beide endpoints. 
Test het happy path en deze edge cases:
- Gebruiker zonder bestaande voorkeuren (standaardwaarden)
- Ongeldige categorie
- Ongeldige channel
- Update van een voorkeur die nog niet bestaat (upsert)

Claude Code schrijft zeven tests. De meeste zijn direct goed. Maar de test voor “ongeldige categorie” stuurt een string "invalid" naar de enum-parameter, en dat levert een andere foutmelding op dan verwacht — het model binding-systeem vangt het eerder af dan onze validatie.

Derde koerscorrectie:

De test voor ongeldige categorie faalt. Het model binding systeem 
geeft een 400 terug voordat onze validatie bereikt wordt. 
Pas de test aan zodat die het werkelijke gedrag test.

Claude Code past de assertion aan naar de daadwerkelijke 400 Bad Request response van het framework. Het voegt ook een opmerking toe in de test die uitlegt waarom de foutmelding van ASP.NET Core komt en niet van onze eigen validatie. Nuttige documentatie voor toekomstige developers.


Fase 5: Review en bijschaven

Nu het spannende deel. Ik laat Claude Code zijn eigen werk reviewen:

Review alle code die je zojuist geschreven hebt. 
Kijk naar: ontbrekende validatie, mogelijke null reference issues, 
of we iets vergeten zijn, en of de code consistent is met de rest 
van het project.

Het vindt twee dingen:

  1. De UpdatedAt timestamp wordt niet gezet bij een PUT. Gemist.
  2. Er ontbreekt een CancellationToken doorgifte in de MediatR handlers.

Beiden terechte punten. Claude Code fixt ze direct. Dit self-review patroon gebruik ik inmiddels standaard — het vangt vrijwel altijd iets op dat je in de flow gemist hebt.


Fase 6: PR en documentatie

Maak een feature branch aan, commit alle wijzigingen met een 
duidelijke commit message, en genereer een PR-beschrijving die 
uitlegt wat er gebouwd is en hoe je het kunt testen.

Claude Code maakt de branch feature/user-notification-preferences, commit met een gestructureerde message, en genereert een PR-beschrijving met:

  • Samenvatting van de wijzigingen
  • Overzicht van de nieuwe endpoints met request/response voorbeelden
  • Testinstructies
  • Database migratie-instructies

De PR-beschrijving is beter dan wat ik zelf zou schrijven op een vrijdagmiddag. Eerlijk is eerlijk.


Wat ik geleerd heb

Context vooraf geven bespaart koerscorrecties. De drie correcties die ik moest maken (geen service-laag, userId uit claims, enum binding-gedrag) — twee daarvan had ik kunnen voorkomen met een betere CLAUDE.md.

Het self-review patroon werkt. Claude Code vindt echte issues in zijn eigen code. Het is geen vervanging voor een code review door een collega, maar het vangt de mechanische fouten die je anders in de PR-review tegenkomt.

Klein beginnen, stap voor stap. De verleiding is groot om te zeggen: “bouw deze hele feature.” Maar dan mis je de mogelijkheid om bij te sturen. Zes gerichte prompts geven een beter resultaat dan één vage opdracht.

De totale tijd? Ongeveer 25 minuten van ticket tot PR. Handmatig zou dit minstens twee uur zijn. En het resultaat is consistenter — Claude Code vergeet niet om timestamps bij te werken of cancellation tokens door te geven (na de review, tenminste).


De prompt-volgorde

Voor als je dit zelf wilt proberen. Dit zijn de zes prompts die ik gebruikt heb, in volgorde:

  1. Begrijp de codebase — “Lees de codebase en begrijp hoe we features bouwen. Stel een plan voor.”
  2. Koerscorrectie — “We gebruiken geen service classes. Pas het plan aan.”
  3. Datamodel — “Maak het EF Core datamodel, voeg toe aan DbContext, maak migratie.”
  4. Endpoints — “Maak GET en PUT endpoints met ons standaard patroon.”
  5. Koerscorrectie — “userId komt uit de authenticated user, niet de URL.”
  6. Tests — “Schrijf xUnit tests voor happy path en edge cases.”
  7. Koerscorrectie — “Test faalt door model binding. Pas de assertion aan.”
  8. Self-review — “Review alle code op ontbrekende validatie en consistentie.”
  9. PR — “Maak branch, commit, en genereer PR-beschrijving.”

Negen prompts in totaal. Drie daarvan zijn koerscorrecties. Dat is normaal — het is geen falen van de tool, het is hoe je samenwerkt met een AI. Je stuurt bij, net zoals je een junior developer zou bijsturen. Alleen gaat het tien keer sneller.


Probeer het zelf

Pak een ticket van je backlog. Niet het makkelijkste, niet het moeilijkste — eentje met een database-wijziging, een paar endpoints en tests. Volg deze prompt-volgorde en kijk hoe ver je komt.

Het enige wat je nodig hebt: een goede CLAUDE.md die je projectconventies beschrijft. Als je die nog niet hebt, begin daar. Het bespaart je meer koerscorrecties dan je denkt.