npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ffbb-api-client

v0.1.0

Published

TypeScript client for the FFBB (French Basketball Federation) API - Directus REST & Meilisearch

Readme

ffbb-api-client

Client TypeScript pour l'API de la FFBB (Fédération Française de Basketball).

Supporte l'API REST Directus et la recherche full-text Meilisearch. Zéro dépendances runtime (utilise fetch natif, Node 18+).

Installation

npm install ffbb-api-client

Démarrage rapide

import { FFBBClient } from 'ffbb-api-client';

const client = new FFBBClient();
await client.authenticate(); // Récupère les tokens automatiquement

// Récupérer un club avec ses équipes
const club = await client.getOrganisme('11402', {
  fields: ['id', 'nom', 'engagements.id', 'engagements.nom'],
  deep: { engagements: { _limit: 100 } },
});

console.log(club.nom); // "LE CLUB DE BRETTEVILLE-SUR-ODON BASKET"

Authentification

Automatique (recommandé)

const client = new FFBBClient();
await client.authenticate(); // Récupère les tokens depuis /items/configuration

Manuelle

const client = new FFBBClient({
  token: 'votre-token-directus',
  meilisearchToken: 'votre-token-meilisearch',
});

API Directus (REST)

Récupérer un élément

// Par collection et ID
const poule = await client.getPoule('poule-id', {
  fields: ['id', 'nom', 'rencontres.*', 'classements.*'],
  deep: {
    rencontres: {
      _limit: 1000,
      _sort: ['date_rencontre'],
      _filter: { saison: { actif: { _eq: true } } },
    },
  },
});

Méthodes de commodité

const organisme = await client.getOrganisme(id, query?);
const poule = await client.getPoule(id, query?);
const rencontre = await client.getRencontre(id, query?);
const competition = await client.getCompetition(id, query?);
const engagement = await client.getEngagement(id, query?);

Accès générique

import { COLLECTIONS } from 'ffbb-api-client';

// Get
const item = await client.get<MonType>(COLLECTIONS.salles, 'id');

// List
const items = await client.list<MonType>(COLLECTIONS.rencontres, {
  filter: { joue: { _eq: true } },
  sort: ['-date_rencontre'],
  limit: 50,
});

// Auto-pagination
for await (const item of client.listAll<Rencontre>(COLLECTIONS.rencontres)) {
  console.log(item.nomEquipe1, 'vs', item.nomEquipe2);
}

Sélection de champs

Utilisez la notation pointée pour les relations :

const club = await client.getOrganisme('11402', {
  fields: [
    'id',
    'nom',
    'engagements.id',
    'engagements.nom',
    'engagements.idCompetition.nom',
    'engagements.idCompetition.typeCompetition',
    'engagements.idCompetition.saison.id',
    'engagements.idCompetition.categorie.code',
  ],
});

Filtrage

Supporte tous les opérateurs Directus :

// Égalité
{ typeCompetition: { _eq: 'DIV' } }

// Contient
{ nom: { _contains: 'Paris' } }

// Dans une liste
{ id: { _in: ['1', '2', '3'] } }

// Relation imbriquée
{ idCompetition: { saison: { id: { _eq: '1036' } } } }

Relations profondes (deep)

const club = await client.getOrganisme('11402', {
  deep: {
    engagements: {
      _limit: 1000,
      _filter: { idCompetition: { saison: { id: { _eq: '1036' } } } },
      _sort: ['nom'],
    },
  },
});

Recherche Meilisearch

// Recherche simple
const results = await client.searchIndex<Organisme>('ffbbserver_organismes', {
  q: 'Paris',
  limit: 10,
});

for (const hit of results.hits) {
  console.log(hit.nom);
}

// Recherche multi-index
const multi = await client.multiSearch([
  { indexUid: 'ffbbserver_organismes', q: 'Paris', limit: 5 },
  { indexUid: 'ffbbserver_competitions', q: 'U13', limit: 5 },
]);

Recherche géographique

import { buildGeoSort, buildGeoFilter, GeoSortOrder } from 'ffbb-api-client';

const results = await client.searchIndex<Organisme>('ffbbserver_organismes', {
  q: 'basketball',
  filter: buildGeoFilter(48.8566, 2.3522, 50), // 50km autour de Paris
  sort: [buildGeoSort(48.8566, 2.3522, GeoSortOrder.NEAREST_FIRST)],
  limit: 10,
});

Assets (logos, images)

// Construire une URL
const logoUrl = client.assetUrl('uuid', {
  width: 300,
  height: 300,
  fit: 'inside',
  quality: 80,
  format: 'webp',
});

// Télécharger
const buffer = await client.downloadAsset('uuid', { width: 300, format: 'webp' });

