🚀 Scaling Tailwind with Angular CVA: Why Utility Sprawl Kills Design Systems

“Tailwind becomes exponentially more maintainable when styling decisions become architecture — not scattered utility strings.”

The Problem No One Talks About

You start an Angular project. You reach for Tailwind. Within days, you’re shipping polished UIs at a pace that feels genuinely productive.

Then the codebase grows.

Three months in, your button component looks like this:

<button
  class="px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600
         text-white font-semibold text-sm focus:ring-2
         focus:ring-blue-300 disabled:opacity-50
         disabled:cursor-not-allowed">
  <ng-content />
</button>

Fine. Manageable. You ship it.

Six months later, that same string — or a close variation of it — exists across 40 different components. Each one slightly different. None of them the canonical source of truth.

A designer asks you to change the primary button radius from rounded-lg to rounded-xl.

You open your editor, run a search, and find 43 results across 31 files.

That is not a Tailwind problem.

That is a design-system architecture problem.

What Utility Sprawl Actually Costs

The conversation around Tailwind scalability usually stops at “it’s verbose.” That misses the real issue entirely.

Utility sprawl is expensive in ways that compound quietly across sprints:

1. Token Changes Become Surgical Operations

In scalable frontend systems, design tokens — the foundational decisions about color, spacing, and scale — change. Brand refreshes happen. Accessibility audits require contrast updates. Product pivots shift the visual language.

When styling decisions live scattered across templates, a single token change becomes a multi-file refactor. At scale, this means:

  • Higher error risk. Engineers miss instances. Visual inconsistency appears.
  • Longer review cycles. PRs touching 30+ files for a color change create review fatigue.
  • Drift accumulation. Not every instance gets updated. The codebase diverges from the design system.
    ### 2. Onboarding Overhead Grows with the Codebase

A new engineer joins your team. They need to add a button.

In a utility-sprawl codebase, they either copy-paste an existing class string (propagating the sprawl) or invent their own combination (introducing a new variant that doesn’t align with the design system). There is no readable contract that says “here are the valid button states.”

CVA solves this with a single readable API.

3. Cognitive Load Scales With Duplication

Every time a developer encounters a 14-class utility string, they must mentally decode the intent. What does this component communicate? Is it primary? Destructive? A ghost button?

None of that is readable from utility classes. The intent is buried in the implementation.

4. UI Inconsistency Becomes Invisible

The most dangerous aspect of utility sprawl is that the inconsistency it creates is invisible in isolation. A button with rounded-lg and a button with rounded-md look nearly identical at a glance. Over time, these micro-divergences accumulate into a UI that feels slightly off — without anyone being able to point to exactly why.

Understanding CVA: Class Variance Authority

CVA is a small TypeScript utility that introduces variant-driven styling into your component architecture. It doesn’t replace Tailwind. It gives Tailwind structure.

The core idea: instead of building components by concatenating utility strings, you define a variant map — a composable API that encodes all valid visual states of a component.

import { cva } from 'class-variance-authority';

export const buttonVariants = cva(
  // Base classes — always applied
  'inline-flex items-center justify-center rounded-lg font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary:     'bg-blue-600 text-white hover:bg-blue-700',
        secondary:   'bg-gray-100 text-gray-900 hover:bg-gray-200',
        ghost:       'hover:bg-gray-100 text-gray-700',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
        outline:     'border border-gray-300 bg-transparent hover:bg-gray-50 text-gray-700',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
        xl: 'h-14 px-8 text-lg',
      },
      fullWidth: {
        true: 'w-full',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

Now any component — or any engineer — that needs a button doesn’t need to know Tailwind. They need to know the variant API:

buttonVariants({ variant: 'primary', size: 'lg' })
// → 'inline-flex items-center ... bg-blue-600 text-white h-12 px-6 text-base'

buttonVariants({ variant: 'ghost', size: 'sm' })
// → 'inline-flex items-center ... hover:bg-gray-100 text-gray-700 h-8 px-3 text-sm'

That is one source of truth. One place to update. One contract the entire team can read.

Integrating CVA with Angular Standalone Components

Angular’s standalone component architecture is the right primitive for design systems. Paired with CVA and @HostBinding, components become clean, declarative visual contracts.

The Button Component

// button.component.ts
import { Component, Input, HostBinding } from '@angular/core';
import { VariantProps } from 'class-variance-authority';
import { buttonVariants } from './button.variants';

type ButtonVariants = VariantProps<typeof buttonVariants>;

@Component({
  selector: 'app-button',
  standalone: true,
  template: `<ng-content />`,
})
export class ButtonComponent {
  @Input() variant: ButtonVariants['variant'] = 'primary';
  @Input() size: ButtonVariants['size'] = 'md';
  @Input() fullWidth: ButtonVariants['fullWidth'] = false;

  @HostBinding('class')
  get classes(): string {
    return buttonVariants({
      variant: this.variant,
      size: this.size,
      fullWidth: this.fullWidth,
    });
  }
}
<!-- Usage in any template -->
<app-button variant="primary" size="lg">Submit</app-button>
<app-button variant="ghost" size="sm">Cancel</app-button>
<app-button variant="destructive" size="md">Delete Account</app-button>

Notice what this achieves:

  • No utility classes in templates. The template communicates intent, not implementation.
  • Type-safe variants. VariantProps infers valid values directly from the CVA definition. Invalid variants fail at compile time.
  • Single update point. Changing the primary button style means editing one object, not 40 files.

  • Self-documenting API. A new engineer reads variant="destructive" and understands what the component communicates without needing to decode class strings.

Extending the Pattern: Building a UI System

CVA’s real power emerges when you apply it consistently across a component library. Consider how this scales to an input component:

// input.variants.ts
import { cva } from 'class-variance-authority';

export const inputVariants = cva(
  'flex w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
  {
    variants: {
      state: {
        default: 'border-gray-300 focus-visible:ring-blue-500',
        error:   'border-red-500 focus-visible:ring-red-500 text-red-900',
        success: 'border-green-500 focus-visible:ring-green-500',
      },
      size: {
        sm: 'h-8 text-xs',
        md: 'h-10 text-sm',
        lg: 'h-12 text-base',
      },
    },
    defaultVariants: {
      state: 'default',
      size: 'md',
    },
  }
);

And a badge component:

// badge.variants.ts
import { cva } from 'class-variance-authority';

export const badgeVariants = cva(
  'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
  {
    variants: {
      intent: {
        default:     'bg-gray-100 text-gray-800',
        primary:     'bg-blue-100 text-blue-800',
        success:     'bg-green-100 text-green-800',
        warning:     'bg-yellow-100 text-yellow-800',
        destructive: 'bg-red-100 text-red-800',
      },
      outline: {
        true: 'bg-transparent border',
      },
    },
    defaultVariants: {
      intent: 'default',
    },
  }
);

Now your design system is variant-consistent by architecture. Every component speaks the same language: variant, size, intent, state. New engineers learn the pattern once and apply it everywhere.

Setting Up Design Tokens

CVA works best when your Tailwind configuration is token-driven. Custom tokens ensure that your CVA variants reference semantic values — not raw Tailwind utility names.

// tailwind.config.ts
import type { Config } from 'tailwindcss';

export default {
  content: ['./src/**/*.{html,ts}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50:  '#EEF1F9',
          100: '#C7D0EE',
          500: '#283A8F',  // Navy — primary
          600: '#1E2A6B',  // Deep Navy
          700: '#16206A',
        },
        accent: {
          coral:  '#FF6B35',
          teal:   '#00C4B4',
          gold:   '#FFB800',
          purple: '#8B5CF6',
        },
      },
      borderRadius: {
        'component': '0.5rem',
        'card':      '0.75rem',
        'panel':     '1rem',
      },
      fontSize: {
        'label-sm': ['0.75rem', { lineHeight: '1rem',    letterSpacing: '0.05em' }],
        'label-md': ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.025em' }],
      },
    },
  },
} satisfies Config;

Now your CVA variants reference design tokens:

primary: 'bg-brand-500 text-white hover:bg-brand-600',
// instead of
primary: 'bg-blue-600 text-white hover:bg-blue-700',

A brand color change becomes a single edit in tailwind.config.ts. Every component updates automatically.

