Awesome-omni-skill integration
Backend-Frontend integration patterns expert. Type-safe API contracts with Pydantic-Zod validation sync (Python FastAPI) or Prisma-TypeScript native (Next.js). Shadcn forms connected to backend, error handling, loading states. Use when creating full-stack features.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/integration-nera0875" ~/.claude/skills/diegosouzapw-awesome-omni-skill-integration-970e01 && rm -rf "$T"
skills/development/integration-nera0875/SKILL.mdIntegration Skill - Backend ↔ Frontend Patterns
Expert intégration backend (Python FastAPI ou Next.js) ↔ frontend React + shadcn
Inspiré de : Stripe API patterns, Vercel full-stack architecture, Prisma best practices
Scope
Chargé par: executor agent (quand feature full-stack détectée)
Deux chemins d'intégration:
Path A: Python FastAPI + React + shadcn (REST API)
- Backend: Python + FastAPI + Pydantic
- Frontend: React + Next.js + shadcn
- Communication: REST API (JSON)
- Validation: Pydantic backend ↔ Zod frontend (mirrored)
Path B: Next.js Full-Stack + React + shadcn (Server Actions)
- Backend: Next.js Server Actions + Prisma
- Frontend: React + Next.js + shadcn
- Communication: Server Actions (RPC-like)
- Validation: Zod + Prisma native types
Patterns couverts:
- Type-safe API contracts (Pydantic → TypeScript OU Prisma → TypeScript native)
- Validation synchronisée (Pydantic ↔ Zod OU Zod + Prisma enums)
- Forms shadcn → Backend (FastAPI routes OU Server Actions)
- Error handling (standardisé backend → UI feedback)
- Loading states (React Query OU useOptimistic)
- Database patterns (Prisma best practices)
Pattern #1: Type-Safe API Contract (Path A - FastAPI)
Backend (Pydantic schemas)
# backend/app/schemas/task.py from pydantic import BaseModel, Field, validator from datetime import datetime from typing import Literal class TaskBase(BaseModel): title: str = Field(..., min_length=1, max_length=200) description: str | None = None status: Literal["pending", "in_progress", "completed"] = "pending" priority: Literal["low", "medium", "high"] = "medium" @validator("title") def title_not_empty(cls, v): if not v.strip(): raise ValueError("Title cannot be empty") return v.strip() class TaskCreate(TaskBase): """Input creation (pas d'ID)""" pass class TaskUpdate(BaseModel): """Update partiel (tous optionnels)""" title: str | None = Field(None, min_length=1, max_length=200) description: str | None = None status: Literal["pending", "in_progress", "completed"] | None = None priority: Literal["low", "medium", "high"] | None = None class TaskResponse(TaskBase): """Output avec metadata DB""" id: str created_at: datetime updated_at: datetime user_id: str class Config: from_attributes = True
Frontend (TypeScript types synchronisés)
// frontend/src/types/task.ts // Mirror exact Pydantic schemas export type TaskStatus = "pending" | "in_progress" | "completed" export type TaskPriority = "low" | "medium" | "high" export interface TaskBase { title: string description?: string | null status: TaskStatus priority: TaskPriority } export interface TaskCreate extends TaskBase { // Pas d'ID pour création } export interface TaskUpdate { // Tous optionnels pour update partiel title?: string description?: string | null status?: TaskStatus priority?: TaskPriority } export interface TaskResponse extends TaskBase { id: string created_at: string // ISO string du datetime Python updated_at: string user_id: string }
Principe: Types frontend MIRRORED exactement depuis Pydantic pour type-safety.
Pattern #2: Validation Synchronisée (Pydantic ↔ Zod)
Backend Validation (Pydantic)
# backend/app/schemas/task.py class TaskCreate(BaseModel): title: str = Field(..., min_length=1, max_length=200) description: str | None = None @validator("title") def title_not_empty(cls, v): if not v.strip(): raise ValueError("Title cannot be empty") return v.strip()
Frontend Validation (Zod - SYNCHRONISÉ)
// frontend/src/schemas/task.ts import { z } from "zod" // Mirror EXACT validation Pydantic export const taskCreateSchema = z.object({ title: z.string() .min(1, "Title cannot be empty") .max(200, "Title too long") .refine(val => val.trim().length > 0, "Title cannot be empty"), description: z.string().optional().nullable(), status: z.enum(["pending", "in_progress", "completed"]).default("pending"), priority: z.enum(["low", "medium", "high"]).default("medium"), }) export type TaskCreateInput = z.infer<typeof taskCreateSchema>
RÈGLE CRITIQUE: Validation frontend identique backend.
- Même min/max lengths
- Même regex patterns
- Même error messages (traduits si nécessaire)
Principe: Frontend validation = UX rapide, Backend validation = sécurité. (Defense in depth - jamais trust client)
Pattern #3: Shadcn Form → FastAPI Complete
Backend API Route
# backend/app/api/routes/tasks.py from fastapi import APIRouter, HTTPException, Depends from app.schemas.task import TaskCreate, TaskResponse from app.services.task_service import task_service router = APIRouter(prefix="/tasks", tags=["tasks"]) @router.post("/", response_model=TaskResponse, status_code=201) async def create_task( task: TaskCreate, user_id: str = Depends(get_current_user) ): """Crée nouvelle task avec validation""" try: return task_service.create_task(user_id, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Internal server error")
Frontend API Client
// frontend/src/lib/api/tasks.ts import { TaskCreate, TaskResponse } from "@/types/task" const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" export async function createTask(task: TaskCreate): Promise<TaskResponse> { const response = await fetch(`${API_BASE}/api/tasks`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${getToken()}`, }, body: JSON.stringify(task), }) if (!response.ok) { const error = await response.json() throw new Error(error.detail || "Failed to create task") } return response.json() }
Shadcn Form Component (Complete)
// frontend/src/components/task-form.tsx "use client" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { useMutation, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { taskCreateSchema, type TaskCreateInput } from "@/schemas/task" import { createTask } from "@/lib/api/tasks" export function TaskForm({ onSuccess }: { onSuccess?: () => void }) { const queryClient = useQueryClient() // Form avec Zod validation const form = useForm<TaskCreateInput>({ resolver: zodResolver(taskCreateSchema), defaultValues: { title: "", description: "", status: "pending", priority: "medium", }, }) // Mutation avec React Query const mutation = useMutation({ mutationFn: createTask, onSuccess: () => { // Invalidate cache pour refresh liste queryClient.invalidateQueries({ queryKey: ["tasks"] }) toast.success("Task created successfully") form.reset() onSuccess?.() }, onError: (error: Error) => { toast.error(error.message) }, }) const onSubmit = (data: TaskCreateInput) => { mutation.mutate(data) } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> {/* Title */} <FormField control={form.control} name="title" render={({ field }) => ( <FormItem> <FormLabel>Title</FormLabel> <FormControl> <Input placeholder="Enter task title" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> {/* Description */} <FormField control={form.control} name="description" render={({ field }) => ( <FormItem> <FormLabel>Description</FormLabel> <FormControl> <Textarea placeholder="Enter task description" {...field} value={field.value || ""} /> </FormControl> <FormMessage /> </FormItem> )} /> {/* Priority */} <FormField control={form.control} name="priority" render={({ field }) => ( <FormItem> <FormLabel>Priority</FormLabel> <Select onValueChange={field.onChange} defaultValue={field.value}> <FormControl> <SelectTrigger> <SelectValue placeholder="Select priority" /> </SelectTrigger> </FormControl> <SelectContent> <SelectItem value="low">Low</SelectItem> <SelectItem value="medium">Medium</SelectItem> <SelectItem value="high">High</SelectItem> </SelectContent> </Select> <FormMessage /> </FormItem> )} /> {/* Submit */} <Button type="submit" disabled={mutation.isPending}> {mutation.isPending ? "Creating..." : "Create Task"} </Button> </form> </Form> ) }
Checklist connexion complète:
- ✅ Zod schema mirrored depuis Pydantic
- ✅ TypeScript types synchronisés
- ✅ React Hook Form + Zod resolver
- ✅ React Query mutation (loading + error states)
- ✅ Toast notifications (success/error)
- ✅ Cache invalidation (refresh après create)
- ✅ Form reset après success
Pattern #4: Error Handling (Backend → Frontend)
Backend Error Responses (standardisé)
# backend/app/api/routes/tasks.py from fastapi import HTTPException from pydantic import ValidationError @router.post("/") async def create_task(task: TaskCreate): try: return task_service.create_task(task) except ValueError as e: # Business logic error raise HTTPException( status_code=400, detail={"message": str(e), "type": "validation_error"} ) except PermissionError as e: # Authorization error raise HTTPException( status_code=403, detail={"message": str(e), "type": "permission_error"} ) except Exception as e: # Unexpected error (log + generic message) logger.error(f"Unexpected error: {e}", exc_info=True) raise HTTPException( status_code=500, detail={"message": "Internal server error", "type": "server_error"} )
Frontend Error Handling
// frontend/src/lib/api/client.ts export class APIError extends Error { constructor( message: string, public status: number, public type?: string ) { super(message) } } export async function apiClient<T>( url: string, options?: RequestInit ): Promise<T> { const response = await fetch(url, { ...options, headers: { "Content-Type": "application/json", ...options?.headers, }, }) if (!response.ok) { const error = await response.json().catch(() => ({})) throw new APIError( error.detail?.message || error.detail || "Request failed", response.status, error.detail?.type ) } return response.json() }
// frontend/src/components/task-form.tsx const mutation = useMutation({ mutationFn: createTask, onError: (error: APIError) => { // Error handling basé sur type switch (error.type) { case "validation_error": toast.error(`Validation error: ${error.message}`) break case "permission_error": toast.error("You don't have permission to perform this action") break case "server_error": toast.error("Server error. Please try again later.") break default: toast.error(error.message) } }, })
Principe: Errors backend standardisés avec types → Frontend gère selon type. (Inspiration: Stripe API errors)
Pattern #5: Loading States (React Query)
Liste avec Loading/Error/Empty States
// frontend/src/components/task-list.tsx "use client" import { useQuery } from "@tanstack/react-query" import { Card } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Alert, AlertDescription } from "@/components/ui/alert" import { getTasks } from "@/lib/api/tasks" export function TaskList() { const { data, isLoading, error } = useQuery({ queryKey: ["tasks"], queryFn: getTasks, }) // Loading state if (isLoading) { return ( <div className="space-y-4"> {[...Array(3)].map((_, i) => ( <Card key={i} className="p-4"> <Skeleton className="h-6 w-3/4 mb-2" /> <Skeleton className="h-4 w-full" /> </Card> ))} </div> ) } // Error state if (error) { return ( <Alert variant="destructive"> <AlertDescription> Failed to load tasks: {error.message} </AlertDescription> </Alert> ) } // Empty state if (!data || data.length === 0) { return ( <div className="text-center p-8 text-muted-foreground"> No tasks yet. Create your first task! </div> ) } // Data state return ( <div className="space-y-4"> {data.map((task) => ( <Card key={task.id} className="p-4"> <h3 className="font-semibold">{task.title}</h3> <p className="text-sm text-muted-foreground">{task.description}</p> </Card> ))} </div> ) }
Checklist states:
- ✅ Loading (Skeleton UI)
- ✅ Error (Alert destructive)
- ✅ Empty (Message vide)
- ✅ Data (Liste tasks)
Pattern #6: Optimistic Updates
// frontend/src/hooks/useTasks.ts import { useMutation, useQueryClient } from "@tanstack/react-query" import { updateTask } from "@/lib/api/tasks" import type { TaskResponse, TaskUpdate } from "@/types/task" export function useUpdateTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }: { id: string; data: TaskUpdate }) => updateTask(id, data), // Optimistic update AVANT requête serveur onMutate: async ({ id, data }) => { // Cancel outgoing queries await queryClient.cancelQueries({ queryKey: ["tasks"] }) // Snapshot previous value const previousTasks = queryClient.getQueryData<TaskResponse[]>(["tasks"]) // Optimistically update cache queryClient.setQueryData<TaskResponse[]>(["tasks"], (old) => old?.map((task) => task.id === id ? { ...task, ...data } : task ) || [] ) // Return context avec previous value return { previousTasks } }, // Si erreur → Rollback onError: (err, variables, context) => { if (context?.previousTasks) { queryClient.setQueryData(["tasks"], context.previousTasks) } toast.error("Failed to update task") }, // Après success → Refetch pour sync onSettled: () => { queryClient.invalidateQueries({ queryKey: ["tasks"] }) }, }) }
Principe: Update UI immédiatement (optimistic), rollback si erreur serveur. (Stripe Dashboard pattern)
Checklist Intégration Full-Stack
Backend (FastAPI + Pydantic)
- Pydantic schemas définis (Base, Create, Update, Response)
- Validation Pydantic complète (min/max, regex, custom validators)
- API routes avec error handling standardisé
- Services layer (business logic séparée)
- Error responses structurées (message + type)
Frontend (React + shadcn + Zod)
- TypeScript types mirrored depuis Pydantic
- Zod schemas synchronisés avec Pydantic validation
- API client avec error handling
- React Query setup (mutations + queries)
- Shadcn forms avec react-hook-form + Zod resolver
- Loading states (Skeleton UI)
- Error states (Alert components)
- Empty states (messages appropriés)
- Success feedback (toast notifications)
- Cache invalidation (après mutations)
- Optimistic updates (si applicable)
Anti-Patterns à Éviter
❌ Validation différente frontend vs backend
// Frontend: max 100 chars z.string().max(100) // Backend: max 200 chars Field(..., max_length=200) // → Incohérence = bugs
✅ Validation synchronisée exactement
❌ Types frontend pas à jour
// Backend ajouté field "priority" // Frontend oublie → TypeScript errors partout
✅ Types générés ou mirrored manuellement avec checklist
❌ Pas de loading states
{data?.map(...)} // Pas de loading → Flash vide
✅ Skeleton UI pendant loading
❌ Errors non gérés
onError: () => {} // Error silencieux = mauvaise UX
✅ Toast + error messages clairs
Workflow Executor avec Integration Skill
Quand executor crée feature full-stack:
1. executor détecte: feature nécessite backend + frontend 2. Load skills: backend + frontend + integration 3. Phase A - Backend: - Crée Pydantic schemas (selon integration patterns) - Crée API route avec error handling standardisé - Crée service layer 4. Phase B - Frontend: - Crée TypeScript types (mirrored Pydantic) - Crée Zod schemas (synchronized validation) - Crée API client - Crée shadcn form (complete pattern) - Loading/Error/Empty states 5. Validation: - Checklist intégration complète - Types synchronisés ✓ - Validation identique ✓ - Error handling ✓
Principes
- Type-Safety First - Types synchronisés Pydantic ↔ TypeScript
- Validation Mirrored - Frontend validation = Backend validation
- Error Handling Standardisé - Errors structurés backend → Frontend gère
- Loading States Obligatoires - Skeleton UI, pas flash vide
- Optimistic Updates - Meilleure UX, rollback si erreur
- Defense in Depth - Validation frontend (UX) + backend (sécurité)
Inspiré de:
- Stripe Dashboard (error handling + loading states + optimistic updates)
- Vercel (full-stack TypeScript patterns)
Version: 1.0.0 Last updated: 2025-01-10 Maintained by: executor agent (loaded pour features full-stack)
Pattern #7: Prisma + Next.js Full-Stack (Path B - Type-Safe Native)
Database Schema (Prisma)
// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tasks Task[] @@index([email]) } model Task { id String @id @default(cuid()) title String description String? status TaskStatus @default(PENDING) priority TaskPriority @default(MEDIUM) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String @@index([userId]) @@index([status]) } enum TaskStatus { PENDING IN_PROGRESS COMPLETED } enum TaskPriority { LOW MEDIUM HIGH }
Commandes Prisma:
# Générer client TypeScript npx prisma generate # Créer migration npx prisma migrate dev --name add_tasks # Push schema (dev rapide) npx prisma db push # Ouvrir Prisma Studio npx prisma studio
Prisma Client Singleton (Pattern Vercel)
// lib/prisma.ts import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }) if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Principe: Singleton pour éviter épuisement connexions DB (Vercel best practice)
Next.js API Route avec Prisma (Server Actions)
// app/actions/tasks.ts 'use server' import { revalidatePath } from 'next/cache' import { prisma } from '@/lib/prisma' import { z } from 'zod' import { TaskStatus, TaskPriority } from '@prisma/client' // Validation Zod (synchronized avec Prisma enums) const taskCreateSchema = z.object({ title: z.string().min(1, "Title required").max(200, "Title too long"), description: z.string().optional(), status: z.nativeEnum(TaskStatus).default(TaskStatus.PENDING), priority: z.nativeEnum(TaskPriority).default(TaskPriority.MEDIUM), userId: z.string().cuid(), }) export type TaskCreateInput = z.infer<typeof taskCreateSchema> export async function createTask(input: TaskCreateInput) { try { // Validation const validated = taskCreateSchema.parse(input) // Prisma create const task = await prisma.task.create({ data: validated, include: { user: { select: { id: true, name: true, email: true } } } }) // Revalidate cache revalidatePath('/dashboard') return { success: true, data: task } } catch (error) { if (error instanceof z.ZodError) { return { success: false, error: error.errors[0].message } } return { success: false, error: "Failed to create task" } } } export async function getTasks(userId: string) { const tasks = await prisma.task.findMany({ where: { userId }, include: { user: { select: { id: true, name: true } } }, orderBy: { createdAt: 'desc' } }) return tasks } export async function updateTask(id: string, userId: string, data: Partial<TaskCreateInput>) { try { // Check ownership const task = await prisma.task.findUnique({ where: { id }, select: { userId: true } }) if (!task || task.userId !== userId) { return { success: false, error: "Task not found or access denied" } } // Update const updated = await prisma.task.update({ where: { id }, data, }) revalidatePath('/dashboard') return { success: true, data: updated } } catch (error) { return { success: false, error: "Failed to update task" } } } export async function deleteTask(id: string, userId: string) { try { // Check ownership const task = await prisma.task.findUnique({ where: { id }, select: { userId: true } }) if (!task || task.userId !== userId) { return { success: false, error: "Task not found or access denied" } } await prisma.task.delete({ where: { id } }) revalidatePath('/dashboard') return { success: true } } catch (error) { return { success: false, error: "Failed to delete task" } } }
Avantages Server Actions:
- ✅ Type-safe natif (Prisma types auto-générés)
- ✅ Pas de route API explicite
- ✅ Revalidation cache Next.js intégrée
- ✅ Streaming support
Frontend avec Server Actions + Prisma Types
// app/dashboard/page.tsx (Server Component) import { getTasks } from '@/app/actions/tasks' import { TaskList } from '@/components/task-list' import { getCurrentUser } from '@/lib/auth' export default async function DashboardPage() { const user = await getCurrentUser() const tasks = await getTasks(user.id) return ( <div className="container py-6"> <h1 className="text-3xl font-bold mb-6">Dashboard</h1> <TaskList initialTasks={tasks} userId={user.id} /> </div> ) }
// components/task-list.tsx (Client Component) 'use client' import { useState, useOptimistic } from 'react' import { Task } from '@prisma/client' import { deleteTask, updateTask } from '@/app/actions/tasks' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { toast } from 'sonner' type TaskWithUser = Task & { user: { id: string; name: string } } export function TaskList({ initialTasks, userId }: { initialTasks: TaskWithUser[] userId: string }) { const [tasks, setTasks] = useState(initialTasks) const [optimisticTasks, addOptimisticTask] = useOptimistic( tasks, (state, deletedId: string) => state.filter(t => t.id !== deletedId) ) const handleDelete = async (taskId: string) => { // Optimistic update addOptimisticTask(taskId) // Server action const result = await deleteTask(taskId, userId) if (result.success) { setTasks(tasks.filter(t => t.id !== taskId)) toast.success("Task deleted") } else { // Rollback optimistic update setTasks(tasks) toast.error(result.error) } } const handleStatusChange = async (taskId: string, newStatus: Task['status']) => { const result = await updateTask(taskId, userId, { status: newStatus }) if (result.success) { setTasks(tasks.map(t => t.id === taskId ? { ...t, status: newStatus } : t )) toast.success("Status updated") } else { toast.error(result.error) } } return ( <div className="space-y-4"> {optimisticTasks.map(task => ( <Card key={task.id} className="p-4"> <div className="flex items-center justify-between"> <div> <h3 className="font-semibold">{task.title}</h3> <p className="text-sm text-muted-foreground">{task.description}</p> <p className="text-xs text-muted-foreground mt-1"> Status: {task.status} | Priority: {task.priority} </p> </div> <div className="flex gap-2"> <Button size="sm" variant="outline" onClick={() => handleStatusChange( task.id, task.status === 'PENDING' ? 'IN_PROGRESS' : 'COMPLETED' )} > Next Status </Button> <Button size="sm" variant="destructive" onClick={() => handleDelete(task.id)} > Delete </Button> </div> </div> </Card> ))} </div> ) }
Prisma Relations & Include Patterns
// Get task with user const task = await prisma.task.findUnique({ where: { id }, include: { user: true, // Include relation complète } }) // Select specific fields only (optimisation) const task = await prisma.task.findUnique({ where: { id }, include: { user: { select: { id: true, name: true, email: true } } } }) // Nested includes (deep relations) const user = await prisma.user.findUnique({ where: { id }, include: { tasks: { where: { status: 'PENDING' }, orderBy: { createdAt: 'desc' }, take: 10, } } }) // Count relations const user = await prisma.user.findUnique({ where: { id }, include: { _count: { select: { tasks: true } } } })
Prisma Transactions (ACID guarantees)
// Sequential operations transaction const result = await prisma.$transaction(async (tx) => { // 1. Create task const task = await tx.task.create({ data: { title: "New task", userId } }) // 2. Update user stats await tx.user.update({ where: { id: userId }, data: { tasksCount: { increment: 1 } } }) // 3. Create notification await tx.notification.create({ data: { userId, message: `Task "${task.title}" created` } }) return task }) // Rollback automatique si erreur
Interactive transactions (complexes):
const result = await prisma.$transaction( async (tx) => { // Multiple queries avec logique conditionnelle const user = await tx.user.findUnique({ where: { id: userId } }) if (!user) throw new Error("User not found") if (user.tasksCount >= 100) { throw new Error("Task limit reached") } return await tx.task.create({ data: taskData }) }, { maxWait: 5000, // Max wait acquire lock timeout: 10000, // Max transaction duration } )
Prisma Middleware (Logging, Soft Delete)
// lib/prisma.ts import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() // Logging middleware prisma.$use(async (params, next) => { const before = Date.now() const result = await next(params) const after = Date.now() console.log(`Query ${params.model}.${params.action} took ${after - before}ms`) return result }) // Soft delete middleware prisma.$use(async (params, next) => { if (params.model === 'Task') { if (params.action === 'delete') { // Change to update with deletedAt params.action = 'update' params.args['data'] = { deletedAt: new Date() } } if (params.action === 'findMany' || params.action === 'findFirst') { // Filter out soft deleted params.args.where = { ...params.args.where, deletedAt: null } } } return next(params) }) export { prisma }
Pattern #8: Prisma + Zod Full Validation
Generate Zod from Prisma (automatique)
# Install zod-prisma npm install zod-prisma-types # Update prisma schema # prisma/schema.prisma generator zod { provider = "zod-prisma-types" output = "../src/lib/zod" } # Generate npx prisma generate
Résultat auto-généré:
// src/lib/zod/index.ts (auto-generated) import { z } from 'zod' export const TaskSchema = z.object({ id: z.string().cuid(), title: z.string().min(1).max(200), description: z.string().nullable(), status: z.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED']), priority: z.enum(['LOW', 'MEDIUM', 'HIGH']), createdAt: z.date(), updatedAt: z.date(), userId: z.string().cuid(), }) export const TaskCreateSchema = TaskSchema.omit({ id: true, createdAt: true, updatedAt: true, }) export const TaskUpdateSchema = TaskCreateSchema.partial()
Usage dans Server Actions:
import { TaskCreateSchema } from '@/lib/zod' export async function createTask(input: unknown) { const validated = TaskCreateSchema.parse(input) // Auto-validated return prisma.task.create({ data: validated }) }
Checklist Prisma + Next.js Integration
Setup
- Prisma schema défini (
)schema.prisma - Enums définis pour status/priority/etc
- Relations définies (
)@relation - Indexes créés (
)@@index - Prisma client généré (
)npx prisma generate - Migrations appliquées (
)npx prisma migrate dev
Backend (Server Actions)
- Prisma client singleton (
)lib/prisma.ts - Zod validation schemas (auto-generated ou manuels)
- Server actions avec error handling
- Ownership checks (userId validation)
- Cache revalidation (
)revalidatePath - Transactions si opérations multiples
Frontend (React)
- Prisma types importés (
)@prisma/client - Server Components fetch data (Prisma direct)
- Client Components actions (Server Actions)
- Optimistic updates (
)useOptimistic - Loading states (Suspense boundaries)
- Error boundaries
Performance
- Select only needed fields (pas
si besoin juste id)include: { user: true } - Indexes sur colonnes filtrées/triées
- Connection pooling configuré (Vercel/Railway)
- Pagination si listes longues
Quand utiliser quel Path?
| Path | Stack | Cas d'usage |
|---|---|---|
| Path A (FastAPI) | Python + FastAPI + Pydantic + React + shadcn | Backend Python séparé, API REST, microservices, machine learning intégré |
| Path B (Prisma) | Next.js + Prisma + React + shadcn | Full-stack Next.js, Server Actions, déploiement Vercel simplifié |
Recommandation:
- Path A (FastAPI) si besoin backend Python pur ou API séparée
- Path B (Prisma) si stack Next.js full-stack avec Server Actions
Workflow Executor avec Integration Skill + Prisma
User: "Crée CRUD tasks avec Prisma"
Executor:
1. Load skills: frontend + backend + integration 2. Détecte: Prisma mentionné → Prisma pattern 3. Backend (Server Actions): - Prisma schema (Task model + enums) - npx prisma migrate dev - Server actions (create, get, update, delete) - Zod validation (nativeEnum TaskStatus) 4. Frontend: - Server Component (getTasks Prisma direct) - Client Component (Server Actions + useOptimistic) - Shadcn form 5. Checklist Prisma ✓
Patterns couverts: ✅
- Path A (FastAPI): Pydantic → Zod sync, REST API, React Query
- Path B (Prisma): Prisma native types, Server Actions, useOptimistic
- Prisma best practices (schema, singleton, relations, transactions, middleware)
- Error handling standardisé
- Loading states (Skeleton UI)
- Optimistic updates
Version: 1.2.0 Updated: 2025-01-10 - Nettoyé références tRPC/SQLAlchemy, focus FastAPI + Prisma