Claude Code — desarrollo-guiado-por-documentacion
✓ VERIFICADO
🔍
PASO 1 — Detectar Stack y Versiones
Leyendo package.json → versiones exactas identificadas
next 15.3.2 ← App Router estable, Server Actions GA
react / react-dom 19.1.0 ← useActionState disponible (ex useFormState)
typescript 5.7.3 ← satisfies operator disponible
zod 3.23.8 ← safeParse, z.infer disponibles
→ Buscando docs oficiales para: Server Actions (Next 15), useActionState (React 19), Zod v3 schema validation
📡
PASO 2 — Fetch Documentación Oficial
nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
Server Actions se declaran con "use server" en el módulo o inline. Se pasan directamente al atributo action= del form.
NUEVO en Next 15: binding explícito de args extra con .bind() sin romper serialización.
react.dev/reference/react/useActionState#usage
Hook oficial React 19 para estado de formularios. Retorna [state, action, isPending].
DEPRECADO: useFormState de react-dom (React 18) → reemplazado por useActionState en React 19.
zod.dev/api#safeparse
Validación sin lanzar excepciones: schema.safeParse(data) devuelve { success, data, error }.
RECOMENDADO: usar z.infer<typeof Schema> para tipado automático sin duplicar interfaces.
nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#error-handling
Las Server Actions deben devolver objetos serializables. Para errores de validación: retornar { errors: FieldErrors }.
NUNCA: lanzar Error() dentro de Server Actions para errores de usuario — solo para errores fatales de servidor.
⚠ CONFLICTO DETECTADO — requiere decisión
El componente existente ContactForm.tsx en el proyecto usa useFormState de react-dom (patrón React 18).
React 19 ha renombrado este hook a useActionState e importado desde react, no desde react-dom.

Opción A: Usar useActionState (React 19, docs actuales) — consistente con esta implementación nueva.
Opción B: Mantener useFormState — consistente con código existente, pero patrón deprecado.
Decisión tomada: Opción A con comentario de migración, ya que es código nuevo y React 19 es la versión instalada.
⚙️
PASO 3 — Implementación con Patrones Documentados
app/clinicas/nueva/actions.ts
TypeScript · Server Action
// Source: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
"use server"; // directiva de módulo — aplica a todas las exportaciones

import { z } from "zod";
import { db } from "@/lib/db";

// Zod schema — z.infer evita duplicar la interfaz
// Source: https://zod.dev/api#safeparse + https://zod.dev/api#zodtype-infer
const ClinicaSchema = z.object({
  nombre_clinica: z.string().min(3, "Mínimo 3 caracteres").max(100),
  email_admin:    z.string().email("Email inválido"),
  telefono:       z.string().regex(/^\+?[0-9\s\-]{7,15}$/, "Teléfono inválido"),
  plan:           z.enum(["starter", "pro", "enterprise"]),
});

export type ClinicaFormState = {
  errors?: Partial<Record<keyof z.infer<typeof ClinicaSchema>, string[]>>;
  success?: boolean;
  clinicaId?: string;
};

// Server Action — retorna estado serializable (no lanza Error para errores de usuario)
// Source: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#error-handling
export async function crearClinica(
  _prevState: ClinicaFormState,
  formData: FormData
): Promise<ClinicaFormState> {
  const raw = {
    nombre_clinica: formData.get("nombre_clinica"),
    email_admin:    formData.get("email_admin"),
    telefono:       formData.get("telefono"),
    plan:           formData.get("plan"),
  };

  // safeParse — no lanza excepciones, devuelve { success, data, error }
  // Source: https://zod.dev/api#safeparse
  const parsed = ClinicaSchema.safeParse(raw);
  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  const clinica = await db.clinica.create({ data: parsed.data });
  return { success: true, clinicaId: clinica.id };
}
app/clinicas/nueva/ClinicaForm.tsx
TypeScript · Client Component
// Source: https://react.dev/reference/react/useActionState#usage
"use client";

// useActionState importado desde "react" (React 19)
// NOTA: useFormState de react-dom está deprecado en React 19 — NO usar
// Migración: https://react.dev/blog/2024/04/25/react-19-upgrade-guide#useformstate-deprecated
import { useActionState } from "react";
import { crearClinica, type ClinicaFormState } from "./actions";

const initialState: ClinicaFormState = {};

export function ClinicaForm() {
  // useActionState(action, initialState) → [state, wrappedAction, isPending]
  // isPending: true mientras el Server Action está en vuelo (sin useTransition manual)
  // Source: https://react.dev/reference/react/useActionState#usage
  const [state, formAction, isPending] = useActionState(crearClinica, initialState);

  if (state.success) {
    return (
      <div className="alert-success">
        Clínica creada · ID: {state.clinicaId}
      </div>
    );
  }

  return (
    <form action={formAction}>
      <!-- action= acepta Server Action directamente; Next 15 maneja serialización -->
      <!-- Source: nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations -->

      <Field
        name="nombre_clinica"
        label="Nombre de la clínica"
        errors={state.errors?.nombre_clinica}
      />
      <Field
        name="email_admin"
        label="Email administrador"
        type="email"
        errors={state.errors?.email_admin}
      />
      <Field
        name="telefono"
        label="Teléfono"
        errors={state.errors?.telefono}
      />

      <select name="plan">
        <option value="starter">Starter — 49€/mes</option>
        <option value="pro">Pro — 99€/mes</option>
        <option value="enterprise">Enterprise — a medida</option>
      </select>

      <button type="submit" disabled={isPending}>
        {isPending ? "Creando clínica..." : "Crear clínica"}
      </button>
    </form>
  );
}
📎
PASO 4 — Tabla de Fuentes y Decisiones
Decisión Fuente Oficial Estado
useActionState en vez de useFormState
"useFormState is now called useActionState and is imported from react, not react-dom"
Verificado
isPending como tercer retorno de useActionState
"useActionState returns [state, formAction, isPending] — no useTransition needed"
Verificado
action= del form acepta Server Action directamente (no handler)
"Pass the server action directly to the form's action prop"
Verificado
safeParse en vez de parse (no lanza Error de validación)
"safeParse returns { success, data, error } without throwing"
Verificado
No lanzar Error() para errores de usuario en Server Actions
"Thrown errors are for unexpected failures, not validation errors"
Verificado
form.reset() después de submit exitoso
No encontré documentación oficial Next.js 15 específica para reset post-action
Sin verificar
Lista de Verificación Final
  • Versiones identificadas desde package.json (Next 15.3.2, React 19.1.0, Zod 3.23.8)
  • Documentación oficial consultada para todos los patrones principales
  • Todas las fuentes son docs oficiales (no Stack Overflow, no blogs)
  • Código sigue patrones de la versión instalada (no de memoria)
  • Cada decisión no trivial tiene URL completa en comentario
  • API deprecada (useFormState) identificada y evitada, con nota de migración
  • Conflicto con código existente documentado y resuelto explícitamente
  • Patrón sin documentación oficial (form.reset) marcado como NO VERIFICADO
5
Fuentes verificadas
4
Docs consultadas
1
Conflicto resuelto
1
Patrón sin verificar