The cn() Utility: Handling Class Merging

One practical consideration with CVA and Tailwind: class conflicts. If a consumer needs to override a variant’s base styles, naive class concatenation creates conflicts that Tailwind can’t resolve predictably.

The solution is tailwind-merge combined with clsx:

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}
// button.component.ts — with class override support
import { cn } from '../lib/utils';

@HostBinding('class')
get classes(): string {
  return cn(
    buttonVariants({ variant: this.variant, size: this.size }),
    this.class  // consumer-provided override class
  );
}

Now consumers can safely extend component styles without class conflicts:

<!-- Adds margin without breaking the base variant -->
<app-button variant="primary" size="lg" class="mt-4 w-full">
  Submit
</app-button>

Compound Variants: Handling Complex State Logic

CVA supports compound variants — styling rules that only apply when multiple variants are combined. This is essential for components with complex visual logic:

export const buttonVariants = cva(
  'inline-flex items-center justify-center ...',
  {
    variants: {
      variant: {
        primary:   'bg-blue-600 text-white',
        outline:   'border border-gray-300 bg-transparent',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        lg: 'h-12 px-6 text-base',
      },
      loading: {
        true: 'cursor-wait',
      },
    },
    compoundVariants: [
      // When loading + primary: dim the background specifically
      {
        variant: 'primary',
        loading: true,
        class: 'bg-blue-400 hover:bg-blue-400',
      },
      // When loading + outline: special border treatment
      {
        variant: 'outline',
        loading: true,
        class: 'border-gray-200 text-gray-400',
      },
    ],
    defaultVariants: {
      variant: 'primary',
      size: 'md',
      loading: false,
    },
  }
);

And the Angular component:

@Component({
  selector: 'app-button',
  standalone: true,
  template: `
    @if (loading) {
      <span class="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
    }
    <ng-content />
  `,
})
export class ButtonComponent {
  @Input() variant: ButtonVariants['variant'] = 'primary';
  @Input() size: ButtonVariants['size'] = 'md';
  @Input() loading: ButtonVariants['loading'] = false;

  @HostBinding('class')
  get classes(): string {
    return buttonVariants({
      variant: this.variant,
      size: this.size,
      loading: this.loading,
    });
  }
}

Applying CVA to a Complete Angular UI Library

Here is how a production Angular design system organises CVA-powered components:

src/
└── lib/
    ├── utils.ts                    ← cn() utility
    ├── tokens/
    │   └── tailwind.config.ts      ← Design tokens
    ├── button/
    │   ├── button.variants.ts      ← CVA definition
    │   ├── button.component.ts     ← Angular component
    │   └── button.component.spec.ts
    ├── input/
    │   ├── input.variants.ts
    │   └── input.component.ts
    ├── badge/
    │   ├── badge.variants.ts
    │   └── badge.component.ts
    ├── card/
    │   ├── card.variants.ts
    │   └── card.component.ts
    └── index.ts                    ← Public API

The index.ts public API exports both the Angular components and the variant functions — allowing consumers to use the variant logic independently when building composed components:

// lib/index.ts
export { ButtonComponent } from './button/button.component';
export { buttonVariants } from './button/button.variants';

export { InputComponent } from './input/input.component';
export { inputVariants } from './input/input.variants';

export { BadgeComponent } from './badge/badge.component';
export { badgeVariants } from './badge/badge.variants';

export { cn } from './utils';

The Enterprise Perspective: What This Changes at Scale

In production Angular systems, the architectural benefits of CVA compound over time. Here is what changes concretely as the component library grows:

Design Governance Becomes Possible

When styling decisions live in CVA variant definitions, design reviews become focused discussions about the variant API — not line-by-line Tailwind class audits. A PR that adds a brand variant to buttonVariants is reviewable in 30 seconds. A PR that changes bg-blue-600 across 40 templates is a review liability.

Junior–Senior Collaboration Improves

A junior engineer working in a CVA-powered system cannot easily introduce an off-system button variant. They use app-button with a documented variant input. The component API is the guardrail. The design system governs itself.

This is meaningfully different from a utility-first approach, where every engineer makes independent styling decisions on every component — and the design system exists only in a Figma file no one has time to consult.

Refactoring Has a Known Cost

In a CVA-powered library, the cost of a visual change is always predictable: edit one variant definition, verify the component, ship. At 500 components, the cost of changing the primary button color is still one edit. In a utility-sprawl codebase, the cost of that same change scales with the component count.

Onboarding Shrinks to the Variant API

New engineers learn the component library by reading the variant definitions. The CVA source is the documentation. There is no gap between “what the design system says the button should look like” and “what the code actually does” — they are the same artefact.

Common Objections — Addressed Directly

“This adds a dependency for something Tailwind already handles.”

Tailwind handles utility composition. CVA handles variant architecture. These are different problems. Tailwind’s @apply directive can consolidate classes, but it does not give you typed variant APIs, compound variant logic, or a composable styling contract. CVA is the missing architectural layer.

“We can solve this with CSS custom properties and Angular themes.”

Absolutely valid — and orthogonal. Design tokens via CSS custom properties is a complementary pattern, not a competing one. CVA governs which classes are applied per variant. Design tokens govern what values those classes resolve to. They work together: tokens define your brand, CVA defines your components.

“We can just create services or constants that export class strings.”

Some teams do this. It works at small scale. The problem is that plain string constants don’t give you compound variant logic, type-safe APIs, defaultVariants, or the cn() merge behaviour. CVA is purpose-built for this problem — reinventing it manually produces a less capable, harder-to-maintain version of the same thing.

“Tailwind’s component extraction already handles this.”

Tailwind recommends component extraction (putting components behind a component class or a template partial) as the solution to utility repetition. For simple, static components, this is sufficient. For components with multiple interactive states, loading states, size variants, intent variants, and compound behaviours — CVA provides the typed, composable API that component extraction alone does not.

Migration Strategy: Moving an Existing Codebase to CVA

Migrating a large Angular codebase to CVA does not require a big-bang refactor. The approach that works in production:

Phase 1 — Audit (1–2 weeks)

Identify your highest-frequency components: buttons, inputs, badges, form controls. Search the codebase for the most commonly duplicated class strings. These are your highest-ROI migration targets.

Phase 2 — Define Variants for Core Primitives (1–2 weeks)

Start with the button. Create button.variants.ts and ButtonComponent. Run both in parallel — the new CVA-powered component and the legacy template-based markup. Do not deprecate yet.

Phase 3 — Incremental Component Migration (ongoing)

Migrate new component work to CVA by default. When touching existing components, migrate them as part of the task — not as a separate refactor ticket. This amortises the migration cost across normal feature development.

Phase 4 — Deprecate Legacy Patterns

Once the CVA components reach coverage, mark legacy template-based button usage with TypeScript deprecation comments. Set a deprecation deadline. The codebase self-heals over the next quarter.

Phase 5 — Variant Governance

Add variant review to your design-system PR process. Any new variant to an existing component requires a discussion: does this variant belong in the system, or is it a one-off that should be handled differently? This is where the real architectural dividend appears.

Key Takeaways

Let me be direct about what this pattern does and does not solve.

CVA does not fix:

  • Tailwind purging issues
  • Bundle size concerns
  • Runtime performance
  • CSS specificity conflicts in complex layouts
    CVA does fix:
  • Utility duplication across components
  • Inconsistent visual variants without a source of truth
  • Onboarding friction from template-level styling decisions
  • The cost of design token changes at scale
  • The gap between “what the design system specifies” and “what the code implements”
    One recurring issue in UI libraries is that styling architecture is treated as a secondary concern — something to be addressed later, after features ship. In production Angular applications, later arrives faster than expected. At 200 components, the maintenance cost of utility sprawl is already significant. At 500, it becomes a blocker.

Styling decisions should live in systems — not templates.

CVA is not a silver bullet. It is an architectural pattern that makes your Tailwind-based Angular application behave like a design system rather than a collection of independently styled components.

The difference compounds at scale.

Resources

— reference implementation of CVA in a production component library

Discussion

In many teams, the Tailwind migration debate happens at the component level — “should we switch to CSS Modules?” — when the real blocker isn’t utility classes at all. It’s the absence of a variant architecture.

