Modèles de Développement Backend
Modèles d'architecture backend et bonnes pratiques pour des applications côté serveur évolutives.
Quand Activer
- Concevoir des points de terminaison d'API REST ou GraphQL
- Implémenter les couches repository, service ou controller
- Optimiser les requêtes de base de données (N+1, indexation, pooling de connexions)
- Ajouter de la mise en cache (Redis, en mémoire, en-têtes de cache HTTP)
- Mettre en place des tâches d'arrière-plan ou un traitement asynchrone
- Structurer la gestion des erreurs et la validation pour les API
- Construire des middlewares (authentification, journalisation, limitation de débit)
Modèles de Conception d'API
Structure d'API RESTful
// PASS : URLs basées sur les ressources
GET /api/markets # Lister les ressources
GET /api/markets/:id # Obtenir une ressource unique
POST /api/markets # Créer une ressource
PUT /api/markets/:id # Remplacer une ressource
PATCH /api/markets/:id # Mettre à jour une ressource
DELETE /api/markets/:id # Supprimer une ressource
// PASS : Paramètres de requête pour le filtrage, le tri, la pagination
GET /api/markets?status=active&sort=volume&limit=20&offset=0
Modèle Repository
// Abstraction de la logique d'accès aux données
interface MarketRepository {
findAll(filters?: MarketFilters): Promise<Market[]>
findById(id: string): Promise<Market | null>
create(data: CreateMarketDto): Promise<Market>
update(id: string, data: UpdateMarketDto): Promise<Market>
delete(id: string): Promise<void>
}
class SupabaseMarketRepository implements MarketRepository {
async findAll(filters?: MarketFilters): Promise<Market[]> {
let query = supabase.from('markets').select('*')
if (filters?.status) {
query = query.eq('status', filters.status)
}
if (filters?.limit) {
query = query.limit(filters.limit)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
// Autres méthodes...
}
Modèle Service Layer
// Logique métier séparée de l'accès aux données
class MarketService {
constructor(private marketRepo: MarketRepository) {}
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
// Logique métier
const embedding = await generateEmbedding(query)
const results = await this.vectorSearch(embedding, limit)
// Récupération des données complètes
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
// Tri par similarité
return markets.sort((a, b) => {
const scoreA = results.find(r => r.id === a.id)?.score || 0
const scoreB = results.find(r => r.id === b.id)?.score || 0
return scoreA - scoreB
})
}
private async vectorSearch(embedding: number[], limit: number) {
// Implémentation de la recherche vectorielle
}
}
Modèle Middleware
// Pipeline de traitement requête/réponse
export function withAuth(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Non autorisé' })
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Jeton invalide' })
}
}
}
// Utilisation
export default withAuth(async (req, res) => {
// Le gestionnaire a accès à req.user
})
Modèles de Base de Données
Optimisation des Requêtes
// PASS : BON : Sélectionner uniquement les colonnes nécessaires
const { data } = await supabase
.from('markets')
.select('id, name, status, volume')
.eq('status', 'active')
.order('volume', { ascending: false })
.limit(10)
// FAIL : MAUVAIS : Tout sélectionner
const { data } = await supabase
.from('markets')
.select('*')
Prévention du Problème N+1
// FAIL : MAUVAIS : Problème de requête N+1
const markets = await getMarkets()
for (const market of markets) {
market.creator = await getUser(market.creator_id) // N requêtes
}
// PASS : BON : Extraction par lots
const markets = await getMarkets()
const creatorIds = markets.map(m => m.creator_id)
const creators = await getUsers(creatorIds) // 1 requête
const creatorMap = new Map(creators.map(c => [c.id, c]))
markets.forEach(market => {
market.creator = creatorMap.get(market.creator_id)
})
Modèle de Transaction
async function createMarketWithPosition(
marketData: CreateMarketDto,
positionData: CreatePositionDto
) {
// Utiliser une transaction Supabase
const { data, error } = await supabase.rpc('create_market_with_position', {
market_data: marketData,
position_data: positionData
})
if (error) throw new Error('Échec de la transaction')
return data
}
// Fonction SQL dans Supabase
CREATE OR REPLACE FUNCTION create_market_with_position(
market_data jsonb,
position_data jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
BEGIN
-- La transaction commence automatiquement
INSERT INTO markets VALUES (market_data);
INSERT INTO positions VALUES (position_data);
RETURN jsonb_build_object('success', true);
EXCEPTION
WHEN OTHERS THEN
-- Le rollback se fait automatiquement
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
END;
$$;
Stratégies de Mise en Cache
Couche de Cache Redis
class CachedMarketRepository implements MarketRepository {
constructor(
private baseRepo: MarketRepository,
private redis: RedisClient
) {}
async findById(id: string): Promise<Market | null> {
// Vérifier d'abord le cache
const cached = await this.redis.get(`market:${id}`)
if (cached) {
return JSON.parse(cached)
}
// Absence dans le cache - récupérer depuis la base de données
const market = await this.baseRepo.findById(id)
if (market) {
// Mettre en cache pendant 5 minutes
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
}
return market
}
async invalidateCache(id: string): Promise<void> {
await this.redis.del(`market:${id}`)
}
}
Modèle Cache-Aside
async function getMarketWithCache(id: string): Promise<Market> {
const cacheKey = `market:${id}`
// Essayer le cache
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Absence dans le cache - récupérer depuis la base de données
const market = await db.markets.findUnique({ where: { id } })
if (!market) throw new Error('Marché introuvable')
// Mettre à jour le cache
await redis.setex(cacheKey, 300, JSON.stringify(market))
return market
}
Modèles de Gestion des Erreurs
Gestionnaire d'Erreurs Centralisé
class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message)
Object.setPrototypeOf(this, ApiError.prototype)
}
}
export function errorHandler(error: unknown, req: Request): Response {
if (error instanceof ApiError) {
return NextResponse.json({
success: false,
error: error.message
}, { status: error.statusCode })
}
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Échec de la validation',
details: error.issues
}, { status: 400 })
}
// Journaliser les erreurs inattendues
console.error('Erreur inattendue :', error)
return NextResponse.json({
success: false,
error: 'Erreur interne du serveur'
}, { status: 500 })
}
// Utilisation
export async function GET(request: Request) {
try {
const data = await fetchData()
return NextResponse.json({ success: true, data })
} catch (error) {
return errorHandler(error, request)
}
}
Nouvelle Tentative avec Backoff Exponentiel
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
// Backoff exponentiel : 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError!
}
// Utilisation
const data = await fetchWithRetry(() => fetchFromAPI())
Authentification et Autorisation
Validation du Jeton JWT
import jwt from 'jsonwebtoken'
interface JWTPayload {
userId: string
email: string
role: 'admin' | 'user'
}
export function verifyToken(token: string): JWTPayload {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
return payload
} catch (error) {
throw new ApiError(401, 'Jeton invalide')
}
}
export async function requireAuth(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new ApiError(401, 'Jeton d\'autorisation manquant')
}
return verifyToken(token)
}
// Utilisation dans une route API
export async function GET(request: Request) {
const user = await requireAuth(request)
const data = await getDataForUser(user.userId)
return NextResponse.json({ success: true, data })
}
Contrôle d'Accès Basé sur les Rôles
type Permission = 'read' | 'write' | 'delete' | 'admin'
interface User {
id: string
role: 'admin' | 'moderator' | 'user'
}
const rolePermissions: Record<User['role'], Permission[]> = {
admin: ['read', 'write', 'delete', 'admin'],
moderator: ['read', 'write', 'delete'],
user: ['read', 'write']
}
export function hasPermission(user: User, permission: Permission): boolean {
return rolePermissions[user.role].includes(permission)
}
export function requirePermission(permission: Permission) {
return (handler: (request: Request, user: User) => Promise<Response>) => {
return async (request: Request) => {
const user = await requireAuth(request)
if (!hasPermission(user, permission)) {
throw new ApiError(403, 'Permissions insuffisantes')
}
return handler(request, user)
}
}
}
// Utilisation - La HOF enveloppe le gestionnaire
export const DELETE = requirePermission('delete')(
async (request: Request, user: User) => {
// Le gestionnaire reçoit un utilisateur authentifié avec une permission vérifiée
return new Response('Supprimé', { status: 200 })
}
)
Limitation de Débit
La limitation de débit doit utiliser un magasin partagé tel que Redis, une passerelle, ou le limiteur natif de la plateforme. N'utilisez pas de compteurs en mémoire par processus pour les API de production : ils sont réinitialisés lors du déploiement, sont répartis entre les réplicas, et échouent ouvertement dans les environnements serverless ou multi-instances.
Gardez la couche backend responsable du choix du point d'intégration et de la forme de l'erreur ; utilisez api-design pour le contrat HTTP et security-review pour l'examen des cas d'abus.
Tâches d'Arrière-Plan et Files d'Attente
Modèle de File d'Attente Simple
class JobQueue<T> {
private queue: T[] = []
private processing = false
async add(job: T): Promise<void> {
this.queue.push(job)
if (!this.processing) {
this.process()
}
}
private async process(): Promise<void> {
this.processing = true
while (this.queue.length > 0) {
const job = this.queue.shift()!
try {
await this.execute(job)
} catch (error) {
console.error('Échec du travail :', error)
}
}
this.processing = false
}
private async execute(job: T): Promise<void> {
// Logique d'exécution du travail
}
}
// Utilisation pour l'indexation des marchés
interface IndexJob {
marketId: string
}
const indexQueue = new JobQueue<IndexJob>()
export async function POST(request: Request) {
const { marketId } = await request.json()
// Ajouter à la file d'attente au lieu de bloquer
await indexQueue.add({ marketId })
return NextResponse.json({ success: true, message: 'Travail mis en file d\'attente' })
}
Journalisation et Surveillance
Journalisation Structurée
interface LogContext {
userId?: string
requestId?: string
method?: string
path?: string
[key: string]: unknown
}
class Logger {
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...context
}
console.log(JSON.stringify(entry))
}
info(message: string, context?: LogContext) {
this.log('info', message, context)
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context)
}
error(message: string, error: Error, context?: LogContext) {
this.log('error', message, {
...context,
error: error.message,
stack: error.stack
})
}
}
const logger = new Logger()
// Utilisation
export async function GET(request: Request) {
const requestId = crypto.randomUUID()
logger.info('Récupération des marchés', {
requestId,
method: 'GET',
path: '/api/markets'
})
try {
const markets = await fetchMarkets()
return NextResponse.json({ success: true, data: markets })
} catch (error) {
logger.error('Échec de la récupération des marchés', error as Error, { requestId })
return NextResponse.json({ error: 'Erreur interne' }, { status: 500 })
}
}
Rappel : Les modèles backend permettent de créer des applications côté serveur évolutives et maintenables. Choisissez les modèles adaptés à votre niveau de complexité.