Endpoints spéciaux

// Version de l'API
const version = await client.getApiVersion();

// Matchs en direct
const lives = await client.getLives();

// Saisons
const saisons = await client.getSaisons();

Accès bas niveau

Pour des requêtes avancées, accédez directement aux clients sous-jacents :

// Directus REST
const data = await client.directus.request<any>('/custom/endpoint');

// Meilisearch
const results = await client.search.search<any>('ffbbserver_organismes', { q: 'test' });

Utilitaire Query Builder

Le query builder peut être utilisé de manière autonome :

import { buildQueryString } from 'ffbb-api-client';

const qs = buildQueryString({
  fields: ['id', 'nom', 'rencontres.id'],
  deep: { rencontres: { _limit: 1000 } },
  filter: { typeCompetition: { _eq: 'DIV' } },
  sort: ['-date_rencontre'],
  limit: 50,
});
// "fields[]=id&fields[]=nom&fields[]=rencontres.id&deep[rencontres][_limit]=1000&..."

Gestion des erreurs

import { FFBBError, FFBBAuthError, FFBBRequestError, FFBBTimeoutError } from 'ffbb-api-client';

try {
  await client.getPoule('123');
} catch (e) {
  if (e instanceof FFBBAuthError) {
    // Token invalide ou expiré → ré-authentifier
    await client.authenticate();
  } else if (e instanceof FFBBTimeoutError) {
    console.error(`Timeout après ${e.timeoutMs}ms`);
  } else if (e instanceof FFBBRequestError) {
    console.error(`HTTP ${e.status}: ${e.statusText}`);
  } else if (e instanceof FFBBError) {
    console.error('Erreur API:', e.message);
  }
}

Modèle de données : phases et équipes

L'API FFBB gère les compétitions en phases (Phase 1 = saison régulière, Phase 2 = play-offs / accession). Chaque phase est une compétition distincte, ce qui crée des engagements séparés pour la même équipe.

Hiérarchie des entités