What’s the biggest UI scalability challenge in your current Angular application?

Drop it in the comments. I read every reply and try to give a specific architectural suggestion based on your actual constraints.

📌 More From Me
I share daily insights on web development, architecture, and frontend ecosystems.
Follow me here on Dev.to, and connect on LinkedIn for professional discussions.

🌐 Connect With Me
If you enjoyed this post and want more insights on scalable frontend systems, follow my work across platforms:

🔗 LinkedIn — Professional discussions, architecture breakdowns, and engineering insights.
📸 Instagram — Visuals, carousels, and design‑driven posts under the Terminal Elite aesthetic.
🧠 Website — Articles, tutorials, and project showcases.
🎥 YouTube — Deep‑dive videos and live coding sessions.

Why interoperability failures are a product problem — and how to stop firefighting later

I’ve spent the last five years arguing with testers, architects, and notified-body assessors about why a device “doesn’t work with the hospital network”. To be fair, network oddities happen — but in almost every case I’ve seen the root cause was a design and compliance gap we could have prevented.

Interoperability isn’t just an engineering exercise. Under the MDR it sits squarely inside your device’s safety and performance case (see Annex I, General safety and performance requirements). Practically, that means connectivity decisions belong in your Technical File from day one, not added as an afterthought at verification.

Why interoperability is more than APIs

When people say “interoperability” they usually mean three things at once:

  • Data formats and semantics (HL7/FHIR, DICOM, device-specific payloads)
  • Networking and transport (Wi‑Fi, BLE, TCP/IP, gateways)
  • Operational behaviours (state machine, timeouts, retries, firmware updates)

A failure in any of these domains can become a safety issue. For example, a delayed message or a dropped firmware update can change device behaviour or clinical decisions. That immediately pulls ISO 14971 risk management and IEC 62304 software lifecycle workstreams into the picture.

In practice this means you must treat interoperability as a full risk item:

  • Define intended connectivity scenarios in your intended use and user profiles
  • Map hazards that specifically arise from network interactions
  • Include mitigations for loss of connectivity, corrupted messages, and incompatible versions

What regulators and notified bodies actually look for

Across different notified bodies the questions vary, but the theme is consistent: “How do you know this will behave safely in the real environment?” Expect to show:

  • A clear set of intended interoperability use cases in your IFU/labeling (Annex I)
  • Traceable risk controls for each connectivity hazard (ISO 14971)
  • Software architecture documentation showing how communication stacks are isolated and managed (IEC 62304)
  • Cybersecurity risk assessment and patch/upgrade strategy (refer to MDCG guidance and IEC 81001‑5‑1 principles)
  • Test matrices that include negative tests, flaky network tests, and version interoperability tests

Don’t be surprised if assessors ask for evidence of testing in “production-like” networks. Lab-only tests that assume perfect networks rarely satisfy them.

A practical checklist for your Technical File

I keep the following checklist in every connected-device Technical File. It’s concise enough for a reviewer and exhaustive enough for an audit.

Use-case and architecture

  • Intended connectivity scenarios and user stories
  • Network topologies considered (direct, via gateway, cloud)
  • Software/SW-architecture diagram indicating communication layers

Risk and safety

  • ISO 14971 hazard analysis items explicitly tying risk to connectivity
  • Failure modes (e.g., message loss, replay, man-in-the-middle)
  • Residual risk and rationale

Software & cybersecurity

  • IEC 62304 software classification and V-model traceability
  • Cybersecurity risk management file (threat model, mitigations, verification)
  • Patch/upgrade policy and rollback strategy

Verification & validation

  • Interoperability test matrix (protocols, versions, edge cases)
  • Performance tests under degraded network conditions (latency, jitter)
  • Usability testing for connectivity-related tasks (pairing, reconnection) per IEC 62366-1

Post-market

  • PMCF/PMPF plan for connectivity incidents
  • PSUR/SSCP items that monitor interoperable/failure modes
  • Vulnerability disclosure and coordinated vulnerability response plan

How to operationalise this in your QMS

If your QMS treats connectivity as “just another feature”, you’ll pay in CAPAs later. Here’s what I change when I’m asked to harden a connected product:

  • Make connectivity a distinct design control stream. Link requirements to architecture, tests, and risk items in one place (connected workflow). This prevents orphaned requirements that live only in Jira tickets.
  • Use change impact analysis for network-related changes. A seemingly trivial firmware change can affect protocol behaviour — map that through design controls before release.
  • Formalise supplier assurance for third-party stacks (Bluetooth stacks, cloud SDKs). Ask for evidence: CVE monitoring, security development lifecycle artefacts, and maintenance windows.
  • Integrate incident handling with PMS. A security vulnerability can be a safety event; ensure your CAPA and vigilance triggers are aligned.
  • Make traceability visible. In an audit I want to see a single trace from requirement → risk control → test → release. That reduces follow-up evidence requests and lowers audit friction.

If you use an eQMS, make these items native: traceability links, change impact mapping, and a CAPA workflow that includes cybersecurity and interoperability artefacts. Controlled assistance (AI-guided summaries, for example) is useful to surface gaps, but ensure reviewability and audit trails; regulators want to see human oversight.

A few pitfalls I keep seeing

  • Treating “supported” interfaces as optional. If you ship with a GUI that exposes a connectivity setting, include it in usability and security testing.
  • Ignoring version interoperability. Backward compatibility testing often gets cut; that’s when upgrades brick integrations in the field.
  • Leaving vendor-supported cloud dependencies out of the Technical File. Cloud components that influence clinical behaviour are part of the device ecosystem.

Final thought

Interoperability and connectivity are systems problems, not feature checkboxes. Build them into your risk-management and software lifecycle artefacts from the outset, and your CAPA queue will thank you.

What connectivity failure have you seen in the field that could have been avoided with better design and traceability?

Formerly Known As

Keeping Up With The Renamed World

Tech changes fast.
Sometimes too fast.

One day you are confidently talking about a service, framework, tool, or company… and the next day it has a completely different name, a new logo, a rebrand announcement, and an updated pricing page. 😭

That is exactly why I am starting this series:

“Formerly Known As”

A blog series where I keep up with the former names of current resources, services, platforms, tools, and technologies across Cloud, AI, Web Development, and Tech in general.

Because honestly?

A huge part of learning tech is also learning its history.

If you have ever said things like:

  • “Wait… wasn’t this called something else before?”
  • “Isn’t this just a rebrand?”
  • “I still call it Twitter.”
  • “When did this AWS service change names?”
  • “Hold on… what happened to that?”

…then this series is for you, and mostly me.😂

I am going to explore:

1. Old names vs new names
2. Why companies rebrand
3. Mergers and acquisitions
4. Product evolution
5. Retired services
6. Tech nostalgia
7. The confusion these changes create for beginners

And maybe most importantly:
How keeping up with naming changes helps us better understand the tech ecosystem.

Some posts may be quick and fun.
Others may dive deeper into the history behind a platform or service.

Either way, this series is for:

  • Developers 👩🏽‍💻
  • Cloud learners ☁️
  • Curious tech people 🔍
  • Documentation readers 📚
  • People who still accidentally use old names 😅
  • Me.

“Formerly Known As.”

The internet never forgets old names…
and apparently neither do we.

No particular frequency.
No particular theme.
Just the renamed things that cross my mind…and my notifications.

How to Build Type-Safe Form Handlers Using TypeScript Utility Types

Form handling is one of the areas where TypeScript utility types save the most repetition. A typical form involves at least three distinct type states: the initial (possibly empty) form, the in-progress state as the user fills it out, and the validated payload ready for submission. Without utility types, you write three separate interfaces and keep them in sync manually. With utility types, you write one and derive the rest.

Step 1: Define the Submission Schema

Start with the type that represents the complete, valid submission — every field required, no optionals.

interface ContactFormData {
  name: string;
  email: string;
  company: string;
  message: string;
  acceptedTerms: boolean;
}

This is your canonical type. Everything else derives from it.

Step 2: Derive the Form State Type

Form state is the same shape as the submission type, but every field is optional. A user may not have filled anything in yet, and you do not want TypeScript complaining about empty strings or undefined fields during the editing phase.

type ContactFormState = Partial<ContactFormData>;

That is it. One line. ContactFormState has the same fields as ContactFormData but all optional.

