Best Practices
Guidelines for building secure, performant, and user-friendly DeFi applications.
Pourquoi ces Best Practices
Construire une application DeFi fiable nécessite une attention particulière à la sécurité, aux performanceset à l'expérience utilisateur. Ces best practices sont le fruit de notre expérience avec des centaines d'intégrations YaniPay et des retours de notre communauté de développeurs.
Dans l'écosystème blockchain, les erreurs peuvent avoir des conséquences financières irréversibles. Suivre ces recommandations vous permet de construire des applications robustes qui protègent vos utilisateurs tout en offrant une expérience fluide.
Standards Industrie
Cas d'Utilisation
Audite l'intégration YaniPay avant le lancement pour s'assurer que toutes les best practices de sécurité sont respectées.
✓ Zero vulnérabilité détectée lors de l'audit externe grâce aux guidelines
Optimise les performances de l'application pour gérer 10,000 utilisateurs simultanés.
✓ Temps de réponse < 100ms grâce au caching et debouncing recommandés
Écrit une suite de tests complète pour couvrir tous les edge cases de transaction.
✓ Couverture de 95% et zero bug en production depuis 6 mois
Implémente les patterns UX recommandés pour les états de chargement et erreurs.
✓ NPS score de 72 grâce à une expérience utilisateur fluide
Flux de Sécurité
Voici le flux recommandé pour sécuriser chaque transaction :
Defense in Depth
Security Best Practices
API Key Protection
Do: Use environment variables
Store API keys in .env.local and access via process.env
Don't: Expose keys in client code
Never hardcode API keys or commit .env files to version control
1 // Good: Server-side API calls with protected key 2 export async function getServerSideProps() { 3 const response = await fetch('https://api.yanipay.com/v1/prices', { 4 headers: { 5 'Authorization': `Bearer ${process.env.YANIPAY_SECRET_KEY}`, 6 }, 7 }); 8 return { props: { prices: await response.json() } }; 9 } 10 11 // Bad: Exposing secret key in client-side code 12 // const response = await fetch(url, { 13 // headers: { 'Authorization': 'Bearer sk_live_xxxxx' } // NEVER DO THIS 14 // });
Transaction Security
1 import { getCurrentUser } from '@/lib/auth'; 2 import { checkRateLimit, strictRateLimit } from '@/lib/rate-limit'; 3 import { createApiResponse } from '@/lib/api/response'; 4 import { z } from 'zod'; 5 6 // Zod schema for input validation 7 const transferSchema = z.object({ 8 recipientId: z.string().min(1), 9 amountInCents: z.number().int().positive(), 10 currency: z.enum(['EUR', 'YANI']), 11 }); 12 13 export async function POST(req: NextRequest) { 14 // 1. Rate limiting 15 const rateLimitResult = checkRateLimit(req, strictRateLimit); 16 if (!rateLimitResult.success) { 17 return NextResponse.json( 18 { error: 'Too many requests' }, 19 { status: 429, headers: { 'Retry-After': String(rateLimitResult.retryAfter) } } 20 ); 21 } 22 23 // 2. Authentication 24 const auth = await getCurrentUser(req); 25 if (!auth.success || !auth.user) { 26 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 27 } 28 29 // 3. Input validation 30 const body = await req.json(); 31 const validated = transferSchema.safeParse(body); 32 if (!validated.success) { 33 return NextResponse.json( 34 { error: 'Invalid parameters', details: validated.error.flatten() }, 35 { status: 400 } 36 ); 37 } 38 39 // 4. Business logic with amounts in cents (no float precision issues) 40 const { amountInCents } = validated.data; 41 if (amountInCents > 50_000_00) { // 50 000 EUR maximum 42 return NextResponse.json({ error: 'Amount exceeds limit' }, { status: 422 }); 43 } 44 45 // 5. Execute and return 46 return createApiResponse({ success: true, transactionId: 'tx_...' }); 47 }
Patches de Sécurité (2026-04-09) Mis à jour
20+ CVEs corrigées via pnpm.overrides sans breaking changes — les overrides sont transparents pour les consommateurs directs des packages.
2fed86e0)effecthono@hono/node-serveraxiosminimatchajvtarfast-xml-parserbrace-expansionpicomatchyaml@xmldom/xmldom{
"pnpm": {
"overrides": {
"axios": ">=1.7.4",
"minimatch": ">=3.0.5",
"ajv": ">=6.12.3",
"tar": ">=6.2.1",
"yaml": ">=2.3.4",
"brace-expansion": ">=1.1.12",
"picomatch": ">=2.3.1",
"@xmldom/xmldom": ">=0.8.10",
"fast-xml-parser": ">=4.4.1",
"hono": ">=4.6.4",
"@hono/node-server": ">=1.13.2"
}
}
}Validation Zod & Rate Limiting renforcé Mis à jour
Commit f220a81b — T19+T20+T21 : validation Zod ajoutée sur les routes sensibles, rate limiting renforcé selon le type de route.
| Type de route | Rate Limiter | Limite |
|---|---|---|
| Auth (signin, login) | authRateLimit | 5 req / 15 min |
| Register, KYC, OTP, verify-phone | strictRateLimit | 10 req / min |
| Payments, API générale | apiRateLimit | 100 req / 15 min |
Masquage IBAN dans les réponses API Mis à jour
Commit f220a81b — T21 : les IBANs complets ne sont plus exposés dans les réponses API. Seuls les 4 derniers chiffres sont affichés.
// Masquer l'IBAN — affiche uniquement les 4 derniers chiffres
export function maskIban(iban: string): string {
if (!iban || iban.length < 8) return '****';
const cleaned = iban.replace(/\s/g, '');
return `${cleaned.slice(0, 2)}** **** **** **** ${cleaned.slice(-4)}`;
}
// Exemple : FR76 **** **** **** 4782
// Usage dans une route API :
const account = await prisma.bankAccount.findUnique({ where: { id } });
return createApiResponse({
iban: maskIban(account.iban), // jamais l'IBAN complet
balance: account.balanceCents,
});Audit Logs RGPD/LCB-FT Mis à jour
Commit 465f4216 — T17+T18 : audit logs ajoutés pour traçabilité réglementaire RGPD et LCB-FT. Chaque action sensible est tracée avec horodatage, utilisateur et résultat.
import { prisma } from '@/lib/prisma';
export type AuditAction =
| 'kyc.submitted'
| 'kyc.approved'
| 'kyc.rejected'
| 'payment.created'
| 'payment.refunded'
| 'card.frozen'
| 'user.login'
| 'user.logout'
| 'iban.viewed'
| 'document.uploaded';
export async function auditLog(params: {
action: AuditAction;
userId: string;
targetId?: string;
metadata?: Record<string, unknown>;
}) {
await prisma.auditLog.create({
data: {
action: params.action,
userId: params.userId,
targetId: params.targetId,
metadata: params.metadata ?? {},
createdAt: new Date(),
},
});
}
// Usage dans une route API :
// await auditLog({ action: 'payment.created', userId: auth.user.userId, targetId: tx.id });Performance Optimization
Use Caching
Cache price feeds and static data to reduce API calls
Debounce Inputs
Debounce user inputs to prevent excessive API calls
1 import { useState, useEffect, useMemo } from 'react'; 2 import { useDebounce } from 'use-debounce'; 3 4 export function useOptimizedQuote(tokenIn: string, tokenOut: string, amount: string) { 5 const [quote, setQuote] = useState(null); 6 const [isLoading, setIsLoading] = useState(false); 7 8 // Debounce the amount to prevent excessive API calls 9 const [debouncedAmount] = useDebounce(amount, 300); 10 11 // Memoize the cache key 12 const cacheKey = useMemo( 13 () => `${tokenIn}-${tokenOut}-${debouncedAmount}`, 14 [tokenIn, tokenOut, debouncedAmount] 15 ); 16 17 useEffect(() => { 18 if (!debouncedAmount || parseFloat(debouncedAmount) <= 0) { 19 setQuote(null); 20 return; 21 } 22 23 // Check cache first 24 const cached = sessionStorage.getItem(cacheKey); 25 if (cached) { 26 const { data, timestamp } = JSON.parse(cached); 27 if (Date.now() - timestamp < 10000) { // 10 second cache 28 setQuote(data); 29 return; 30 } 31 } 32 33 // Fetch fresh quote 34 const fetchQuote = async () => { 35 setIsLoading(true); 36 try { 37 const response = await fetch( 38 `/api/defi/dex/quote?tokenIn=${tokenIn}&tokenOut=${tokenOut}&amountIn=${debouncedAmount}` 39 ); 40 const data = await response.json(); 41 setQuote(data); 42 43 // Cache the result 44 sessionStorage.setItem(cacheKey, JSON.stringify({ 45 data, 46 timestamp: Date.now(), 47 })); 48 } finally { 49 setIsLoading(false); 50 } 51 }; 52 53 fetchQuote(); 54 }, [cacheKey, tokenIn, tokenOut, debouncedAmount]); 55 56 return { quote, isLoading }; 57 }
Error Handling
1 'use client'; 2 3 import { useState } from 'react'; 4 import { apiPost } from '@/lib/api/client'; 5 import { ApiError } from '@/lib/api/client'; 6 7 interface TransferPayload { 8 recipientId: string; 9 amountInCents: number; 10 currency: 'EUR' | 'YANI'; 11 } 12 13 export function TransferForm() { 14 const [error, setError] = useState<string | null>(null); 15 const [loading, setLoading] = useState(false); 16 17 const handleTransfer = async (payload: TransferPayload) => { 18 setError(null); 19 setLoading(true); 20 21 try { 22 await apiPost('/api/payments/transfer', payload); 23 } catch (err) { 24 if (err instanceof ApiError) { 25 // ApiError already handles 401 redirect and 429 retryAfter 26 if (err.status === 422) { 27 setError('Amount exceeds authorized limit'); 28 } else if (err.status === 400) { 29 setError('Invalid transfer parameters. Please check the form.'); 30 } else { 31 setError(err.message ?? 'Transfer failed'); 32 } 33 } else { 34 setError('An unexpected error occurred'); 35 console.error('Transfer error:', err); 36 } 37 } finally { 38 setLoading(false); 39 } 40 }; 41 42 return ( 43 <div> 44 {error && ( 45 <div className="p-4 mb-4 bg-red-900/50 border border-red-500 rounded-lg text-red-400"> 46 {error} 47 </div> 48 )} 49 <button disabled={loading} onClick={() => handleTransfer({ recipientId: '...', amountInCents: 1000, currency: 'EUR' })}> 50 {loading ? 'En cours...' : 'Envoyer'} 51 </button> 52 </div> 53 ); 54 }
Testing Strategies
1 /** 2 * @jest-environment node 3 */ 4 5 // Declare mocks BEFORE imports (jest.mock is hoisted) 6 const mockGetCurrentUser = jest.fn(); 7 const mockCheckRateLimit = jest.fn(); 8 const mockTransactionCreate = jest.fn(); 9 10 jest.mock('@/lib/auth', () => ({ 11 getCurrentUser: (...args: unknown[]) => mockGetCurrentUser(...args), 12 })); 13 14 jest.mock('@/lib/rate-limit', () => ({ 15 checkRateLimit: (...args: unknown[]) => mockCheckRateLimit(...args), 16 strictRateLimit: {}, 17 })); 18 19 jest.mock('@/lib/prisma', () => ({ 20 prisma: { 21 transaction: { 22 create: (...args: unknown[]) => mockTransactionCreate(...args), 23 }, 24 }, 25 })); 26 27 import { POST } from '@/app/api/payments/transfer/route'; 28 import { NextRequest } from 'next/server'; 29 30 describe('POST /api/payments/transfer', () => { 31 beforeEach(() => { 32 // Use resetAllMocks (not clearAllMocks) to clear mockResolvedValueOnce queues 33 jest.resetAllMocks(); 34 35 // Default: rate limit OK 36 mockCheckRateLimit.mockReturnValue({ success: true, remaining: 99 }); 37 38 // Default: authenticated user 39 mockGetCurrentUser.mockResolvedValue({ 40 success: true, 41 user: { userId: 'usr_123', email: 'user@yanipay.com', role: 'PARTICULIER' }, 42 }); 43 }); 44 45 it('returns 401 when not authenticated', async () => { 46 mockGetCurrentUser.mockResolvedValueOnce({ success: false }); 47 48 const req = new NextRequest('http://localhost/api/payments/transfer', { 49 method: 'POST', 50 body: JSON.stringify({ recipientId: 'usr_456', amountInCents: 1000, currency: 'EUR' }), 51 }); 52 53 const res = await POST(req); 54 expect(res.status).toBe(401); 55 }); 56 57 it('returns 429 when rate limit exceeded', async () => { 58 mockCheckRateLimit.mockReturnValueOnce({ success: false, retryAfter: 60 }); 59 60 const req = new NextRequest('http://localhost/api/payments/transfer', { 61 method: 'POST', 62 body: JSON.stringify({ recipientId: 'usr_456', amountInCents: 1000, currency: 'EUR' }), 63 }); 64 65 const res = await POST(req); 66 expect(res.status).toBe(429); 67 }); 68 69 it('returns 400 for invalid payload', async () => { 70 const req = new NextRequest('http://localhost/api/payments/transfer', { 71 method: 'POST', 72 body: JSON.stringify({ amountInCents: -100 }), // missing fields + negative amount 73 }); 74 75 const res = await POST(req); 76 expect(res.status).toBe(400); 77 }); 78 79 it('creates transaction for valid request', async () => { 80 mockTransactionCreate.mockResolvedValueOnce({ id: 'tx_abc123' }); 81 82 const req = new NextRequest('http://localhost/api/payments/transfer', { 83 method: 'POST', 84 body: JSON.stringify({ recipientId: 'usr_456', amountInCents: 1000, currency: 'EUR' }), 85 }); 86 87 const res = await POST(req); 88 expect(res.status).toBe(200); 89 expect(mockTransactionCreate).toHaveBeenCalledTimes(1); 90 }); 91 });
User Experience
Show loading states
Always indicate when data is being fetched or transactions are pending
Confirm important actions
Ask for confirmation on high-value transactions or irreversible actions
Display clear error messages
Help users understand what went wrong and how to fix it
Provide transaction feedback
Show transaction status and provide links to block explorer
RBAC — Controle d'accès par rôle
YaniPay utilise quatre rôles Prisma : PARTICULIER, PROFESSIONNEL, EMPLOYE et ADMIN. Toujours inclure ADMIN dans les allowedRolesdes routes protégées.
1 import { getCurrentUser } from '@/lib/auth'; 2 import { checkRateLimit, apiRateLimit } from '@/lib/rate-limit'; 3 import { createApiResponse, unauthorizedResponse, forbiddenResponse } from '@/lib/api/response'; 4 import { NextRequest } from 'next/server'; 5 6 export async function GET(req: NextRequest) { 7 // 1. Rate limiting 8 const rl = checkRateLimit(req, apiRateLimit); 9 if (!rl.success) { 10 return NextResponse.json({ error: 'Too many requests' }, { status: 429 }); 11 } 12 13 // 2. Authentication 14 const auth = await getCurrentUser(req); 15 if (!auth.success || !auth.user) { 16 return unauthorizedResponse(); 17 } 18 19 // 3. RBAC — roles Prisma exacts (pas 'PRO', pas 'USER') 20 const allowedRoles = ['PROFESSIONNEL', 'ADMIN']; 21 if (!allowedRoles.includes(auth.user.role ?? '')) { 22 return forbiddenResponse(); 23 } 24 25 // 4. Business logic 26 return createApiResponse({ stats: {} }); 27 }
Production Checklist
Pre-Launch Audit
Références
Sources officielles et ressources complémentaires :
Restez à Jour
OWASP Security Guide
Standards sécurité Web
React Best Practices
Performance React
Testing Library
Tests React
Nielsen Norman UX
Heuristiques UX
Related Resources
Last updated: 2026-04-09