Modèles de Développement Backend
Modèles d'architecture backend et bonnes pratiques pour des applications serveur scalables.
Quand Activer
- Conception d'endpoints API REST ou GraphQL
- Implémentation des couches Repository, Service ou Controller
- Optimisation des requêtes de base de données (N+1, indexation, pooling de connexions)
- Ajout de cache (Redis, mémoire, en-têtes de cache HTTP)
- Configuration de jobs en arrière-plan ou de traitement asynchrone
- Structuration de la gestion des erreurs et de la validation pour les API
- Construction de middleware (authentification, journalisation, limitation de débit)
Modèles de Conception d'API
Structure API RESTful
// BON: 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
// BON: 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
// Logique d'accès aux données abstraite
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 de Couche Service
// 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: 'Unauthorized' })
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
}
// Utilisation
export default withAuth(async (req, res) => {
// Le handler a accès à req.user
})
Modèles de Base de Données
Optimisation des Requêtes
// BON: Ne sélectionner que les colonnes nécessaires
const { data } = await supabase
.from('markets')
.select('id, name, status, volume')
.eq('status', 'active')
.order('volume', { ascending: false })
.limit(10)
// MAUVAIS: Tout sélectionner
const { data } = await supabase
.from('markets')
.select('*')
Prévention du Problème N+1
// 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
}
// BON: Récupération par lot
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 la transaction Supabase
const { data, error } = await supabase.rpc('create_market_with_position', {
market_data: marketData,
position_data: positionData
})
if (error) throw new Error('Transaction failed')
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 démarre 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 le cache d'abord
const cached = await this.redis.get(`market:${id}`)
if (cached) {
return JSON.parse(cached)
}
// Échec du cache - récupérer de 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)
// Échec du cache - récupérer de la base de données
const market = await db.markets.findUnique({ where: { id } })
if (!market) throw new Error('Market not found')
// 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: 'Validation failed',
details: error.issues
}, { status: 400 })
}
// Journaliser les erreurs inattendues
console.error('Unexpected error:', error)
return NextResponse.json({
success: false,
error: 'Internal server error'
}, { 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 Token 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, 'Invalid token')
}
}
export async function requireAuth(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new ApiError(401, 'Missing authorization token')
}
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, 'Insufficient permissions')
}
return handler(request, user)
}
}
}
// Utilisation - HOF enveloppe le handler
export const DELETE = requirePermission('delete')(
async (request: Request, user: User) => {
// Le handler reçoit l'utilisateur authentifié avec l'autorisation vérifiée
return new Response('Deleted', { 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, répartis entre les réplicas, et échouent en mode ouvert dans les environnements serverless ou multi-instances.
Gardez la couche backend responsable du choix du point d'intégration et de la forme des erreurs ; utilisez api-design pour le contrat HTTP et security-review pour l'examen des cas d'abus.
Jobs en 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('Job failed:', error)
}
}
this.processing = false
}
private async execute(job: T): Promise<void> {
// Logique d'exécution du job
}
}
// 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: 'Job queued' })
}
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('Fetching markets', {
requestId,
method: 'GET',
path: '/api/markets'
})
try {
const markets = await fetchMarkets()
return NextResponse.json({ success: true, data: markets })
} catch (error) {
logger.error('Failed to fetch markets', error as Error, { requestId })
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
}
}
Souvenez-vous : Les modèles backend permettent de créer des applications serveur scalables et maintenables. Choisissez des modèles adaptés à votre niveau de complexité.