In a React component:

import { useState } from 'react';

function ContactForm() {
  const [formState, setFormState] = useState<ContactFormState>({});

  function handleChange(field: keyof ContactFormData, value: string | boolean) {
    setFormState(prev => ({ ...prev, [field]: value }));
  }

  // ...
}

Using keyof ContactFormData for the field parameter means TypeScript will catch any typo in field names at the call site. If you rename a field, every call to handleChange with the old name becomes a compile error.

Step 3: Define the Validated Type

After validation, you know all fields are present. Required<T> makes this explicit:

type ValidatedContactForm = Required<ContactFormData>;

Since ContactFormData already has all required fields, ValidatedContactForm is structurally identical here. The value is when your base type has optional fields — Required<T> strips all ? modifiers and gives you a type you can assert after validation.

function validateForm(state: ContactFormState): asserts state is ValidatedContactForm {
  const required: (keyof ContactFormData)[] = [
    'name', 'email', 'company', 'message', 'acceptedTerms'
  ];

  for (const field of required) {
    if (state[field] === undefined || state[field] === '') {
      throw new Error(`${field} is required`);
    }
  }
}

After validateForm(state) runs without throwing, TypeScript knows state is ValidatedContactForm — all fields are present and non-undefined.

Step 4: Handle Partial Pre-Population

Some forms pre-populate from existing data — editing a profile, resuming a draft. The source data is usually a different type than the form type.

interface UserProfile {
  id: string;
  displayName: string;
  email: string;
  bio?: string;
  avatarUrl: string;
  createdAt: Date;
}

interface ProfileFormData {
  displayName: string;
  email: string;
  bio: string;
}

// Initialize form from profile data
function initFormFromProfile(profile: UserProfile): Partial<ProfileFormData> {
  return {
    displayName: profile.displayName,
    email: profile.email,
    bio: profile.bio ?? '',
  };
}

The function returns Partial<ProfileFormData> because bio is optional on UserProfile and may be absent. The form starts pre-populated with what is available.

Step 5: Build a Reusable Field Change Handler

With Partial<T> and keyof T, you can build a generic field change handler that works for any form:

type FormUpdater<T> = (field: keyof T, value: T[keyof T]) => void;

function useFormState<T>(initial: Partial<T> = {}) {
  const [state, setState] = useState<Partial<T>>(initial);

  const update: FormUpdater<T> = (field, value) => {
    setState(prev => ({ ...prev, [field]: value }));
  };

  return { state, update };
}

Usage:

const { state, update } = useFormState<ContactFormData>();

update('name', 'Jane Doe');
update('email', '[email protected]');
// TypeScript enforces field names and value types

The update function is fully typed. If ContactFormData.email is a string, passing a number to update('email', 42) is a type error. If you pass a field name that does not exist on ContactFormData, it is also a type error.

Step 6: Handle Multi-Section Forms

For longer forms, you might split fields across sections while still validating against one schema. Pick<T, K> lets you create section-specific types:

interface CheckoutFormData {
  // Shipping
  shippingName: string;
  shippingAddress: string;
  shippingCity: string;
  shippingPostalCode: string;
  // Payment
  cardNumber: string;
  expiryDate: string;
  cvv: string;
  // Promo
  promoCode?: string;
}

type ShippingSection = Pick<CheckoutFormData, 'shippingName' | 'shippingAddress' | 'shippingCity' | 'shippingPostalCode'>;
type PaymentSection = Pick<CheckoutFormData, 'cardNumber' | 'expiryDate' | 'cvv'>;

Each section component receives its relevant slice. You validate each section independently and only combine them into the full CheckoutFormData at submission.

Step 7: Discriminated Union for Form Step State

For multi-step forms, a discriminated union makes the current step state explicit:

type FormStep =
  | { step: 'shipping'; data: Partial<ShippingSection> }
  | { step: 'payment'; data: Partial<PaymentSection> }
  | { step: 'review'; data: CheckoutFormData };

The final step requires CheckoutFormData (fully populated), while earlier steps accept partial data. TypeScript enforces that you cannot reach the review step without complete data.

Validation Library Integration

This pattern works cleanly with Zod and React Hook Form. Zod schemas can infer TypeScript types directly, so you define validation rules and get the type for free:

import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  company: z.string().min(1),
  message: z.string().min(10),
  acceptedTerms: z.literal(true),
});

type ContactFormData = z.infer<typeof contactSchema>;
type ContactFormState = Partial<ContactFormData>;

The z.infer<typeof contactSchema> approach means you write the schema once and get both runtime validation and compile-time types from it. Partial<T> still applies to the inferred type for form state.

Tracking Touched Fields

A common UX requirement is showing validation errors only after a field has been interacted with — not on initial load. You need to track which fields have been touched separately from which fields have values.

type TouchedFields<T> = Partial<Record<keyof T, boolean>>;

function useFormWithTouched<T>(initial: Partial<T> = {}) {
  const [state, setState] = useState<Partial<T>>(initial);
  const [touched, setTouched] = useState<TouchedFields<T>>({});

  const touch = (field: keyof T) => {
    setTouched(prev => ({ ...prev, [field]: true }));
  };

  const update = (field: keyof T, value: T[keyof T]) => {
    setState(prev => ({ ...prev, [field]: value }));
  };

  const isTouched = (field: keyof T): boolean => !!touched[field];

  return { state, touch, update, isTouched };
}

Record<keyof T, boolean> creates an object with the same keys as your form data type, each holding a boolean. Partial<Record<keyof T, boolean>> makes all those keys optional so you can start with an empty object. keyof T constrains the touch and isTouched functions to valid field names — passing a nonexistent field name is a type error.

This pattern composes with the validation step: once a field is touched and its value is present, you run validation and show errors. The state shape is fully typed throughout.

What This Pattern Prevents

Without utility types, the common failure mode is having a ContactFormData interface and a separate ContactFormState interface that are supposed to match but diverge as fields are added or renamed. Someone adds a required phone field to ContactFormData, does not update ContactFormState, and the form silently allows submission without it.

With Partial<ContactFormData> as the form state type, there is no separate interface to keep in sync. The form state is always derived from the submission type. Adding phone to ContactFormData automatically adds it to the form state.

Further Reading

The TypeScript handbook covers Partial, Required, Pick, and keyof with their full type definitions. React documentation covers useState typing patterns. MDN Web Docs is a useful reference for the underlying JavaScript form event handling. For a reference covering all built-in utility types with production patterns for API layers, form handling, and configuration objects, see this guide to TypeScript utility types at TypeScript Utility Types: Practical Code Snippets and Patterns.

Semantic AI Search for Coding

Semantic AI Search for Coding

As software projects become larger and more complex, developers often spend a lot of time searching through files to find specific code. Sometimes you remember what a function does, but not its exact name. Traditional search systems only work properly when you type the exact keyword, which can become frustrating and time-consuming.

This is where Semantic AI Search becomes useful.

Semantic AI Search is a smart search technique powered by Artificial Intelligence (AI). Instead of searching only for exact words, it understands the meaning and context behind the query. This allows developers to search code more naturally and efficiently.

For example, a developer can search:

“Code for user login system”

Even if the project does not contain the exact words user login system, the AI can still identify related authentication or sign-in logic.

This makes coding faster, smarter, and much easier to manage.

What is Semantic AI Search?

Semantic AI Search is an AI-powered search system that understands the intent behind a query rather than matching exact keywords.

Traditional Search
Searches exact words only
Fails if different variable names are used
Cannot understand coding intent
Semantic AI Search
Understands meaning and context
Finds related code even with different names
Supports natural language searching

For example:

Search Query Traditional Search Semantic AI Search
“dark mode feature” Needs exact keyword Finds theme toggle code
“payment system” Needs matching words Finds checkout logic
“authentication code” Needs exact term Finds login functions
Why is Semantic Search Important?

In large projects, developers waste a lot of time manually searching through files. Semantic AI search solves this problem by making code search more intelligent.

Benefits
Saves development time
Improves productivity
Helps beginners understand projects faster
Makes debugging easier
Reduces manual searching
Improves teamwork in large projects

Companies like GitHub and Google already use AI-powered systems to improve coding experiences.

How Semantic AI Search Works

