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.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: integration
description: 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.
allowed-tools: Read, Write, Edit, Glob, Grep
---
# Integration 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:**
1. **Type-safe API contracts** (Pydantic → TypeScript OU Prisma → TypeScript native)
2. **Validation synchronisée** (Pydantic ↔ Zod OU Zod + Prisma enums)
3. **Forms shadcn → Backend** (FastAPI routes OU Server Actions)
4. **Error handling** (standardisé backend → UI feedback)
5. **Loading states** (React Query OU useOptimistic)
6. **Database patterns** (Prisma best practices)
---
## Pattern #1: Type-Safe API Contract (Path A - FastAPI)
### Backend (Pydantic schemas)
```python
# 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)
```typescript
// 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)
```python
# 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É)
```typescript
// 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
```python
# 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
```typescript
// 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)
```tsx
// 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é)
```python
# 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
```typescript
// 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()
}
```
```tsx
// 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
```tsx
// 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
```tsx
// 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**
```typescript
// 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**
```typescript
// Backend ajouté field "priority"
// Frontend oublie → TypeScript errors partout
```
✅ **Types générés ou mirrored manuellement avec checklist**
---
❌ **Pas de loading states**
```tsx
{data?.map(...)} // Pas de loading → Flash vide
```
✅ **Skeleton UI pendant loading**
---
❌ **Errors non gérés**
```tsx
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
1. **Type-Safety First** - Types synchronisés Pydantic ↔ TypeScript
2. **Validation Mirrored** - Frontend validation = Backend validation
3. **Error Handling Standardisé** - Errors structurés backend → Frontend gère
4. **Loading States Obligatoires** - Skeleton UI, pas flash vide
5. **Optimistic Updates** - Meilleure UX, rollback si erreur
6. **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
// 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([u