Maîtriser les IDs en JSON : UUID, nanoid, autoincrement. Bonnes pratiques, sécurité et patterns réutilisables.
Introduction et importance
Un ID (identifiant) est une valeur unique qui représente un objet dans une base de données ou une API. C'est un élément fondamental de toute application web moderne.
Critères d'un bon ID :
- ✅ Unique (jamais de doublon)
- ✅ Immuable (une fois créé, ne change pas)
- ✅ Court (optimise la base de données)
- ✅ Non-prédictible (sécurité)
- ✅ Distribué (scalable, pas de bottleneck central)
- ✅ Performant à générer et indexer
Impact sur votre application :
- ⚡ Performance des requêtes
- 🔒 Sécurité (IDs prédictibles = énumération)
- 📈 Scalabilité (croissance de la base)
- 💾 Taille du stockage
Types d'IDs
| Type | Format | Avantages | Inconvénients |
|---|---|---|---|
| Autoincrement | 1, 2, 3... | Simple, petit, indexé rapide | Prédictible, pas distribué |
| UUID v4 | 550e8400-e29b-41d4-a716-446655440000 | Unique globally, distribué | Long (36 caractères), performance BD |
| UUID v6 | 1ef4c7a9-3e7c-6000-9000-000000000000 | Sortable, performant index | Moins connu, adoption lente |
| Nanoid | V1StGXR_Z5j | Compact (21 chars), rapide, URL-safe | Moins d'adoption que UUID |
| Snowflake | 1234567890123456789 | Sortable, distribué (Twitter) | Complexe à mettre en place |
Autoincrement
Autoincrement = compteur qui s'incrémente automatiquement (1, 2, 3, etc.)
Avantages :
- ✅ Très rapide à générer
- ✅ Petit en taille (4-8 bytes)
- ✅ Excellente performance d'indexation
- ✅ Sortable par défaut (ordre chronologique)
Inconvénients :
- ❌ Prédictible (
/users/1, /users/2— énumération) - ❌ Pas distribué (génération centralisée)
- ❌ Expose données sensibles (nombre total d'users)
- ❌ Problèmes avec multi-DB (collisions possibles)
Exemple SQL :
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
-- ID généré automatiquement = 1
Cas d'usage appropriés :
- ✅ Applications internes
- ✅ Prototypage/MVP
- ✅ IDs non exposés publiquement
- ✅ Single database (pas distribué)
UUID v4 et v6
UUID = Universally Unique Identifier
UUID v4 (Random) :
550e8400-e29b-41d4-a716-446655440000
^ ^ ^ ^ ^ ^
2^122 combinaisons possibles (pratiquement infini)
Génération UUID v4 :
import crypto from 'crypto';
const id = crypto.randomUUID();
console.info(id); // "550e8400-e29b-41d4-a716-446655440000"
UUID v6 (Sortable) - Nouveau !
UUID v6 améliore v4 : commence par timestamp, donc sortable et meilleure perf d'indexation.
import { v6 as uuidv6 } from 'uuid';
const id = uuidv6();
// 1ef4c7a9-3e7c-6000-9000-000000000000 (timestamp au début)
Avantages UUID :
- ✅ Unique globalement (distribué)
- ✅ Pas d'énumération possible
- ✅ Standard officiel (RFC 4122)
- ✅ Compatible partout
Inconvénients UUID :
- ❌ Très long (36 caractères avec tirets)
- ❌ Stockage : 16 bytes vs 4 bytes pour int
- ❌ Index plus lent (v4 est random)
- ❌ Pas sortable (v4 seulement, v6 améliore ça)
Comparaison v4 vs v6 :
// UUID v4 (random) - mauvais pour indexation
550e8400-e29b-41d4-a716-446655440000
6ba7b810-9dad-11d1-80b4-00c04fd430c8
936da01f-9abd-4d9d-80c7-02af85c822a8
// UUID v6 (sortable) - meilleur pour indexation
1ef4c7a9-0000-6000-9000-000000000000
1ef4c7a9-0001-6000-9000-000000000001
1ef4c7a9-0002-6000-9000-000000000002
Nanoid : compact et performant
Nanoid est une alternative moderne à UUID, créée par Vercel (Next.js).
Exemple :
V1StGXR_Z5j
NQlCUeP7u8q
9ZyFfxJK2m_
Installation et utilisation :
npm install nanoid
import { nanoid } from 'nanoid';
const id = nanoid();
console.info(id); // "V1StGXR_Z5j" (21 caractères par défaut)
const shortId = nanoid(10);
console.info(shortId); // "Z5j_V1StGX" (10 caractères)
const customAlphabet = nanoid(12);
console.info(customAlphabet); // IDs de 12 caractères
Avantages Nanoid :
- ✅ Très compact (21 chars vs 36 pour UUID)
- ✅ URL-safe (pas de chars spéciaux)
- ✅ Rapide à générer
- ✅ Bonne entropie (216 bits)
- ✅ Pas de tirets (meilleur pour URLs)
Inconvénients Nanoid :
- ❌ Moins standardisé que UUID
- ❌ Adopté principalement chez Vercel/Node community
- ❌ Non sortable par défaut
Comparaison Tail :
Autoincrement : 1 byte
UUID : 36 chars (16 bytes)
Nanoid : 21 chars (13 bytes)
Génération en JavaScript/Node.js
Autoincrement (Prisma) :
// schema.prisma
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
UUID avec Prisma :
// schema.prisma
import { v4 } from 'uuid';
model User {
id String @id @default(cuid()) // or uuid()
name String
email String @unique
}
Nanoid dans une API Express :
import express from 'express';
import { nanoid } from 'nanoid';
const app = express();
app.use(express.json());
const users = [];
app.post('/users', (req, res) => {
const user = {
id: nanoid(), // Générer un Nanoid unique
name: req.body.name,
email: req.body.email,
createdAt: new Date()
};
users.push(user);
res.status(201).json(user);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);
res.json(user);
});
app.listen(3000);
Générer des IDs en batch :
import { nanoid } from 'nanoid';
// Générer 1000 IDs uniques
const ids = Array.from({ length: 1000 }, () => nanoid());
console.info(ids[0]); // "V1StGXR_Z5j"
console.info(ids[999]); // "9ZyFfxJK2m_"
Stockage et indexation
PostgreSQL avec UUID :
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
-- UUID généré automatiquement
MongoDB avec Nanoid :
import { MongoClient } from 'mongodb';
import { nanoid } from 'nanoid';
const client = new MongoClient(uri);
const db = client.db('myapp');
const users = db.collection('users');
await users.insertOne({
_id: nanoid(), // Utiliser Nanoid comme _id
name: 'Alice',
email: 'alice@example.com'
});
// Requête
const user = await users.findOne({ _id: 'V1StGXR_Z5j' });
Index performance :
-- Index sur ID (toujours créer un index primaire)
CREATE INDEX idx_user_id ON users(id);
-- Requête rapide (O(1) avec index)
SELECT * FROM users WHERE id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
-- Recherche lente sans index (O(n))
SELECT * FROM users WHERE email = 'alice@example.com';
-- Solution : ajouter index aussi
CREATE INDEX idx_user_email ON users(email);
Taille stockage comparée :
| Type | Taille | Pour 1M documents |
|---|---|---|
| INT (Autoincrement) | 4 bytes | 4 MB |
| UUID (36 chars) | 36 bytes | 36 MB |
| Nanoid (21 chars) | 21 bytes | 21 MB |
Sécurité et prédictibilité
❌ Danger : Autoincrement prévisible
GET /api/users/1
GET /api/users/2
GET /api/users/3
-- Attaquant peut énumérer tous les users !
✅ Solution : UUID ou Nanoid non-prédictible
GET /api/users/V1StGXR_Z5j
GET /api/users/NQlCUeP7u8q
-- Impossible d'énumérer sans connaître le format
Entropie des IDs :
Autoincrement : 0 bits d'entropie (100% prédictible)
UUID v4 : 122 bits d'entropie (cryptographiquement sûr)
Nanoid : 216 bits d'entropie (réellement aléatoire)
Bonnes pratiques sécurité :
- ✅ Toujours utiliser UUID/Nanoid pour les ressources publiques
- ✅ Vérifier authorization au-delà de l'ID (ce n'est pas authentification)
- ✅ Ajouter rate limiting pour éviter énumération
- ✅ Hasher/chiffrer les IDs sensibles
Vérification d'authorization :
app.get('/api/posts/:id', authenticateUser, async (req, res) => {
const post = await Post.findById(req.params.id);
// ❌ Mauvais : juste vérifier que l'ID existe
if (!post) return res.status(404).send('Not found');
// ✅ Bon : vérifier que c'est l'utilisateur propriétaire
if (post.userId !== req.user.id) {
return res.status(403).send('Unauthorized');
}
res.json(post);
});
Patterns réels
Pattern 1 : User ID + Ressource ID
// Meilleur pour permission, immutenabilité
POST /api/users/V1StGXR_Z5j/posts
{
"id": "NQlCUeP7u8q",
"title": "...",
"userId": "V1StGXR_Z5j"
}
Pattern 2 : Verifies + UUID pour partage
// Document partageable, ID non-séquentiel
POST /api/documents
{
"id": "3fa85f64-5717-4562-b3fc", // UUID
"shareLink": "https://app.com/share/abc123" // Token unique
}
Pattern 3 : Composite ID
// Useful pour relationner efficacement
{
"id": "org-123|proj-456|task-789",
"organizationId": "org-123",
"projectId": "proj-456",
"taskId": "task-789"
}
Erreurs courantes
❌ Erreur 1 : Autoincrement pour données publiques
❌ Mauvais :
GET /api/public-profiles/1
GET /api/public-profiles/2
-- Énumération facile
✅ Bon :
GET /api/public-profiles/V1StGXR_Z5j
-- Impossible sans connaître l'ID exact
❌ Erreur 2 : Générer UUID via Date
❌ Mauvais :
const fakeId = Math.random().toString(36).substr(2, 9);
// Pas assez d'entropie, collisions possibles
✅ Bon :
import { nanoid } from 'nanoid';
const id = nanoid();
// 216 bits d'entropie, zéro collisions
❌ Erreur 3 : ID trop long dans URL
❌ Mauvais :
/api/users/550e8400-e29b-41d4-a716-446655440000
/api/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
-- URLs longues et inesthétiques
✅ Bon :
/api/users/V1StGXR_Z5j
-- Compact et lisible
❌ Erreur 4 : Pas d'index sur l'ID
❌ Mauvais :
SELECT * FROM users WHERE id = 123;
-- Si pas d'index, scan complet table (O(n))
✅ Bon :
CREATE INDEX idx_users_id ON users(id);
-- Recherche O(1) avec index B-tree
❌ Erreur 5 : Confondre ID et authentification
❌ Mauvais :
app.get('/api/profile/:userId', (req, res) => {
const user = Users.findById(req.params.userId);
// DANGER : n'importe quel ID accède au profil
res.json(user);
});
✅ Bon :
app.get('/api/profile/:userId', authenticateUser, (req, res) => {
// Vérifier que userId === req.user.id
if (req.params.userId !== req.user.id) {
return res.status(403).send('Forbidden');
}
res.json(req.user);
});
Bonnes pratiques
Checklist pour choisir un ID :
- ✅ IDs publiques = UUID/Nanoid (jamais autoincrement)
- ✅ Single database = autoincrement si privé OK
- ✅ Distributed system = UUID v6 ou Nanoid
- ✅ APIs = Nanoid (compact et URL-safe)
- ✅ Mobile apps = Nanoid (taille réduite)
Recommandations par cas :
| Cas d'usage | Recommandation | Raison |
|---|---|---|
| API REST publique | Nanoid | Compact, sûr, URL-safe |
| App interne | Autoincrement | Performance, stockage minimal |
| Microservices | UUID v6 | Distribué, sortable |
| MongoDB | Nanoid dans _id | Compact, compatible |
Configuration recommandée :
// Node.js API moderne
import { nanoid } from 'nanoid';
app.post('/api/resource', (req, res) => {
const resource = {
id: nanoid(), // ID unique
...req.body,
createdAt: new Date(), // Timestamp
updatedAt: new Date(),
version: 1 // Versioning
};
db.create(resource);
res.status(201).json(resource);
});