The working process is simple:

The AI scans and understands the codebase
Code is converted into numerical patterns called embeddings
User queries are also converted into embeddings
The AI compares meanings instead of exact words
The most relevant code is displayed

This is why semantic search can understand intent instead of depending completely on keywords.

Using Python for Semantic AI Search

Python is one of the best programming languages for building AI-powered applications because of its powerful libraries and simple syntax.

Some commonly used libraries are:

Sentence Transformers
FAISS
NumPy
Transformers

These libraries help the AI understand text similarity and semantic meaning.

Installing Required Libraries

Before starting, install the required libraries using:

pip install sentence-transformers faiss-cpu

Python Program for Semantic AI Search

from sentence_transformers import SentenceTransformer
import numpy as np

# Sample code descriptions
documents = [
    "Function for user login",
    "Database connection setup",
    "Dark mode toggle feature",
    "Payment gateway integration"
]

# Load AI model
model = SentenceTransformer('all-MiniLM-L6-v2')

# Convert documents into embeddings
doc_embeddings = model.encode(documents)

# User search query
query = "authentication system"

# Convert query into embedding
query_embedding = model.encode([query])

# Calculate similarity
scores = np.dot(doc_embeddings, query_embedding.T)

# Get best match
best_match = np.argmax(scores)

print("Best Match:")
print(documents[best_match])

Explanation of the Program

The program first imports the required libraries.

from sentence_transformers import SentenceTransformer
import numpy as np

A list containing sample code descriptions is created.

documents = [
“Function for user login”,
“Database connection setup”,
“Dark mode toggle feature”,
“Payment gateway integration”
]

The AI model is then loaded.

model = SentenceTransformer(‘all-MiniLM-L6-v2’)

The model converts both the documents and the user query into embeddings so that their meanings can be compared.

Finally, the program calculates similarity scores and returns the most relevant result.

Output

Best Match:
Function for user login

The AI understands that authentication system is closely related to user login even though the exact words are different.

Advantages of Semantic AI Search

  1. Smarter Search

The system understands meaning instead of exact keywords.

  1. Faster Development

Developers can quickly locate important code sections.

  1. Beginner Friendly

New developers can understand large projects more easily.

  1. Improved Productivity

Less time spent searching means more time building applications.

  1. Natural Language Search

Developers can search using normal English sentences.

Limitations of Semantic AI Search

Although semantic search is powerful, it still has some limitations.

Large projects may require more processing power
AI models can sometimes return inaccurate results
Initial setup may be difficult for beginners
Accuracy depends on the quality of the AI model

However, semantic search is still much more efficient than traditional keyword searching.

Real-World Applications

Semantic AI Search is widely used in modern development tools.

Applications Include
AI coding assistants
Smart IDE search systems
Bug detection tools
Code recommendation systems
Documentation search
AI developer copilots

These tools help developers work faster and more efficiently.

Conclusion

Semantic AI Search for Coding is changing the way developers interact with large codebases. Unlike traditional search systems, it understands context and meaning, making code search smarter and more efficient.

Using Python and AI libraries such as Sentence Transformers, developers can build intelligent systems capable of understanding natural language queries. As AI technology continues to grow, semantic code search will become an important part of future software development and modern coding tools.

Comment j’utilise Claude Code — ne rien ranger, tout déléguer, y aller au feeling

Originally published at moday.me. Building MODAY in public.

Comment j’utilise Claude Code — ne rien ranger, tout déléguer, y aller au feeling

Petit retour en arrière : comment je m’en sers vraiment

J’ai écrit beaucoup sur la construction de MODAY, mais je n’avais jamais écrit sur la façon dont je pilote Claude Code au quotidien.

Le choix de la stack, l’assemblage du pipeline de publication, la localisation en neuf langues, le chatbot monté en 15 minutes — tout ça, c’était le côté « ce que j’ai construit ».

La boutique est ouverte, on est entrés dans la phase d’exploitation, alors c’est le bon moment pour prendre un peu de recul et écrire la version méta.

Conclusion en tête : je ne range rien, je délègue, j’y vais au feeling.

Ce sera probablement à contre-courant de la plupart des guides « comment utiliser Claude Code » que vous croisez.

Le CLAUDE.md, c’est Claude qui l’a écrit

On vous dira : « rangez bien les infos de votre projet dans CLAUDE.md ». Identité de marque, stack technique, conventions de code, priorités. L’humain organise pour que Claude Code puisse attraper le contexte du projet. C’est l’usage standard.

Sur MODAY, je n’ai écrit aucune ligne moi-même dans CLAUDE.md. Je ne sais même pas exactement ce qu’il y a dedans.

Au début du build, j’ai dit à Claude Code : « tu peux ranger les fichiers toi-même si tu veux ». Il s’est mis à compiler tout seul l’identité de marque, la stack, les tables SKU, la configuration Shopify Markets, les priorités par phase. Quand j’y jette un œil de temps en temps, c’est probablement mieux foutu que ce que j’aurais fait à la main — et sûrement plus lisible pour Claude.

Du début à la fin, je n’ai jamais « rangé » quoi que ce soit. Le geste de ranger lui-même, je l’ai passé du côté de Claude.

Les sessions, je les découpe au feeling

Je découpe en sessions, oui. Mais sans règle.

Pas de séparation stricte par objectif. C’est plutôt « contenu », « UI/UX », « MD (tâches diverses) » — des paquets larges. Génération d’images, traduction, code : tout peut se mélanger dans la même session.

Je ne sépare pas par rôle (« session traduction », « session code »). Je sépare par flux de travail. C’est grossier, mais ça produit un état où « les idées s’enchaînent toutes seules ».

Je suis en train de retoucher l’UI, et tout à coup : « ah, vu ce changement, il faudrait aussi ajuster les traductions ». Je lance la traduction dans la même session, sans transition. Comme le coût de porter le contexte d’une session à l’autre est nul, le fil de mes décisions ne casse jamais. C’est étonnamment efficace.

Je donne quasiment aucune directive technique

Je l’ai écrit dans un autre billet : au moment du choix de la stack, je n’ai donné zéro contrainte technique. Uniquement des contraintes côté business. Cette posture n’a pas changé une fois passé en phase d’implémentation.

La seule chose que je rappelle explicitement, c’est :

« Utilise Shopify et Gelato de manière standard, et garde l’implémentation custom au minimum. »

C’est ma règle d’or sur les SaaS. À long terme, ce qui tient le mieux, c’est de rester dans le cadre prévu par l’éditeur. Le sur-mesure paraît séduisant à court terme, mais finit toujours par casser à la prochaine mise à jour. Flux standards, API standards, structure de thème standard. On en sort uniquement quand c’est vraiment nécessaire.

Avec juste ça en tête, je peux laisser à Claude Code la quasi-totalité des détails : appels d’API, stratégie de tests, gestion d’erreurs, mise en œuvre.

« Tu peux me montrer ? » — « Non, débrouille-toi »

Claude Code me demande parfois confirmation. « Tu peux me montrer le contenu de ce fichier ? », « C’est bon comme ça ? », « Comment j’écris les tests ? ».

À presque toutes ces questions, je réponds : « fais-le toi-même ».

Tu veux voir un fichier, utilise ton outil de lecture. Tu attends une décision, prends-la toi-même. Résultat : la plupart du temps il va lire, il décide, il enchaîne directement avec l’implémentation.

C’est la mise en pratique de ce que j’ai écrit dans un autre billet : « je veux passer l’initiative côté IA ». Ne pas reprendre la décision, laisser la décision où elle est.

Au final, il ne reste que ce qui ne peut absolument pas passer par une API. Compte bancaire, validation des paiements, ouverture de comptes chez les prestataires, mise en facturation, création des clés API. Ces choses-là exigent un contrat avec un humain — c’est moi qui m’y colle. Tout le reste vit côté Claude.

Faire patrouiller GA4 tous les jours, et exiger des propositions

Nouvelle routine, lancée depuis l’ouverture.

Tous les jours, je fais patrouiller Claude Code dans GA4, et je lui demande de sortir des problèmes et des pistes d’amélioration. Je regarde la liste, j’élimine seulement ce qui est manifestement à côté de la plaque. Les 60–70 % qui restent, je les fais implémenter dans la foulée.