Organisme (club)
 └── Engagement (inscription d'une équipe dans une compétition)
      ├── idCompetition (compétition / phase)
      │    ├── id, nom, code
      │    ├── categorie (SE, U13, U18...)
      │    ├── idCompetitionPere → null (Phase 1) ou { id, nom } (Phase 2 → pointe vers Phase 1)
      │    └── typeCompetitionGenerique → logo
      └── idPoule (poule dans laquelle joue l'équipe)
           ├── rencontres[] (matchs)
           └── classements[] (classement de la poule)

Identifier les phases

Le champ idCompetitionPere sur une compétition permet de lier Phase 2 → Phase 1 :

  • Phase 1 : idCompetitionPere est null
  • Phase 2 : idCompetitionPere.id pointe vers l'ID de la compétition Phase 1
// Récupérer les engagements avec le lien entre phases
const club = await client.getOrganisme('11402', {
  fields: [
    'id', 'nom',
    'engagements.id', 'engagements.nom', 'engagements.numeroEquipe',
    'engagements.idPoule.id', 'engagements.idPoule.nom',
    'engagements.idCompetition.id', 'engagements.idCompetition.nom',
    'engagements.idCompetition.code', 'engagements.idCompetition.typeCompetition',
    'engagements.idCompetition.saison.id',
    'engagements.idCompetition.categorie.code',
    'engagements.idCompetition.categorie.ordre',
    // Lien Phase 2 → Phase 1
    'engagements.idCompetition.idCompetitionPere.id',
    'engagements.idCompetition.idCompetitionPere.nom',
  ],
  deep: {
    engagements: {
      _limit: 1000,
      _filter: { idCompetition: { saison: { id: { _eq: '1036' } } } },
    },
  },
});

Grouper les phases d'une même équipe

const engagements = club.engagements;

// Séparer Phase 1 et Phase 2
const phase1 = engagements.filter(e => !e.idCompetition.idCompetitionPere);
const phase2 = engagements.filter(e => !!e.idCompetition.idCompetitionPere);

// Construire la map Phase 1 : competitionId → engagement
const phase1Map = new Map(phase1.map(e => [e.idCompetition.id, e]));

// Associer chaque Phase 2 à sa Phase 1
for (const p2 of phase2) {
  const parentCompetitionId = p2.idCompetition.idCompetitionPere.id;
  const p1 = phase1Map.get(parentCompetitionId);
  // p1 peut être undefined si le club n'avait pas d'engagement
  // dans cette compétition en Phase 1 (changement de division)
}

Récupérer matchs et classements d'une poule

Chaque engagement est lié à une poule (idPoule). La poule contient les rencontres et classements :

const poule = await client.getPoule(engagement.idPoule.id, {
  fields: [
    'id', 'nom',
    'rencontres.id', 'rencontres.numero', 'rencontres.numeroJournee',
    'rencontres.resultatEquipe1', 'rencontres.resultatEquipe2',
    'rencontres.joue', 'rencontres.nomEquipe1', 'rencontres.nomEquipe2',
    'rencontres.date_rencontre',
    'rencontres.idEngagementEquipe1.id', 'rencontres.idEngagementEquipe1.nom',
    'rencontres.idEngagementEquipe1.idOrganisme.code',
    'rencontres.idOrganismeEquipe1.id', 'rencontres.idOrganismeEquipe1.logo.id',
    'rencontres.idEngagementEquipe2.id', 'rencontres.idEngagementEquipe2.nom',
    'rencontres.idEngagementEquipe2.idOrganisme.code',
    'rencontres.idOrganismeEquipe2.id', 'rencontres.idOrganismeEquipe2.logo.id',
    'rencontres.salle.libelle', 'rencontres.salle.commune.libelle',
    'classements.id', 'classements.idEngagement.id', 'classements.idEngagement.nom',
    'classements.matchJoues', 'classements.points', 'classements.position',
    'classements.gagnes', 'classements.perdus',
  ],
  deep: {
    rencontres: {
      _limit: 1000,
      _filter: { saison: { actif: { _eq: true } } },
      _sort: ['date_rencontre'],
    },
  },
});

Filtrer les matchs d'un club

Les matchs (rencontres) d'une poule concernent toutes les équipes de la poule. Pour isoler ceux d'un club :

const CLUB_CODE = 'NOR0014042';

const clubMatches = poule.rencontres.filter(match =>
  match.idEngagementEquipe1?.idOrganisme?.code === CLUB_CODE ||
  match.idEngagementEquipe2?.idOrganisme?.code === CLUB_CODE
);

Conventions de nommage des codes

| Pattern | Exemple | Signification | |--------------|-----------------------------------------------------|-----------------------------------| | PNM | Pré nationale masculine | Code Phase 1 | | PNM-P2 | Pré nationale masculine - Phase 2 | Code Phase 2 (suffixe -P2) | | DMU13-2 | Départementale masculine U13 - Division 2 | Suffixe -N = numéro de division | | DMU18-2-P2 | Départementale masculine U18 - Division 2 - Phase 2 | Division + Phase |

Le code seul n'est pas fiable pour lier les phases (la division peut changer entre Phase 1 et Phase 2). Utiliser idCompetitionPere à la place.

Collections disponibles

import { COLLECTIONS } from 'ffbb-api-client';

COLLECTIONS.organismes    // 'ffbbserver_organismes'
COLLECTIONS.poules        // 'ffbbserver_poules'
COLLECTIONS.rencontres    // 'ffbbserver_rencontres'
COLLECTIONS.competitions  // 'ffbbserver_competitions'
COLLECTIONS.engagements   // 'ffbbserver_engagements'
COLLECTIONS.salles        // 'ffbbserver_salles'
COLLECTIONS.terrains      // 'ffbbserver_terrains'
COLLECTIONS.tournois      // 'ffbbserver_tournois'
COLLECTIONS.entraineurs   // 'ffbbserver_entraineeurs'
COLLECTIONS.formations    // 'ffbbserver_formations'
COLLECTIONS.pratiques     // 'ffbbnational_pratiques'
COLLECTIONS.communes      // 'ffbbserver_communes'
COLLECTIONS.officiels     // 'ffbbserver_officiels'
COLLECTIONS.saisons       // 'ffbbserver_saisons'

Types exportés

Tous les types des collections sont exportés :

import type {
  Organisme, Engagement, Competition, Poule, Rencontre,
  Classement, Salle, Saison, Terrain, Tournoi, Entraineur,
  Formation, Commune, Officiel, Pratique, ApiVersion, LiveMatch,
  Logo, Configuration,
  // Query types
  DirectusQuery, DirectusFilter, DeepConfig, DirectusResponse,
  // Meilisearch types
  MeilisearchQuery, MeilisearchResponse, MeilisearchIndex,
  MultiSearchQuery, MultiSearchResponse,
} from 'ffbb-api-client';

Configuration

| Option | Défaut | Description | |--------------------|-------------------------------------|---------------------------------------------------| | baseUrl | https://api.ffbb.com | URL de base de l'API Directus | | token | — | Token Directus (sinon, utiliser authenticate()) | | meilisearchUrl | https://meilisearch-prod.ffbb.app | URL Meilisearch | | meilisearchToken | — | Token Meilisearch | | fetch | globalThis.fetch | Implémentation fetch personnalisée | | timeout | 30000 | Timeout des requêtes en ms |

Licence

MIT