Auteur/autrice : khalil

  • Indexation des bases de données et optimisation des requêtes, plongée en profondeur

    Indexation des bases de données et optimisation des requêtes, plongée en profondeur

    Si vous me donnez une requête lente et une heure, le correctif est un index plus souvent que toute autre chose. Les index sont l’outil de performance le plus puissant d’une base relationnelle, et c’est aussi là que je vois l’intuition la plus confiante et la plus fausse. Les gens ajoutent un index sur chaque colonne « au cas où », ou ils ajoutent un index multi-colonnes dans le mauvais ordre et se demandent pourquoi le planificateur l’ignore. Voici mon modèle mental de fonctionnement des index et de la manière dont je décide lesquels construire.

    Rien de tout cela ne compte si le schéma en dessous est un désastre, donc si ce n’est pas déjà fait, les fondations viennent d’une bonne conception et normalisation. Supposons ici que le modèle est sain et que nous le rendons rapide.

    Ce qu’est réellement un index

    Un index est une structure de données séparée et triée qui permet à la base de trouver des lignes sans parcourir toute la table. Le choix par défaut dans la plupart des bases relationnelles est un arbre B, qui maintient les clés en ordre trié et prend en charge les recherches d’égalité et les parcours de plage en temps logarithmique. Cet ordre trié est tout l’intérêt, et il explique presque tout ce qu’un index peut et ne peut pas faire.

    Parce que les clés sont triées, un arbre B est excellent pour trois choses. Trouver une valeur précise, trouver une plage de valeurs, et renvoyer des lignes déjà triées pour que la base puisse sauter une étape de tri séparée. Il est inutile pour l’inverse. Une requête qui demande tout sauf une valeur, ou qui enveloppe la colonne dans une fonction que l’index ne connaît pas, ne peut pas utiliser la structure triée et retombe sur un parcours complet.

    Le coût dont personne ne parle

    Chaque index que vous ajoutez rend les lectures plus rapides et les écritures plus lentes. Ce n’est pas un slogan, c’est mécanique. Quand vous insérez, mettez à jour ou supprimez une ligne, la base doit mettre à jour chaque index couvrant les colonnes touchées. Une table avec huit index paie huit petites opérations de maintenance à chaque écriture. Les index prennent aussi de l’espace disque et de la mémoire, et un index gonflé qui ne tient pas en cache cesse d’être l’accélération que vous vouliez.

    Je n’indexe donc pas par défense. J’indexe en réponse à des preuves. Le bon nombre d’index est le plus petit ensemble qui rend vos vraies requêtes rapides, et trouver cet ensemble passe par la lecture des plans de requête plutôt que par la devinette.

    Lire le plan de requête

    La compétence la plus utile en performance de base de données est la lecture de la sortie EXPLAIN. Elle vous dit ce que le planificateur a l’intention de faire, et EXPLAIN ANALYZE vous dit ce qui s’est réellement passé avec des temps réels. Je le lance en permanence.

    -- Voir le plan et les vrais chiffres d'exécution
    EXPLAIN (ANALYZE, BUFFERS)
    SELECT id, placed_at
    FROM orders
    WHERE customer_id = 42
      AND status = 'paid'
    ORDER BY placed_at DESC
    LIMIT 20;

    Ce que je regarde, dans l’ordre. Y a-t-il un parcours séquentiel sur une grande table où j’attendais un parcours d’index ? C’est l’alarme la plus bruyante. À quel point l’estimation de lignes du planificateur est-elle éloignée du nombre réel ? Un grand écart signifie des statistiques périmées et je lance ANALYZE sur la table. Y a-t-il un tri coûteux qu’un index pourrait satisfaire directement ? La même table est-elle parcourue plus d’une fois ?

    Index composites et ordre des colonnes

    Les index multi-colonnes sont là où le plus de points se gagnent et se perdent. La règle que j’ai mis trop de temps à intérioriser est que l’ordre des colonnes est primordial, et il découle de la structure triée. Un index sur (customer_id, status, placed_at) est trié d’abord par customer_id, puis par status, puis par placed_at. Cet ordre lui permet de servir une requête filtrant sur customer_id seul, ou customer_id et status, ou les trois. Il ne peut pas servir efficacement une requête qui filtre uniquement sur status, parce que status n’est pas la colonne de tête.

    La ligne directrice que j’utilise est les colonnes d’égalité d’abord, puis la colonne de plage ou de tri en dernier. Pour la requête ci-dessus, un index sur (customer_id, status, placed_at) est proche de l’idéal. Les deux prédicats d’égalité restreignent la recherche, et parce que placed_at est la dernière colonne et déjà triée, la base peut satisfaire le ORDER BY et le LIMIT sans tri séparé. Un index, pas d’étape de tri, vingt lignes.

    -- Colonnes d'égalité d'abord, puis la colonne de tri
    CREATE INDEX idx_orders_customer_status_time
        ON orders (customer_id, status, placed_at DESC);

    Index couvrants et parcours index-only

    Il y a une astuce supplémentaire. Si un index contient toutes les colonnes dont une requête a besoin, la base peut répondre à la requête depuis le seul index et ne jamais toucher la table. C’est un parcours index-only, et il peut être bien plus rapide parce qu’il évite les lectures aléatoires de retour vers le tas. J’y arrive en incluant les colonnes supplémentaires que la requête renvoie.

    -- INCLUDE ajoute des colonnes de charge utile sans changer la clé de tri
    CREATE INDEX idx_orders_cover
        ON orders (customer_id, status)
        INCLUDE (placed_at, id);

    Je ne fais pas cela partout, parce que des index plus larges coûtent plus cher à maintenir et à stocker. Mais pour une requête chaude et bien comprise qui tourne en permanence, la transformer en parcours index-only est l’un des meilleurs retours sur effort disponibles.

    Les index que le planificateur refusera discrètement

    Un nombre surprenant d’index restent inutilisés à cause de la façon dont la requête est écrite, pas de la façon dont l’index est construit. Les erreurs classiques que je cherche en premier.

    • Fonctions sur la colonne indexée. WHERE lower(email) = ‘x’ ne peut pas utiliser un index simple sur email. Stockez la valeur normalisée, utilisez un type insensible à la casse, ou construisez un index d’expression sur lower(email).
    • Jokers en tête. LIKE ‘foo%’ peut utiliser un arbre B, mais LIKE ‘%foo’ ne le peut pas, parce que l’ordre trié est inutile quand le début de la chaîne est inconnu.
    • Incompatibilités de type. Comparer une colonne texte à un nombre force une conversion qui peut désactiver l’index. Faites correspondre vos types.
    • Faible sélectivité. Un index sur une colonne à deux valeurs possibles aide rarement, parce que lire l’index plus le tas est souvent plus lent qu’un simple parcours. Le planificateur le sait et le saute, à juste titre.

    Index partiels pour données asymétriques

    L’un de mes outils préférés pour les charges réelles est l’index partiel, qui ne couvre que les lignes correspondant à une condition. Si 95 pour cent de vos commandes sont terminées et que vous interrogez presque toujours la petite tranche encore en attente, n’indexer que les lignes en attente vous donne un index minuscule et rapide qui reste chaud en mémoire.

    -- Indexer seulement les lignes que nous cherchons réellement
    CREATE INDEX idx_orders_pending
        ON orders (placed_at)
        WHERE status = 'pending';

    Cela garde l’index petit, ce qui le garde rapide et peu coûteux à maintenir. Pour les tables à forte asymétrie c’est souvent la différence entre un index qui tient en cache et un qui n’y tient pas.

    Comment je travaille réellement

    Ma boucle est ennuyeuse et elle marche. Trouver la requête lente à partir de vraies métriques, pas d’une intuition. Lancer EXPLAIN ANALYZE et le lire attentivement. Identifier si le problème est un index manquant, un mauvais ordre de colonnes, des statistiques périmées, ou une requête écrite d’une façon qui défait l’indexation. Faire un changement. Mesurer à nouveau. Répéter jusqu’à ce que le plan soit propre.

    Je résiste à l’envie d’ajouter cinq index d’un coup, parce qu’alors je ne peux pas dire lequel a aidé et j’ai souscrit à un surcoût d’écriture dont je n’ai peut-être pas besoin. Un changement, une mesure. Et je révise l’ensemble des index périodiquement, parce que les charges dérivent et l’index essentiel d’hier peut devenir le poids mort d’aujourd’hui qui ne fait que ralentir les écritures. Le même soin qui va dans le schéma et le modèle de données va dans le maintien d’index honnêtes. Mesurer, changer une chose, mesurer à nouveau. Cette discipline bat l’astuce à tous les coups.

  • Scaler les bases de données : replicas de lecture, sharding et partitionnement

    Scaler les bases de données : replicas de lecture, sharding et partitionnement

    Presque toutes les histoires de scaling que j ai vécues finissent à la base de données. Vous pouvez mettre du cache, des files, ajouter une centaine de nœuds applicatifs stateless, et tout ça aide jusqu au moment où une seule base de données primaire est la chose que tous ces nœuds attendent. À ce stade, vous devez scaler la couche de données elle même, et c est un autre genre de problème parce que les données ont du poids. Déplacer du calcul est facile. Déplacer et découper des données sans les perdre ni les corrompre, c est là que vit la vraie ingénierie.

    J y pense comme à une échelle. Chaque barreau est plus puissant et plus douloureux que le précédent, et vous ne devriez monter que jusqu où vous en avez réellement besoin.

    D abord, épuiser les gains faciles

    Avant tout changement d architecture, je m assure que la base ne fait pas simplement du travail inutile. La cause la plus fréquente d une base qui semble trop petite, c est une base à qui il manque des index, qui fait des scans séquentiels sur de grandes tables, ou qui se fait marteler par des requêtes qui auraient dû être cachées. L épuisement du pool de connexions se déguise constamment en problème de scaling. Un schéma bien indexé sur une instance bien dimensionnée encaisse bien plus qu on ne le croit, et je couvre les fondations dans les bonnes pratiques de schéma de base de données. Ne shardez pas une base à qui il manque juste un index.

    Les replicas de lecture, le premier vrai levier

    La plupart des applications lisent bien plus qu elles n écrivent. Timelines, fiches produit, tableaux de bord, résultats de recherche, tout ça ce sont des lectures. Donc le premier mouvement structurel est presque toujours les replicas de lecture. Vous gardez une primaire qui accepte toutes les écritures, et vous diffusez ses changements vers une ou plusieurs copies replicas qui servent les lectures. Désormais votre capacité de lecture scale avec le nombre de replicas pendant que les écritures restent sur la primaire.

    Le piège, c est le retard de réplication. Un replica est une copie toujours un peu en retard, généralement de quelques millisecondes mais parfois de quelques secondes sous charge. Cela crée une classe de bug subtile : un utilisateur écrit quelque chose, est redirigé, la lecture part vers un replica pas encore à jour, et sa propre modification semble avoir disparu. La solution, c est le routage read your writes. Après une écriture, envoyez les lectures de cet utilisateur vers la primaire pendant un court instant, ou pour tout ce où l utilisateur s attend à voir sa propre action immédiatement.

    def get_connection(query_type, just_wrote=False):
        if query_type == "write" or just_wrote:
            return primary_pool.get()
        return replica_pool.get()  # round robin sur les replicas
    
    # Apres mise a jour d un profil, lire depuis la primaire brievement
    update_profile(user_id, data)
    profile = read_profile(user_id, just_wrote=True)

    Les replicas vous donnent aussi autre chose que de la capacité. Ils sont un secours à chaud. Si la primaire meurt, vous pouvez promouvoir un replica, ce qui fait des replicas un élément de votre disponibilité et pas seulement de votre performance.

    Le partitionnement, découper sensément une grosse table

    Les replicas multiplient votre capacité de lecture mais chaque replica détient encore tout le jeu de données, et les écritures vont toujours toutes à une primaire. Quand une seule table atteint des centaines de millions de lignes, la table elle même devient le problème. Les index s alourdissent, le vacuum et la maintenance ralentissent, et les requêtes qui touchent toute la table traînent. Le partitionnement découpe une table logique en plusieurs morceaux physiques tout en gardant une seule primaire.

    Le type le plus utile est le partitionnement par plage de temps. La plupart des grandes tables sont en ajout massif et ordonnées dans le temps : événements, logs, commandes, messages. Si vous partitionnez par mois, une requête sur la semaine dernière ne touche qu une seule partition, et supprimer les vieilles données devient un détachement instantané d une partition entière au lieu d un delete massif.

    CREATE TABLE events (
        id        BIGSERIAL,
        user_id   BIGINT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL,
        payload   JSONB
    ) PARTITION BY RANGE (created_at);
    
    CREATE TABLE events_2026_06 PARTITION OF events
        FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
    
    CREATE TABLE events_2026_07 PARTITION OF events
        FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');

    Ce qu il faut comprendre, c est que le partitionnement reste un seul serveur de base de données. Il aide pour la taille des tables, la maintenance, et les requêtes qui peuvent élaguer vers une seule partition. Il ne fait rien pour le débit d écriture de la machine dans son ensemble, parce que toutes les partitions vivent sur la même machine.

    Le sharding, le dernier barreau et le plus lourd

    Quand une seule primaire ne suit plus les écritures quelle que soit la taille de la machine, vous devez répartir les données sur plusieurs serveurs de base de données indépendants. C est le sharding. Chaque shard est sa propre base détenant un sous ensemble des données, et aucune machine ne les a toutes. C est le mouvement qui scale enfin les écritures horizontalement, et c est aussi celui qui vous coûte le plus.

    Tout dépend de la clé de sharding, la colonne que vous utilisez pour décider sur quel shard vit une ligne. Choisissez bien et la plupart des requêtes touchent un seul shard. Choisissez mal et vous créez des shards chauds qui prennent tout le trafic pendant que d autres restent inactifs, ou vous forcez les requêtes à s éventer sur tous les shards et à rassembler les résultats, ce qui est lent et fragile.

    • Le sharding par hachage répartit les lignes uniformément en hachant la clé. Excellent pour une distribution uniforme, mauvais pour les requêtes par plage parce que les lignes liées se dispersent partout.
    • Le sharding par plage garde les clés liées ensemble, ce qui est bon pour les scans par plage mais tend à créer des points chauds sur la plage la plus récente.
    • Le sharding par annuaire garde une table de correspondance explicite entre clés et shards. Le plus flexible, il permet de rééquilibrer, mais l annuaire lui même devient quelque chose à scaler et à protéger.

    La clé de sharding doit correspondre à la façon dont vous interrogez réellement. Si vous gérez une application multi tenant, sharder par identifiant de tenant est généralement idéal parce que presque chaque requête est déjà cantonnée à un tenant, donc elle atterrit naturellement sur un seul shard. Si vous avez un jour besoin d une requête sans la clé de sharding, vous regardez un scatter gather sur tous les shards, et vous devez concevoir durement pour éviter ça sur les chemins chauds.

    Ce que vous abandonnez en shardant

    Je veux être franc sur les coûts, parce que le sharding se fait romancer. Les jointures inter shards cessent en pratique d exister. Vous dénormalisez ou vous joignez dans la couche applicative. Les transactions qui couvrent plusieurs shards exigent une machinerie de transactions distribuées lente et complexe, donc vous reconcevez pour garder chaque transaction dans un seul shard. Les identifiants globalement uniques ont besoin d un schéma qui ne dépend pas d une séquence unique, donc vous passez aux UUID ou à un générateur de type snowflake. Rééquilibrer quand un shard se remplit est un vrai projet, pas un changement de config. Et chacun de ces coûts est permanent. Une fois que vous shardez, vous vivez avec.

    C est exactement pour ça que le sharding est le dernier barreau. Vous n y montez qu après avoir poussé les replicas, le partitionnement, le cache et les files aussi loin qu ils peuvent aller.

    Comment j enchaîne le tout

    Le chemin que je suis presque à chaque fois ressemble à ceci. Indexer et régler jusqu à épuiser les gains faciles. Ajouter des replicas de lecture et router les lectures hors de la primaire. Partitionner les tables géantes pour que la maintenance et l élagage restent sains. Utiliser le cache et les files, que je couvre dans les patterns de scalabilite backend, pour soulager à la fois les lectures et les écritures. Et seulement quand la primaire d écriture elle même est le plafond dur, je shard, et je traite le choix de la clé de sharding comme la décision la plus importante de tout l effort.

    La leçon d ensemble, c est que scaler une base est une séquence de compromis, pas une mise à niveau unique. Chaque barreau achète de la capacité et facture de la complexité. Les ingénieurs qui se mettent en difficulté sont ceux qui sautent directement au pattern le plus puissant parce qu il sonne impressionnant, et qui passent ensuite deux ans à payer une complexité distribuée qu ils auraient pu différer longtemps avec deux replicas et un bon index.

  • Bonnes pratiques de workflow Git et de branches

    Bonnes pratiques de workflow Git et de branches

    J’ai travaille dans des equipes qui traitaient Git comme un rituel sacre et dans d’autres qui le traitaient comme un tiroir fourre-tout. Aucun de ces extremes ne livre du bon logiciel. Apres des annees a nettoyer des historiques chaotiques et a demeler des catastrophes de fusion, j’ai adopte une serie d’habitudes qui gardent les choses calmes. Aucune n’est ingenieuse. C’est justement le but.

    Choisissez un modele de branches et cessez d’en debattre

    Le modele compte moins que l’accord. Pour la plupart des equipes produit, j’utilise le developpement sur tronc commun avec des branches de fonctionnalite a courte duree de vie. Vous partez de main, vous travaillez un jour ou deux, vous fusionnez, vous supprimez la branche. Plus une branche vit longtemps, plus elle derive, et plus la fusion finale devient penible. Les branches de version a longue duree ont leur place dans un logiciel qui sort a cadence fixe vers des clients qui ne peuvent pas se mettre a jour a la demande, mais pour une application web deployee plusieurs fois par jour, elles ne sont que du poids inutile.

    Ce que j’evite activement, c’est le montage GitFlow elabore ou develop, release, hotfix et feature s’entrelacent. J’ai vu ce systeme derouter les nouveaux pendant des semaines. Si votre deploiement est continu, vos branches devraient l’etre aussi.

    Ecrivez des commits qui expliquent le pourquoi

    Un message de commit est une note pour celui qui lira l’historique a 2h du matin pendant un incident, et cette personne pourrait etre vous. Le diff montre deja ce qui a change. Le message doit capturer pourquoi. Je garde la ligne de sujet sous environ cinquante caracteres, a l’imperatif, et j’utilise le corps pour expliquer le raisonnement quand le changement n’est pas evident.

    fix: empeche le double paiement lors d'une nouvelle tentative
    
    Le client de paiement reessayait sur un 504 alors que
    le debit etait deja passe cote passerelle. On clef
    desormais la requete avec un jeton d'idempotence pour
    que la passerelle deduplique. Ferme #482.

    Les commits atomiques sont l’autre moitie de l’affaire. Un changement logique par commit. Quand un commit fait trois choses sans rapport, vous ne pouvez jamais en annuler une seule proprement, et le bisect devient inutile. Si vous ecrivez « et » dans une ligne de sujet, ce sont deux commits.

    Rebasez votre propre travail, fusionnez le travail partage

    C’est la regle qui evite le plus de douleur. Avant d’ouvrir une pull request, je rebase ma branche sur la derniere version de main pour que mes changements reposent sur la realite actuelle et que la relecture soit fluide. Mais des qu’une branche est partagee ou qu’une PR est ouverte et que d’autres l’ont regardee, j’arrete de rebaser et je fusionne, car reecrire un historique publie oblige tous les autres a recuperer leur etat local.

    • Rebasez pour ranger vos commits locaux avant qu’ils soient publics.
    • Utilisez le rebase interactif pour ecraser les inevitables commits « correction de faute » et « vraie correction ».
    • Ne forcez jamais le push d’une branche sur laquelle d’autres construisent.
    • Protegez main pour que personne ne puisse y pousser directement.

    Gardez main toujours livrable

    La propriete la plus precieuse d’un depot, c’est que main fonctionne toujours. Si main est au vert, vous pouvez sortir une version a tout moment, et un deploiement casse se repare en annulant une seule fusion. J’impose cela avec des verifications obligatoires : les tests et le linting doivent passer avant meme que le bouton de fusion apparaisse. Cela rejoint directement ma facon de mener les relectures, que j’ai abordee dans les bonnes pratiques de revue de code. Un historique propre rend les relectures plus rapides, et de bonnes relectures gardent l’historique propre. Les deux se nourrissent mutuellement.

    Rendez l’annulation banale

    Quand quelque chose casse en production, l’action sure la plus rapide est en general d’annuler, pas de deboguer en direct. Ecraser chaque PR en un seul commit sur main rend cela trivial : une PR est un commit, et l’annuler retire toute la fonctionnalite proprement. J’aime les fusions ecrasees pour cette raison precise sur le code applicatif, meme si pour les bibliotheques ou l’historique de chaque commit a une vraie valeur, je conserve l’historique complet.

    Etiquetez vos versions pour toujours pouvoir repondre a « qu’est-ce qui tournait mardi dernier ». Une etiquette legere ne coute rien et transforme une question vague en reponse d’une ligne.

    Quelques habitudes qui paient discretement

    • Validez un .gitignore sense des le premier jour pour que secrets et artefacts de build n’entrent jamais dans l’historique. Retirer un identifiant fuite de l’historique gache un apres-midi.
    • Tirez avec rebase par defaut pour eviter le bruit des commits de fusion a chaque synchro.
    • Gardez des PR petites. Une PR de 200 lignes recoit une vraie relecture. Une PR de 2000 lignes recoit un tampon.

    Git recompense la discipline plus que le savoir. Vous n’avez pas besoin de memoriser les commandes de plomberie. Vous avez besoin d’un petit ensemble d’accords que tout le monde suit vraiment. Le meme raisonnement revient quand je concois des stockages de donnees, ce que j’ai couvert dans les bonnes pratiques de schema de base de donnees, ou quelques conventions fermes tot epargnent un nettoyage enorme plus tard.

  • Guide de conception d’API REST

    Guide de conception d’API REST

    Une API est une promesse. Des qu’un client en depend, chaque bizarrerie que vous avez livree devient permanente, parce que quelqu’un quelque part a ecrit du code contre cette bizarrerie. J’ai maintenu des API pendant des annees et celles qui ont bien vieilli partageaient le meme trait : elles etaient ennuyeuses et previsibles. Voici comment j’y arrive.

    Modelisez les ressources en noms, prenez les verbes a HTTP

    L’URL devrait nommer une chose. La methode dit ce que vous lui faites. Je vois sans cesse des points d’entree comme /getUser et /createOrderNow, et ils combattent tout l’interet de HTTP. Une conception propre utilise des noms au pluriel pour les collections et laisse la methode porter l’action.

    GET    /orders          liste les commandes
    POST   /orders          cree une commande
    GET    /orders/42       recupere une commande
    PATCH  /orders/42       met a jour des champs
    DELETE /orders/42       la supprime
    
    GET    /orders/42/items ressource imbriquee

    PATCH pour les mises a jour partielles et PUT pour le remplacement complet est une distinction a garder. La plupart des vraies mises a jour touchent quelques champs, donc PATCH est ce que je prends, et PUT devient le cas rare ou le client possede vraiment toute la representation.

    Utilisez les codes de statut comme les clients l’attendent

    Renvoyez le code de statut qui correspond a la realite. Un 200 sur une requete echouee parce qu’on « a mis l’erreur dans le corps » casse tout client generique et tout outil de supervision qui lit la ligne de statut. L’ensemble que j’utilise couvre presque tout :

    • 200 pour une lecture ou une mise a jour reussie, 201 quand vous avez cree quelque chose.
    • 400 pour une entree malformee, 422 quand l’entree est bien formee mais semantiquement invalide.
    • 401 quand vous ne savez pas qui ils sont, 403 quand vous le savez et qu’ils n’ont pas le droit.
    • 404 pour une ressource manquante, 409 pour un conflit comme un doublon.
    • 500 uniquement pour de vraies pannes serveur, jamais pour une erreur du client.

    Rendez les erreurs lisibles par la machine

    Un corps d’erreur devrait aider le code appelant a reagir, pas juste afficher une chaine. Je renvoie un code machine stable a cote d’un message humain, pour que les clients puissent brancher sur le code sans analyser une prose que je pourrais reformuler plus tard.

    {
      "error": {
        "code": "insufficient_funds",
        "message": "La carte a ete refusee.",
        "field": "payment_method"
      }
    }

    La coherence compte ici plus que l’ingeniosite. Chaque erreur de l’API devrait avoir la meme forme, pour qu’un client ecrive un seul gestionnaire d’erreur au lieu de dix. C’est le meme instinct que j’apporte aux logs, dont j’ai parle dans les bonnes pratiques d’observabilite : la structure vaut mieux que la prose quand autre chose doit la lire.

    Prevoyez le versionnage avant d’en avoir besoin

    Vous devrez faire un changement cassant tot ou tard. Decidez comment avant de livrer la v1. Je mets la version dans le chemin, /v1/orders, parce qu’elle est visible, cacheable et triviale a router. Le versionnage par en-tete est plus elegant sur le papier et plus penible en pratique quand quelqu’un debogue avec curl. Quel que soit votre choix, la regle est de ne jamais casser une version existante. Les ajouts comme de nouveaux champs optionnels vont bien. Retirer un champ ou changer son type est une nouvelle version.

    Paginez et filtrez des le premier jour

    Toute collection qui peut grandir grandira, et un GET qui renvoie dix mille lignes finira par expirer et emporter une base de donnees avec lui. J’ajoute la pagination a chaque point d’entree de liste des le depart, meme quand les donnees sont minuscules, parce que l’ajouter plus tard est un changement cassant. La pagination par curseur gere mieux les grands jeux de donnees mouvants que l’offset, qui derive quand des lignes sont inserees en cours de parcours.

    • Renvoyez un curseur stable et un signal clair de « il y en a plus ».
    • Autorisez le filtrage par parametres de requete, et documentez exactement quels champs sont filtrables.
    • Plafonnez la taille de page cote serveur pour qu’un client ne puisse pas tout demander d’un coup.

    Soyez strict sur ce que vous acceptez, genereux sur ce que vous renvoyez

    Validez l’entree durement a la frontiere et rejetez tout ce qui est malforme avec un 400 ou 422 clair. Plus vous attrapez les mauvaises donnees pres du bord, moins elles peuvent corrompre l’aval, ce qui ramene directement aux contraintes sur lesquelles je m’appuie dans les bonnes pratiques de schema de base de donnees. Cote sortie, gardez les reponses stables et previsibles pour que les clients puissent faire confiance a la forme. Une API stricte a la porte et coherente a la sortie est une API qu’on aime construire, et cette bonne volonte est ce qui fait adopter votre plateforme.

  • Débuter avec le moteur de jeu Godot 4

    Débuter avec le moteur de jeu Godot 4

    J’ai pris Godot en main pour une petite game jam il y a deux ans et je ne suis jamais revenu à autre chose pour le 2D. J’avais utilisé Unity avant, et le contraste a été immédiat. Godot s’ouvre en moins de trois secondes, l’éditeur lui même est un seul téléchargement autonome d’une centaine de mégaoctets, et le tout est open source sous licence MIT. Cette dernière partie m’était indifférente jusqu’au jour où Unity a annoncé des frais sur le runtime et j’ai compris à quel point posséder ses outils compte. Cet article est l’orientation que j’aurais aimé qu’on me donne le premier jour.

    Ce qu’est réellement Godot

    Godot est un moteur gratuit et open source pour les jeux 2D et 3D. Il tourne sur Windows, macOS et Linux, et il exporte vers toutes les plateformes de bureau ainsi que le web, Android et iOS. Aucun compte à créer, aucun serveur de licence, aucun écran de démarrage sur votre jeu fini. Vous téléchargez l’éditeur, vous le décompressez, et vous le lancez. Je garde trois versions différentes sur disque parce que changer revient simplement à lancer un autre exécutable.

    La version qui a tout changé est Godot 4. Elle a apporté un moteur de rendu réécrit basé sur Vulkan, une vraie illumination globale pour la 3D, et une couche de script bien plus rapide. Si vous démarrez aujourd’hui, démarrez sur Godot 4. La plupart des tutoriels plus anciens visent Godot 3, et les différences d’API vous feront constamment trébucher.

    Nœuds et scènes, tout le modèle mental

    Tout dans Godot est un nœud. Un sprite est un nœud. Un lecteur audio est un nœud. Une forme de collision est un nœud. Vous organisez les nœuds en arborescence, et une arborescence sauvegardée s’appelle une scène. C’est vraiment toute l’architecture, et une fois que ça fait tilt vous arrêtez de vous battre contre le moteur.

    La partie qui m’a pris une semaine à intégrer, c’est que les scènes s’imbriquent. Une pièce de monnaie est une scène avec un sprite, une zone de collision et un son. Votre niveau est une scène qui contient des dizaines de scènes de pièces comme enfants. Votre jeu entier est une scène qui échange les niveaux. Il n’y a pas de concept séparé de prefab comme dans d’autres moteurs, parce que chaque scène est déjà réutilisable par conception.

    Voici comment je réfléchis au nœud à choisir :

    • Node2D est la base pour tout ce qui a une position, une rotation et une échelle dans l’espace 2D.
    • CharacterBody2D est ce qu’il vous faut pour un joueur ou un ennemi qui se déplace et entre en collision mais que vous contrôlez directement en code.
    • Area2D détecte les chevauchements sans rien pousser, parfait pour les objets à ramasser, les déclencheurs et les zones de dégâts.
    • Sprite2D et AnimatedSprite2D dessinent vos visuels.
    • CanvasLayer contient votre interface pour qu’elle reste fixe pendant que la caméra bouge.

    GDScript n’est pas Python, mais ça y ressemble

    Godot est livré avec son propre langage de script appelé GDScript. Il ressemble beaucoup à Python, avec des blocs basés sur l’indentation et un typage dynamique, mais il est intégré au moteur donc il connaît nativement les nœuds et l’arborescence de scène. Vous pouvez aussi utiliser C# si nécessaire, et il existe une voie C++ via GDExtension pour le code critique en performance, mais j’ai sorti de vrais jeux entièrement en GDScript sans jamais buter sur un mur.

    Chaque script étend un type de nœud. Les deux fonctions que vous écrirez sans cesse sont _ready, qui s’exécute une fois quand le nœud entre dans l’arbre, et _process, qui s’exécute à chaque image. Voici un petit script qui fait tourner un sprite et affiche un message :

    extends Sprite2D
    
    # Vitesse de rotation, en radians par seconde
    var spin_speed := 2.0
    
    func _ready() -> void:
        print("Le sprite est prêt")
    
    func _process(delta: float) -> void:
        # delta est le temps écoulé depuis la dernière image, donc le
        # mouvement reste constant quelle que soit la fréquence d'images
        rotation += spin_speed * delta
    

    Remarquez les indices de type optionnels avec les deux points et le deux points égal pour les types inférés. Je recommande fortement de typer vos variables. L’éditeur vous offre une meilleure autocomplétion, et le compilateur attrape des erreurs que vous trouveriez sinon à l’exécution.

    Le système de signaux, ou comment les nœuds se parlent

    Godot s’appuie beaucoup sur un motif de publication et abonnement appelé signaux. Un bouton émet un signal pressed. Une Area2D émet un signal body_entered quand quelque chose la chevauche. Vous connectez ces signaux à des fonctions, soit en glissant dans l’éditeur, soit en code. Cela garde vos nœuds découplés, parce qu’une pièce n’a pas besoin de connaître le joueur, elle annonce simplement qu’elle a été touchée.

    Vous pouvez connecter un signal en code comme ceci :

    extends Area2D
    
    func _ready() -> void:
        body_entered.connect(_on_body_entered)
    
    func _on_body_entered(body: Node2D) -> void:
        print("Quelque chose est entré : ", body.name)
        queue_free()
    

    L’appel à queue_free retire le nœud en toute sécurité à la fin de l’image. Familiarisez vous tôt avec les signaux. Lutter contre eux en interrogeant l’état à chaque image est l’erreur de débutant la plus courante que je vois.

    La disposition de l’éditeur où vous allez vivre

    L’éditeur a un dock Scène à gauche qui montre votre arborescence de nœuds, un dock FileSystem en dessous pour vos fichiers de projet, un Inspecteur à droite pour ajuster les propriétés, et la fenêtre de visualisation au milieu. Le panneau du bas abrite la console de sortie, le débogueur et l’éditeur d’animation. Je garde le débogueur ouvert en permanence parce que les traces de pile sont bonnes et que l’arbre de scène distant permet d’inspecter un jeu en cours d’exécution en direct.

    Une habitude qui a payé : organisez votre projet en dossiers dès le départ. J’utilise un dossier scenes, un dossier scripts, un dossier assets pour les visuels et l’audio, et un dossier autoload pour les singletons globaux. Godot n’impose aucune structure, donc la discipline vous revient.

    Les autoloads pour l’état global

    Quand vous avez besoin de quelque chose d’accessible partout, comme un gestionnaire de score ou un bus audio, vous enregistrez une scène ou un script comme autoload dans les paramètres du projet. Il devient un singleton qui se charge avant tout le reste et persiste à travers les changements de scène. C’est ainsi que je gère l’inventaire du joueur, les réglages et les données de sauvegarde. N’en abusez pas, mais pour les préoccupations vraiment globales c’est l’outil le plus propre que Godot offre.

    Exporter votre jeu

    Quand vous êtes prêt à partager, vous installez les modèles d’export une fois puis vous choisissez un préréglage pour chaque plateforme. L’export web est mon préféré pour les jams parce que vous téléversez un dossier et n’importe qui peut jouer dans un navigateur instantanément, sans téléchargement. Les exports de bureau produisent un seul exécutable. Tout le processus prend quelques minutes une fois configuré.

    Où aller ensuite

    Le moyen le plus rapide d’apprendre Godot est de construire quelque chose de minuscule et de le terminer. Ne commencez pas par votre RPG de rêve. Faites un jeu où une chose bouge et une chose se produit quand vous en touchez une autre. Une fois les bases du moteur acquises, l’étape naturelle suivante est de les assembler en quelque chose de jouable. J’ai écrit un guide complet exactement là dessus dans créer un jeu 2D dans Godot avec GDScript, où nous branchons un personnage qui se déplace, des collisions et un score à partir de zéro.

    Godot récompense ceux qui livrent. Le moteur s’écarte de votre chemin, la communauté est généreuse, et la documentation est vraiment excellente. Téléchargez le, ouvrez un projet vierge, et ajoutez votre premier nœud dès aujourd’hui.

  • Créer un jeu 2D dans Godot avec GDScript

    Créer un jeu 2D dans Godot avec GDScript

    La meilleure façon d’apprendre un moteur est de terminer un petit jeu, pas de lire indéfiniment. Donc dans cet article nous construisons un jeu 2D complet, certes minuscule, dans Godot 4. Un joueur se déplace, ramasse des pièces pour augmenter un score, et meurt si un ennemi le touche. À la fin vous aurez touché à tous les systèmes nécessaires pour la plupart des projets 2D. Si vous n’avez jamais ouvert Godot, lisez d’abord mon guide débuter avec le moteur de jeu Godot 4, car je suppose ici que vous savez ce qu’est un nœud et une scène.

    Mise en place du projet et de la scène du joueur

    Créez un nouveau projet avec le moteur de rendu Forward Plus. La première scène que nous construisons est le joueur. Ajoutez un CharacterBody2D comme racine, puis donnez lui trois enfants : un Sprite2D pour le visuel, une CollisionShape2D pour le corps physique, et une Camera2D pour que la vue suive le joueur. Réglez la forme de collision sur une capsule ou un rectangle qui correspond grossièrement à votre sprite. Sauvegardez la scène sous player.tscn.

    CharacterBody2D est la bonne base ici parce qu’il nous donne une vélocité intégrée et une méthode move_and_slide qui gère la réponse aux collisions sans qu’on fasse le calcul vectoriel à la main.

    Déplacer le joueur

    Avant d’écrire le code de déplacement, définissez quelques actions d’entrée. Ouvrez les Paramètres du projet, allez dans l’onglet Contrôles, et ajoutez quatre actions nommées move_up, move_down, move_left et move_right. Liez chacune aux touches fléchées et aux touches WASD. Associer les entrées à des actions nommées plutôt que de coder les touches en dur signifie que vous pourrez prendre en charge les manettes et la reconfiguration plus tard sans rien réécrire.

    Attachez maintenant un script à la racine du joueur. Voici la logique de déplacement :

    extends CharacterBody2D
    
    # Pixels par seconde
    @export var speed: float = 220.0
    
    func _physics_process(delta: float) -> void:
        # Construit un vecteur de direction à partir des quatre actions.
        # get_axis renvoie une valeur de -1 à 1 pour chaque paire.
        var direction := Vector2.ZERO
        direction.x = Input.get_axis("move_left", "move_right")
        direction.y = Input.get_axis("move_up", "move_down")
    
        # Normalise pour que le déplacement diagonal ne soit pas plus rapide
        if direction.length() > 1.0:
            direction = direction.normalized()
    
        velocity = direction * speed
        move_and_slide()
    

    Quelques points à souligner. Nous utilisons _physics_process plutôt que _process parce que tout ce qui déplace un corps physique doit tourner sur le pas de temps fixe de la physique. L’annotation @export rend speed modifiable dans l’Inspecteur, donc je peux l’ajuster sans toucher au code. Et normaliser la direction supprime le bug classique où se déplacer en diagonale est environ quarante pour cent plus rapide que tout droit.

    Fabriquer une pièce à ramasser

    Créez une deuxième scène avec une Area2D comme racine. Area2D détecte les chevauchements sans bloquer physiquement quoi que ce soit, ce qui est exactement ce dont un objet à ramasser a besoin. Donnez lui un Sprite2D et une CollisionShape2D. Sauvegardez sous coin.tscn et attachez ce script :

    extends Area2D
    
    # Émis quand le joueur attrape cette pièce
    signal collected
    
    func _ready() -> void:
        body_entered.connect(_on_body_entered)
    
    func _on_body_entered(body: Node2D) -> void:
        if body.is_in_group("player"):
            collected.emit()
            queue_free()
    

    Nous déclarons notre propre signal appelé collected et nous l’émettons quand un joueur entre. La pièce ne sait pas ce qu’est un score, et elle ne devrait pas. Elle annonce simplement qu’elle a été ramassée et se retire. Pour que la vérification de groupe fonctionne, sélectionnez le nœud joueur, ouvrez le dock Nœud à côté de l’Inspecteur, passez à Groupes, et ajoutez le à un groupe nommé player.

    Suivre le score avec un global

    Le score doit survivre à toute scène individuelle, donc enregistrez le comme autoload. Créez un script appelé game_state.gd :

    extends Node
    
    var score: int = 0
    
    signal score_changed(new_score: int)
    
    func add_points(amount: int) -> void:
        score += amount
        score_changed.emit(score)
    
    func reset() -> void:
        score = 0
        score_changed.emit(score)
    

    Enregistrez le dans les Paramètres du projet sous l’onglet Globaux avec le nom GameState. Maintenant n’importe quel script du jeu peut appeler GameState.add_points et écouter le signal score_changed pour mettre à jour l’affichage. C’est la façon la plus propre de partager un état dans Godot sans emmêler vos scènes.

    Brancher le niveau et l’interface

    Construisez une scène de niveau. Déposez y une instance du joueur, dispersez plusieurs instances de pièces autour, et ajoutez un CanvasLayer avec un Label à l’intérieur pour le score. Le CanvasLayer garde le label fixé à l’écran même quand la caméra suit le joueur. Attachez un petit script au label :

    extends Label
    
    func _ready() -> void:
        GameState.score_changed.connect(_on_score_changed)
        _on_score_changed(GameState.score)
    
    func _on_score_changed(new_score: int) -> void:
        text = "Score : " + str(new_score)
    

    Pour que chaque pièce ajoute réellement au score, connectez son signal collected à un gestionnaire dans le script du niveau qui appelle GameState.add_points(10). Vous pouvez le faire dans l’éditeur en sélectionnant une pièce et en utilisant le dock Nœud, ou en code en bouclant sur les pièces dans _ready. Je préfère le code quand il y a beaucoup d’instances, car connecter cinquante pièces à la main dans l’éditeur est fastidieux et source d’erreurs.

    Ajouter un ennemi et une fin de partie

    Un ennemi peut être aussi simple qu’une autre Area2D qui va et vient. Quand elle chevauche le joueur, la partie se termine. Voici un patrouilleur minimal :

    extends Area2D
    
    @export var move_speed: float = 90.0
    var _direction: int = 1
    
    func _ready() -> void:
        body_entered.connect(_on_body_entered)
    
    func _physics_process(delta: float) -> void:
        position.x += move_speed * _direction * delta
        # Inverse la direction aux bords de la patrouille
        if position.x > 600 or position.x < 200:
            _direction *= -1
    
    func _on_body_entered(body: Node2D) -> void:
        if body.is_in_group("player"):
            get_tree().change_scene_to_file("res://scenes/game_over.tscn")
    

    L’appel change_scene_to_file démonte la scène actuelle et en charge une nouvelle. Construisez une scène de fin simple avec un Label et un bouton qui réinitialise le score et recharge le niveau. Pensez à appeler GameState.reset sur ce bouton pour que la prochaine partie démarre propre.

    Du polissage qui pèse plus lourd que prévu

    Un jeu qui fonctionne et un jeu qui fait du bien sont deux choses différentes. Les gains les moins chers que je connaisse :

    • Ajoutez un son court au ramassage d’une pièce. Même un simple bip rend la collecte réelle.
    • Donnez à la caméra une petite valeur de lissage pour qu’elle glisse vers le joueur au lieu de claquer dessus.
    • Jouez un rapide tween d’échelle sur la pièce avant qu’elle disparaisse pour qu’elle éclate plutôt qu’elle s’évanouisse.
    • Ajoutez un léger tremblement d’écran quand le joueur meurt. Vingt minutes de travail, énorme qualité perçue.

    Les tweens dans Godot 4 se créent avec create_tween et sont parfaits pour ce genre de jus sans écrire d’animation image par image.

    Jusqu’où ça monte

    Vous avez maintenant la boucle complète : entrée, déplacement, collision, objets à ramasser, état global, un ennemi et des transitions de scène. Presque tout jeu 2D est une version plus élaborée de ces mêmes pièces. Un jeu de plateforme ajoute la gravité et le saut au script de déplacement. Un shooter fait apparaître des scènes de projectiles sur un minuteur. Un jeu de puzzle remplace la physique par une grille. L’architecture que vous venez de construire se transpose directement.

    Terminez ce jeu, puis cassez le exprès. Ajoutez un deuxième type d’ennemi, un meilleur score qui persiste sur le disque, un écran titre. Livrer petit et itérer, voilà comment on apprend réellement le moteur, bien plus que n’importe quel tutoriel y compris celui ci.