Entre la proposition et l’implémentation, ma seule contribution, c’est un GO/NO-GO. Je n’écris pas de code, je ne pense pas l’architecture. Presque zéro.

Je vais en faire un principe d’exploitation : « une amélioration mise en prod par jour, minimum ». Que dès l’instant où la boutique est ouverte, chaque jour quelque chose s’améliore. Pari assumé : une marque dont la boucle d’amélioration ne s’arrête jamais finit par gagner.

Et 99 % de cette boucle d’amélioration, c’est Claude Code qui la fait tourner.

Je ne range pas, et pourtant c’est rangé

En l’écrivant, je me rends compte que je ne fais quasiment aucun des gestes classiques du développement : « organiser », « concevoir », « planifier ».

CLAUDE.md, je ne l’écris pas. Les sessions, je les fais au feeling. Le workflow n’est pas routinisé. Pas de schéma d’architecture, pas d’outil de gestion de tâches. Et pourtant, du build à l’exploitation, ça tourne.

La vérité, c’est juste que j’ai délégué le rangement à Claude. Moi je ne le fais pas, mais Claude Code, lui, le fait. Donc dans les faits, l’état rangé est maintenu.

La configuration ressemble à celle de Masayoshi Son qui dit « on y va » et déclenche un commando d’exécution ultra-compétent qui se charge à la fois de l’organisation et de l’implémentation. Je reste du côté de celui qui dit « on y va », et le commando d’exécution, c’est l’IA.

Que l’on puisse, en tant qu’individu, reproduire cette configuration à l’ère de l’IA — c’est probablement le truc le plus marquant que j’ai senti en trois semaines.

Pour conclure

Je ne pense pas que ce soit la « bonne » méthode. Il y a sûrement une approche tout aussi valable où l’on écrit un CLAUDE.md soigné, où l’on découpe ses sessions par objectif, où l’on routinise son workflow.

Dans mon cas, simplement, ne pas ranger était plus rapide. Si j’ai du temps à investir, je préfère le passer à juger ce que Claude Code sort plutôt qu’à organiser en amont.

À très vite.

— Yoskee

moday.me

Porte le jour. — Les T-shirts MODAY

Set Pièces Prix
La semaine complète → Mon–Sun (7) $159
La semaine de travail → Mon–Fri (5) $119
Pack de démarrage → Mon · Wed · Fri (3) $79
Le week-end → Sat · Sun (2) $55

Livraison gratuite dès 99 $ · 8 couleurs × 6 tailles · 9 langues

Cómo uso Claude Code: no organizo, delego, y voy a lo que salga

Originally published at moday.me. Building MODAY in public.

Cómo uso Claude Code: no organizo, delego, y voy a lo que salga

Toca mirar atrás y contar cómo uso Claude Code

He escrito bastante sobre la construcción de MODAY hasta aquí, pero todavía no había contado cómo manejo Claude Code por dentro.

La historia de cómo elegí el stack, cómo monté el pipeline de publicación, cómo localicé el blog a nueve idiomas, cómo metí un chatbot en 15 minutos — todo eso iba del lado de qué construí.

Ahora que la tienda ya está abierta y entrando en fase de operación, toca subirse un nivel y escribir la parte meta. Adelanto la conclusión: no organizo, delego, y voy a lo que salga.

Probablemente vaya en dirección contraria a la mayoría de las guías sobre Claude Code que circulan por ahí.

El CLAUDE.md lo escribió Claude

Lo que se suele recomendar es: «organiza la información del proyecto en CLAUDE.md». Marca, stack técnico, convenciones de código, prioridades. Que el humano deje el contexto bien ordenado para que Claude Code pueda agarrarlo. Hasta ahí, el manual estándar.

El CLAUDE.md de MODAY no tiene ni una línea escrita por mí. Tampoco sé exactamente qué hay dentro.

Al principio del proyecto le dije a Claude Code «ordena tú mismo los archivos como te parezca», y empezó a recopilar por su cuenta la información de marca, el stack, la tabla de SKUs, la configuración de Markets y las prioridades por fase. Cuando lo miro por encima, está, seguramente, en un formato más legible para Claude del que yo conseguiría a mano.

De principio a fin, no he hecho ni una tarea de organización. Le he pasado a Claude la tarea de organizar en sí.

Las sesiones las separo «a ojo»

Sí separo sesiones, pero sin reglas.

No tengo una división estricta por propósito. Las agrupo de manera bastante vaga: «creación de contenido», «mejoras de UI/UX», «MD (cosas variadas)». Dentro de la misma sesión mezclo generación de imágenes, traducción y código sin problema.

No las divido por rol (tipo «sesión de traducción», «sesión de código»). Las divido por el flujo del trabajo. Es una división sucia, pero el resultado es que los pensamientos se encadenan.

Estoy retocando una pantalla y se me ocurre «espera, si cambio esto, también convendría ajustar la traducción». Y lanzo la traducción ahí mismo, en la misma sesión. Como el coste de arrastrar contexto entre sesiones es cero, no se me corta el hilo de las decisiones. Eso, sorprendentemente, marca diferencia.

Instrucciones técnicas, prácticamente ninguna

Ya lo conté en otro post: a la hora de elegir el stack, no di ni una condición técnica. Solo condiciones de negocio. Y al entrar en la implementación, la postura no cambió.

Lo único que sí transmito de forma consciente es:
«Usa Shopify y Gelato de la manera estándar. Implementación a medida, lo mínimo».

Lo considero una regla de oro del SaaS. Lo más estable a largo plazo es moverte dentro del rango que el proveedor anticipó. La custom mola a corto plazo, pero se rompe en cuanto el proveedor saca una actualización. Flujos estándar, API estándar, estructura de tema estándar. Solo se sale de ahí cuando es realmente necesario.

Con esa sola directriz, ya puedo dejar en manos de Claude Code casi todo: cómo llamar a la API, la estrategia de pruebas, el manejo de errores, los detalles de implementación.

Cuando me pide aclaraciones, le contesto «hazlo tú»

Claude Code, a veces, viene a confirmar. «¿Me enseñas el contenido de este archivo?», «¿está bien así?», «¿cómo escribo el test?».

A casi todas esas preguntas le respondo «hazlo tú».

Si quiere ver un archivo, que use su propia herramienta. Si me pide una decisión, le devuelvo «decide tú». En la práctica, en casi todos los casos termina leyéndoselo, decidiendo por sí mismo, y llegando hasta la implementación sin más.

Esto es la versión práctica de lo que conté en otro post: «quiero darle la iniciativa al lado de la IA». No devolverle la decisión, dejársela.

Al final, lo único que queda en mi lado son las cosas que no se pueden mover por API. La cuenta bancaria, la revisión del proveedor de pagos, los alta de servicios, los datos de facturación, la emisión de claves API. Ahí los contratos son con humanos, así que ahí sí muevo yo las manos. El resto lo tiene Claude.

Hago que Claude Code patrulle GA4 cada día y proponga mejoras

Esta es una rutina nueva, que empecé después de abrir.

Cada día Claude Code patrulla GA4 y saca una lista de problemas y propuestas de mejora. Yo le echo un ojo y descarto solo lo que claramente está mal enfocado. El 60–70 % restante lo manda a implementar ahí mismo.

Entre la propuesta y la implementación, lo único que hago es decir GO o NG. Escribir código, pensar el diseño — prácticamente cero.

Quiero convertir esto en el concepto operativo: «implementar como mínimo una mejora al día». Desde el momento en el que la tienda abrió, que cada día haya algo que sea un poquito mejor que ayer. Una marca cuyo bucle de mejora no se detiene, a largo plazo, termina ganando. Esa es la apuesta.

Y el 99 % de ese bucle de mejora lo mueve Claude Code.

No organizo, y aun así está organizado

Llegado a este punto, lo pienso otra vez. No hago casi nada del «manual clásico de desarrollo»: organizar, diseñar, planificar.

No tengo un CLAUDE.md escrito por mí. Las sesiones van a ojo. El flujo de trabajo no está convertido en rutina. No hay un diseño técnico explícito, ni una herramienta de gestión de tareas. Y sin embargo, desde la construcción hasta la operación, la cosa rueda.

