Back to skills
SkillHub ClubShip Full StackFull StackBackend

server-actions

Imported from https://github.com/RomualdP/hoki.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C2.6
Composite score
2.6
Best-practice grade
D50.4

Install command

npx @skill-hub/cli install romualdp-hoki-server-actions

Repository

RomualdP/hoki

Skill path: .claude/skills/server-actions

Imported from https://github.com/RomualdP/hoki.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: RomualdP.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install server-actions into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/RomualdP/hoki before adding server-actions to shared team environments
  • Use server-actions for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: Server Actions Generator
description: Génère des Next.js Server Actions comme couche d'orchestration mince entre frontend et backend NestJS. À utiliser lors de la création d'actions, mutations, ou quand l'utilisateur mentionne "server action", "mutation", "form action", "useTransition", "revalidatePath".
allowed-tools: [Read, Write, Edit, Glob, Grep]
---

# Server Actions Generator

## 🎯 Mission

Créer des **Server Actions Next.js** comme **couche d'orchestration mince** entre le frontend et le backend NestJS, avec gestion du cache et des erreurs.

## 🏗️ Philosophie Server Actions

### Qu'est-ce qu'une Server Action ?

Une **Server Action** est une fonction serveur Next.js (`'use server'`) qui :
- ✅ Exécute côté serveur (Next.js server, pas client)
- ✅ Peut être appelée directement depuis un composant client
- ✅ Simplifie les mutations (pas besoin d'API route)
- ✅ Intègre avec les forms HTML natifs

### Architecture Flow

```
Component (Client)
  ↓ useTransition() ou form action
Server Action (Next.js Server) [THIN LAYER]
  ↓ Validation Zod
  ↓ fetch/axios
Backend NestJS API
  ↓ Command Handler (CQRS)
  ↓ Domain Entity
  ↓ Repository
Database (Prisma)
```

### Responsabilités d'une Server Action

**✅ CE QU'ELLE FAIT** :
1. Valider les inputs (Zod)
2. Appeler l'API backend NestJS
3. Gérer le cache Next.js (`revalidatePath`, `revalidateTag`)
4. Formatter les erreurs pour l'UI
5. Retourner un résultat typé

**❌ CE QU'ELLE NE FAIT PAS** :
- ❌ **JAMAIS** de logique métier (dans le backend)
- ❌ **JAMAIS** d'accès direct à la DB (utiliser backend)
- ❌ **JAMAIS** dupliquer la validation backend

## 📝 Template Server Action

### Structure de Fichier

```
features/
└── club-management/
    └── actions/
        ├── create-club.action.ts
        ├── update-club.action.ts
        ├── delete-club.action.ts
        ├── subscribe-to-plan.action.ts
        └── index.ts                  # Barrel export
```

### Template Complet

```typescript
// features/club-management/actions/create-club.action.ts
'use server';

import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { clubsApi } from '../api/clubs.api';

// 1. Schema de validation (synchronisé avec backend DTO)
const createClubSchema = z.object({
  name: z
    .string()
    .min(3, 'Le nom doit contenir au moins 3 caractères')
    .max(100, 'Le nom ne peut pas dépasser 100 caractères'),
  description: z
    .string()
    .max(500, 'La description ne peut pas dépasser 500 caractères')
    .optional(),
});

// 2. Type d'input (inféré depuis schema)
export type CreateClubInput = z.infer<typeof createClubSchema>;

// 3. Type de résultat
export type CreateClubResult =
  | { success: true; data: { id: string } }
  | { success: false; error: { code: string; message: string; details?: any } };

// 4. Server Action
export async function createClubAction(input: CreateClubInput): Promise<CreateClubResult> {
  try {
    // Validate input
    const validated = createClubSchema.parse(input);

    // Call backend API
    const response = await clubsApi.create(validated);

    // Revalidate cache
    revalidatePath('/dashboard/coach');
    revalidatePath('/clubs');

    // Return success
    return {
      success: true,
      data: response,
    };
  } catch (error) {
    // Handle validation errors
    if (error instanceof z.ZodError) {
      return {
        success: false,
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Les données fournies sont invalides',
          details: error.errors,
        },
      };
    }

    // Handle API errors
    if (error instanceof ApiError) {
      return {
        success: false,
        error: {
          code: error.code,
          message: error.getUserMessage(),
        },
      };
    }

    // Handle unknown errors
    return {
      success: false,
      error: {
        code: 'UNKNOWN_ERROR',
        message: 'Une erreur est survenue. Veuillez réessayer.',
      },
    };
  }
}
```

## 🔄 Cache Management

### revalidatePath

Invalide le cache pour un chemin spécifique.

```typescript
'use server';

export async function createClubAction(input: CreateClubInput) {
  const response = await clubsApi.create(input);

  // Revalidate specific paths
  revalidatePath('/dashboard/coach'); // Coach dashboard
  revalidatePath('/clubs'); // Clubs list page
  revalidatePath(`/clubs/${response.id}`); // Club detail page

  return { success: true, data: response };
}
```

**Quand utiliser** :
- ✅ Après création/modification/suppression de données
- ✅ Pour forcer le re-fetch des Server Components
- ✅ Pour mettre à jour l'UI après mutation

### revalidateTag

Invalide le cache par tag (plus flexible).

```typescript
'use server';

export async function createClubAction(input: CreateClubInput) {
  const response = await clubsApi.create(input);

  // Revalidate by tags
  revalidateTag('clubs'); // All clubs-related data
  revalidateTag(`club-${response.id}`); // Specific club

  return { success: true, data: response };
}

// Dans Server Component ou API route
fetch('/api/clubs', {
  next: { tags: ['clubs'] }
});

fetch(`/api/clubs/${id}`, {
  next: { tags: [`club-${id}`, 'clubs'] }
});
```

**Quand utiliser** :
- ✅ Gestion fine du cache
- ✅ Invalidation groupée (ex: tous les "clubs")
- ✅ Avec `fetch` et Next.js cache

## 🎨 Intégration avec Composants

### Avec useTransition (Recommended)

```typescript
// components/ClubCreationForm.tsx
'use client';

import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { createClubAction, CreateClubInput } from '../actions/create-club.action';
import { toast } from 'sonner';

export function ClubCreationForm() {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (formData: FormData) => {
    setError(null);

    const input: CreateClubInput = {
      name: formData.get('name') as string,
      description: formData.get('description') as string,
    };

    startTransition(async () => {
      const result = await createClubAction(input);

      if (result.success) {
        toast.success('Club créé avec succès !');
        router.push(`/clubs/${result.data.id}`);
      } else {
        setError(result.error.message);
        toast.error(result.error.message);
      }
    });
  };

  return (
    <form action={handleSubmit} className="space-y-4">
      <input name="name" placeholder="Nom du club" required />
      <textarea name="description" placeholder="Description" />

      {error && (
        <div className="text-red-500 text-sm">{error}</div>
      )}

      <button
        type="submit"
        disabled={isPending}
        className="btn btn-primary"
      >
        {isPending ? 'Création...' : 'Créer le club'}
      </button>
    </form>
  );
}
```

### Avec Form Action (HTML Native)

```typescript
// components/QuickClubForm.tsx
'use client';

import { useFormStatus } from 'react-dom';
import { createClubAction } from '../actions/create-club.action';

export function QuickClubForm() {
  return (
    <form action={createClubAction}>
      <input name="name" placeholder="Nom du club" required />
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Création...' : 'Créer'}
    </button>
  );
}
```

### Avec useActionState (React 19)

```typescript
'use client';

import { useActionState } from 'react';
import { createClubAction } from '../actions/create-club.action';

export function ClubForm() {
  const [state, formAction, isPending] = useActionState(
    createClubAction,
    { success: false, error: null }
  );

  return (
    <form action={formAction}>
      <input name="name" />

      {state.error && (
        <div className="error">{state.error.message}</div>
      )}

      <button disabled={isPending}>
        {isPending ? 'Envoi...' : 'Envoyer'}
      </button>
    </form>
  );
}
```

## 🚨 Error Handling

### Types d'Erreurs

```typescript
// lib/errors.ts

export class ApiError extends Error {
  constructor(
    public code: string,
    message: string,
    public status?: number,
  ) {
    super(message);
    this.name = 'ApiError';
  }

  getUserMessage(): string {
    const messages: Record<string, string> = {
      VALIDATION_ERROR: 'Les données fournies sont invalides',
      NOT_FOUND: 'La ressource demandée n\'existe pas',
      UNAUTHORIZED: 'Vous devez être connecté',
      FORBIDDEN: 'Vous n\'avez pas les permissions nécessaires',
      INTERNAL_SERVER_ERROR: 'Une erreur interne est survenue',
    };

    return messages[this.code] || this.message;
  }
}
```

### Gestion dans Server Action

```typescript
'use server';

export async function updateClubAction(id: string, input: UpdateClubInput) {
  try {
    const validated = updateClubSchema.parse(input);
    const response = await clubsApi.update(id, validated);

    revalidatePath(`/clubs/${id}`);

    return { success: true, data: response };
  } catch (error) {
    // Zod validation errors
    if (error instanceof z.ZodError) {
      return {
        success: false,
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Données invalides',
          details: error.errors.map(e => ({
            field: e.path.join('.'),
            message: e.message,
          })),
        },
      };
    }

    // API errors (404, 403, etc.)
    if (error instanceof ApiError) {
      return {
        success: false,
        error: {
          code: error.code,
          message: error.getUserMessage(),
        },
      };
    }

    // Network errors
    if (error instanceof TypeError && error.message.includes('fetch')) {
      return {
        success: false,
        error: {
          code: 'NETWORK_ERROR',
          message: 'Impossible de contacter le serveur',
        },
      };
    }

    // Unknown errors
    console.error('Server Action Error:', error);
    return {
      success: false,
      error: {
        code: 'UNKNOWN_ERROR',
        message: 'Une erreur est survenue',
      },
    };
  }
}
```

## 📋 Exemples Complets

### Create (POST)

```typescript
'use server';

export async function createClubAction(input: CreateClubInput) {
  const validated = createClubSchema.parse(input);
  const response = await clubsApi.create(validated);

  revalidatePath('/dashboard/coach');

  return { success: true, data: response };
}
```

### Update (PUT/PATCH)

```typescript
'use server';

export async function updateClubAction(id: string, input: UpdateClubInput) {
  const validated = updateClubSchema.parse(input);
  const response = await clubsApi.update(id, validated);

  revalidatePath(`/clubs/${id}`);
  revalidatePath('/dashboard/coach');

  return { success: true, data: response };
}
```

### Delete (DELETE)

```typescript
'use server';

export async function deleteClubAction(id: string) {
  await clubsApi.delete(id);

  revalidatePath('/dashboard/coach');
  revalidatePath('/clubs');

  return { success: true };
}
```

### Batch Operation

```typescript
'use server';

export async function removeMembersAction(clubId: string, memberIds: string[]) {
  const results = await Promise.allSettled(
    memberIds.map(id => clubsApi.removeMember(clubId, id))
  );

  const successful = results.filter(r => r.status === 'fulfilled').length;
  const failed = results.filter(r => r.status === 'rejected').length;

  revalidatePath(`/clubs/${clubId}/members`);

  return {
    success: failed === 0,
    data: { successful, failed },
  };
}
```

## ✅ Checklist Server Actions

- [ ] `'use server'` directive en première ligne
- [ ] Schema Zod pour validation
- [ ] Types inférés depuis schema (`z.infer<>`)
- [ ] Type de résultat (success/error pattern)
- [ ] Appel API backend (pas de logique métier)
- [ ] `revalidatePath()` ou `revalidateTag()` après mutation
- [ ] Error handling exhaustif (Zod, API, Network, Unknown)
- [ ] Messages d'erreur traduits pour utilisateur
- [ ] Fichier nommé `*.action.ts`
- [ ] Export dans barrel `index.ts`

## 🚨 Erreurs Courantes

### 1. Logique Métier dans Server Action

```typescript
// ❌ MAUVAIS - Logique métier dans Server Action
export async function createClubAction(input: CreateClubInput) {
  // Validation métier (devrait être dans backend)
  if (input.name.includes('bad_word')) {
    return { success: false, error: 'Name invalid' };
  }

  // Calculs métier (devrait être dans backend)
  const price = input.plan === 'PRO' ? 9.99 : 0;

  // ...
}

// ✅ BON - Délégation au backend
export async function createClubAction(input: CreateClubInput) {
  // Validation simple
  const validated = createClubSchema.parse(input);

  // Backend fait toute la logique
  const response = await clubsApi.create(validated);

  revalidatePath('/clubs');
  return { success: true, data: response };
}
```

### 2. Oublier revalidatePath

```typescript
// ❌ MAUVAIS - Cache pas invalidé
export async function createClubAction(input: CreateClubInput) {
  const response = await clubsApi.create(input);
  return { success: true, data: response };
  // UI ne se met pas à jour !
}

// ✅ BON - Cache invalidé
export async function createClubAction(input: CreateClubInput) {
  const response = await clubsApi.create(input);

  revalidatePath('/clubs'); // Important !

  return { success: true, data: response };
}
```

### 3. Erreurs Non Gérées

```typescript
// ❌ MAUVAIS - Erreurs non gérées
export async function createClubAction(input: CreateClubInput) {
  const response = await clubsApi.create(input); // Peut throw
  return { success: true, data: response };
}

// ✅ BON - Toutes les erreurs gérées
export async function createClubAction(input: CreateClubInput) {
  try {
    const response = await clubsApi.create(input);
    return { success: true, data: response };
  } catch (error) {
    // Gestion complète des erreurs
    return {
      success: false,
      error: {
        code: 'ERROR',
        message: 'Une erreur est survenue',
      },
    };
  }
}
```

## 📚 Skills Complémentaires

- **api-contracts** : DTOs, Types, Validation frontend/backend
- **use-optimistic** : Optimistic updates avec Server Actions
- **atomic-component** : Composants utilisant Server Actions

---

**Rappel** : Server Actions = **Couche mince** d'orchestration. Toute la logique métier est dans le **backend NestJS**.
server-actions | SkillHub