Para ser exactos: la organización la he delegado en el lado de Claude. Yo no la hago, pero Claude Code sí. Así que, como efecto secundario, el estado «organizado» se mantiene.

Esta estructura se parece bastante a cuando Masayoshi Son (el fundador de SoftBank) dice «hagámoslo» y un equipo de ejecución súper competente arranca y se encarga de organizar e implementar todo. Yo me quedo en el lado de «hagámoslo», y el equipo de ejecución es la IA.

En la era de la IA, una persona sola puede reproducir esa estructura. Esa es, de lejos, la mayor sensación que me llevo de estas tres semanas.

Cierre

No creo que esta sea la forma correcta. Seguro que también funciona el enfoque clásico — escribir bien el CLAUDE.md, separar las sesiones por propósito, convertir el flujo en rutina.

Pero en mi caso, iba más rápido no organizando. Si tengo un rato libre para dedicarlo a organizar, prefiero usarlo en lanzarle algo a Claude Code y decidir sobre lo que devuelva.

Os sigo contando.

— Yoskee

moday.me

Ponte el día. — Las camisetas MODAY

Set Piezas Precio
La semana completa → Mon–Sun (7) $159
La semana laboral → Mon–Fri (5) $119
Paquete Inicial → Mon · Wed · Fri (3) $79
El fin de semana → Sat · Sun (2) $55

Envío gratis a partir de $99 · 8 colores × 6 tallas · 9 idiomas

Wie ich Claude Code benutze — nicht aufräumen, abgeben, aus dem Bauch heraus

Originally published at moday.me. Building MODAY in public.

Wie ich Claude Code benutze — nicht aufräumen, abgeben, aus dem Bauch heraus

Ein Rückblick, wie ich mit Claude Code eigentlich arbeite

Bis hierhin habe ich viel über den Bau von MODAY geschrieben, aber wie ich Claude Code konkret steuere, kam dabei nie vor.

Stack-Auswahl, Distributions-Pipeline, Lokalisierung in neun Sprachen, der in 15 Minuten zusammengeklopfte Chatbot — das war alles die Seite „was ich gebaut habe”.

Jetzt ist der Shop draußen, der Betrieb läuft an, und es ist ein guter Moment, einmal auf die Metaebene zu wechseln. Vorab das Fazit: nicht aufräumen, abgeben, aus dem Bauch heraus.

Das geht vermutlich ziemlich genau in die entgegengesetzte Richtung dessen, was man sonst so an Claude-Code-Tutorials liest.

CLAUDE.md hat Claude geschrieben

Der übliche Rat lautet: „Trag in der CLAUDE.md sauber alle Projektinformationen zusammen.” Markenkontext, Tech-Stack, Coding-Conventions, Prioritäten. Damit Claude Code den Projektkontext schnell greifen kann — alles vom Menschen vorsortiert. Das ist die Standard-Spielart.

Die CLAUDE.md von MODAY enthält keine einzige Zeile, die ich selbst geschrieben habe. Ich weiß nicht mal genau, was drinsteht.

Ganz am Anfang habe ich zu Claude Code gesagt: „Du darfst dir die Datei selber zurechtlegen.” Daraufhin hat es angefangen, Markenkontext, Tech-Stack, SKU-Tabelle, Markets-Setup und Phasen-Prioritäten von sich aus zusammenzuziehen. Wenn ich gelegentlich reinschaue, sieht das Ganze vermutlich sauberer aus, als ich es per Hand jemals hingekriegt hätte — und vor allem in einer Form, die Claude selbst gut lesen kann.

Von Anfang bis Ende habe ich das „Aufräumen” nicht ein einziges Mal selbst gemacht. Die Aufräum-Aufgabe selbst ist auf die Claude-Seite gewandert.

Sessions teile ich „aus dem Bauch” auf

Ich arbeite schon in getrennten Sessions. Aber eine Regel dafür gibt es nicht.

Ich trenne nicht streng nach Zweck. Es läuft eher grob nach Stimmung: „Content”, „UI/UX-Polishing”, „MD (Kleinkram)”. Bildgenerierung, Übersetzung, Code-Schreiben — das mischt sich alles innerhalb derselben Session.

Ich mache keine „Übersetzungs-Session” und keine „Coding-Session”. Ich schneide nach dem Arbeitsfluss. Klingt schlampig, hat aber den Effekt, dass „Einfälle aneinander andocken”.

Ich schraube am UI rum und merke plötzlich: „Ach, dann sollte ich auch die Übersetzung anpassen.” In derselben Session lasse ich die Übersetzung dann gleich mit drüberlaufen. Weil die Kosten, Kontext über Session-Grenzen zu schleppen, bei null liegen, reißt der Entscheidungsfluss nicht ab. Das wirkt überraschend stark.

Technische Anweisungen gebe ich praktisch keine

Habe ich in einem anderen Post schon mal beschrieben: bei der Stack-Auswahl habe ich keine einzige technische Bedingung mitgegeben. Was ich reingereicht habe, waren ausschließlich Business-Anforderungen. Und auch in der Bauphase hat sich an dieser Haltung nichts geändert.

Das einzige, was ich bewusst gesagt habe, ist:

„Bei Shopify und Gelato bitte den Standard-Pfad gehen, Custom-Implementierungen auf das absolute Minimum.“

Das ist für mich die eiserne Regel bei SaaS. Innerhalb dessen, wofür der Anbieter sein Produkt vorgesehen hat, zu bleiben, ist langfristig schlicht am stabilsten. Custom-Lösungen sind kurzfristig sexy, brechen aber bei der nächsten Vendor-Aktualisierung. Standard-Flows, Standard-API, Standard-Theme-Struktur. Davon abweichen nur dann, wenn es wirklich nicht anders geht.

Wenn das einmal gesetzt ist, kann ich API-Calls, Test-Strategie, Error Handling und sämtliche Implementierungsdetails komplett an Claude Code abgeben.

„Sag mir mal” beantworte ich mit „Mach das selbst”

Claude Code fragt einen ab und zu zurück. „Zeig mir bitte den Inhalt der Datei”, „Ist das so okay?”, „Wie schreibe ich die Tests?”

Auf praktisch alles davon antworte ich: „Mach das selbst.”

Wenn du dir die Datei anschauen willst, benutz deinen View-Tool. Wenn du eine Entscheidung von mir willst, entscheide selbst. Und tatsächlich liest Claude dann in den allermeisten Fällen selbst nach, trifft die Entscheidung selbst und implementiert direkt weiter.

Das ist die Praxis von dem, was ich in einem anderen Post beschrieben habe: die Initiative gehört auf die AI-Seite. Keine Entscheidung zurückspielen, die Entscheidung dort lassen.

Was am Ende übrigbleibt, sind nur noch die Bereiche, die sich partout nicht per API erledigen lassen: Bankkonto, Payment-Prüfung, Anmeldungen bei Diensten, Abo-Registrierung, API-Keys ausstellen. Das sind die Stellen, an denen Verträge mit Menschen gemacht werden — da setze ich mich selbst hin. Alles andere liegt auf der Claude-Seite.

GA4 jeden Tag abklappern lassen und Verbesserungen vorschlagen lassen

Das hier ist eine neue Routine, die erst nach dem Launch dazugekommen ist.

Jeden Tag lasse ich Claude Code GA4 abklappern und Probleme plus Verbesserungsvorschläge ausspucken. Ich überfliege das und werfe nur das raus, was offensichtlich daneben ist. Die verbleibenden 60–70 % lasse ich direkt vor Ort implementieren.

Zwischen Vorschlag und Implementierung mache ich nichts außer GO/NO-GO. Code schreiben, Architektur überlegen — quasi null.

Das will ich zum Betriebs-Prinzip erheben: „Jeden Tag wird mindestens eine Verbesserung tatsächlich live geschaltet.” Vom Moment des Launch an soll jeden Tag irgendetwas am Shop besser geworden sein. Die Wette ist: Eine Marke, deren Verbesserungs-Loop nicht abreißt, wird am Ende stärker.

Und 99 % dieses Loops dreht Claude Code.

Ich räume nichts auf — und trotzdem ist alles aufgeräumt

Wenn ich das bis hierhin so durchlese, fällt mir etwas auf: Ich mache so gut wie keine der klassischen Engineering-Disziplinen wie „Aufräumen”, „Design”, „Planung”.

Keine handgepflegte CLAUDE.md. Sessions aus dem Bauch. Workflows nicht routiniert. Kein Architektur-Diagramm. Kein Task-Manager. Und trotzdem läuft es vom Build bis in den Betrieb irgendwie rund.

Präziser gesagt: Das Aufräumen ist an die Claude-Seite delegiert. Ich räume nicht auf, Claude Code räumt auf. Im Ergebnis bleibt der aufgeräumte Zustand stabil.

Diese Konstellation hat etwas von Masayoshi Son, der einfach „Machen wir” sagt — und dann setzt sich ein hochkarätiges Execution-Team in Bewegung, das Sortierung und Umsetzung komplett übernimmt. Ich bleibe auf der „Machen-wir”-Seite, und das Execution-Team ist eine AI.

Dass eine Einzelperson in der AI-Ära diese Konstellation nachstellen kann — das ist das deutlichste Aha-Erlebnis aus diesen drei Wochen.

Zum Schluss

Ich glaube nicht, dass das die richtige Methode ist. Vermutlich funktioniert auch der Weg, bei dem man die CLAUDE.md ordentlich pflegt, Sessions nach Zweck trennt und Workflows zur Routine schleift — sauber durch.

Bei mir war es nur so, dass nicht-aufräumen schneller war. Wenn ich Zeit ins Aufräumen stecken könnte, würde ich diese Zeit lieber damit verbringen, etwas an Claude Code zu werfen und das Ergebnis zu bewerten.

Bis bald.

— Yoskee

moday.me

Trag den Tag. — Hol dir die MODAY-Shirts

Set Teile Preis
Die ganze Woche → Mon–Sun (7) $159
Die Arbeitswoche → Mon–Fri (5) $119
Starter-Pack → Mon · Wed · Fri (3) $79
Das Wochenende → Sat · Sun (2) $55

Gratisversand ab 99 $ · 8 Farben × 6 Größen · 9 Sprachen

How I Use Claude Code — Don’t Organize, Delegate, Just Vibe

Originally published at moday.me. Building MODAY in public.

How I Use Claude Code — Don’t Organize, Delegate, Just Vibe

Looking back, here’s how I actually use Claude Code

I’ve been writing about building MODAY for weeks now, but I never wrote about how I actually drive Claude Code.

The stack I picked, the distribution pipeline I wired up, the nine-language localization, the chatbot I shipped in 15 minutes — all of that was the “what I built” side of the story.

The store is open. I’m in operations mode now. So it’s time for the meta view.

The short version: don’t organize, delegate, just vibe.

This is probably the opposite of most Claude Code advice you’ll read online.

Claude wrote my CLAUDE.md

The standard advice is: “Write a clean CLAUDE.md with your project context.” Brand info, stack, coding conventions, priorities. Lay it all out neatly so Claude Code can pick up the context. That’s the textbook move.

In MODAY’s CLAUDE.md, I haven’t written a single line myself. I don’t even fully know what’s in it.

Early in the build, I told Claude Code, “feel free to maintain this file yourself.” It started writing in brand info, the stack, the SKU table, Markets settings, phase-by-phase priorities. When I glance at it now and then, it’s organized in a way that’s probably more legible to Claude than anything I’d produce by hand.

Start to finish, I never did the “organizing” work. I handed the act of organizing itself over to Claude.

I split sessions on vibes

I do split sessions. But there’s no rule.

No strict by-purpose taxonomy. Loose buckets like “content,” “UI/UX,” “MD (misc stuff).” Image generation, translation, code — all of it gets mixed inside the same session.

I don’t split by role like “translation session” vs “code session.” I split by the flow of the work. It’s sloppy, but it produces a state where one thought chains into the next.

I’ll be tweaking the UI and suddenly think, “oh, the translations should change to match this.” So I run the translations right there in the same session. The cost of dragging context across sessions is zero, so the flow of judgment doesn’t break. That turns out to matter more than I expected.

I barely give technical instructions

I wrote about this in another post: during stack selection, I didn’t give a single technical condition. Everything I supplied was business-side. That stance hasn’t changed once we got into implementation.

The one thing I actively communicate:

“Use Shopify and Gelato the standard way. Keep custom implementation to a minimum.“

I think this is the iron rule of SaaS. Staying inside the lane the vendor designed for is the most stable long-term play. Custom implementations are seductive in the short term and get destroyed by the next vendor update. Standard flows, standard APIs, standard theme structure. You only leave that lane when you absolutely have to.

Once that’s clear, I can leave the API calls, the testing strategy, the error handling, the implementation details — pretty much all of it — to Claude Code.

When it asks me, I tell it to figure it out itself

Claude Code asks for confirmation sometimes. “Can you show me the contents of this file?” “Is this okay?” “How should I write the tests?”

To almost all of it, my answer is: “figure it out yourself.“

If it wants to see a file, it can open the file itself. If it asks for a judgment call, I say “make the call.” Most of the time it goes off, reads what it needs, makes the decision, and ships the implementation.

This is the practical version of what I wrote in an earlier post: “I want the agency to sit on the AI side.” Don’t return judgments — hand judgment over.

What’s left at the bottom is the layer where APIs can’t drive things. Bank accounts, payment review, service signups, billing, API key issuance. That stuff is human-to-human contracts, so I do it myself. Everything else, I push to Claude’s side.

Daily GA4 patrol, with improvement proposals

Here’s the new operational pattern that started after launch.

Every day, I have Claude Code patrol GA4 and surface issues + improvement ideas. I skim the list and kill the ones that obviously miss. The remaining 60–70% — I have it implement on the spot.

Between the proposal and the implementation, all I’m doing is GO/NO-GO. No coding, almost no design thinking.

I want to make this the operating concept of the brand: “Ship one improvement, every single day.” From the moment the store opened, every day something gets a little better. The bet is that a brand whose improvement loop never stops eventually wins.

And 99% of that loop is Claude Code.

Nothing is organized, but everything is organized

Writing all this out, it hits me: I’m not doing any of the traditional dev hygiene. Organize, design, plan — almost none of it.

No hand-written CLAUDE.md. Sessions on vibes. No routinized workflow. No technical design doc. No task management tool. And yet, from build through operations, it just kind of runs.

To be precise: the organizing is delegated to Claude. I don’t do it, but Claude Code does. So the organized state gets maintained anyway.

The shape of it feels close to when Masayoshi Son says “let’s do it” and a ridiculously competent execution team springs into motion, handles the organizing and the implementation, all of it. I get to stay on the “let’s do it” side, and the execution team is AI.

The biggest thing I’ve internalized in the last three weeks is that, in the AI era, an individual can actually reproduce that shape.

Closing

This is probably not the right way to do it. I’m sure the version where you write a serious CLAUDE.md, split sessions by purpose, and routinize the workflow works fine too.

For me, though, not organizing was just faster. If I had time to spend on organizing, I’d rather throw it at Claude Code and spend that time judging what comes back.

More soon.

— Yoskee

moday.me

Wear the day. — Get the MODAY Tees

Set Pieces Price
The Full Week → Mon–Sun (7) $159
The Workweek → Mon–Fri (5) $119
Starter Pack → Mon · Wed · Fri (3) $79
The Weekend → Sat · Sun (2) $55

Free shipping over $99 · 8 colors × 6 sizes · 9 languages

Why We Started Writing About Cybersecurity

Cybersecurity has stopped being a topic only for large enterprises.

Today, even small businesses depend on:

  • cloud services,
  • email communication,
  • electronic signatures,
  • WordPress websites,
  • remote access,
  • MFA authentication,
  • online payments.

At the same time, phishing attacks, credential theft, and social engineering are becoming more sophisticated every year.

We created Smart Lab to focus on practical security:
not theoretical checklists, but solutions businesses can actually implement.

Here on DEV Community, we plan to share:

  • practical MFA and authentication insights,
  • phishing protection approaches,
  • FIDO2 and passwordless authentication,
  • WordPress security improvements,
  • secure infrastructure practices,
  • lessons learned from real-world projects.

We’re especially interested in the intersection of:
security, usability, and real business needs.

Looking forward to joining the DEV Community.