Appeler l'API depuis un script

Ce guide appelle l'API de plateforme tRPC depuis un script Node ou Bun autonome. Il couvre l'obtention d'un token, une version en fetch brut (pour voir le format de transport superjson), la version typée @trpc/client, la pagination d'une liste et la gestion de l'enveloppe d'erreur.

Obtenir un token

Les appelants programmatiques s'authentifient avec un JWT de session dans l'en-tête Authorization :

Authorization: Bearer <session JWT>

Le token est votre JWT de session, la même valeur que transporte votre cookie de session. Le plus simple pour l'obtenir est Se connecter avec Inklura dans l'Explorateur d'API (voir Authentification). Gardez-le hors du code source ; lisez-le depuis l'environnement :

export INKLURA_TOKEN="<session JWT>"

Le script agit en tant qu'utilisateur du token, avec ses permissions. Le token indique le locataire et le site d'origine de ce compte ; la portée de chaque appel est résolue à la requête et vérifiée au regard de ses accès (voir Sessions et tokens).

Vérifiez que le token est accepté avant tout travail réel en appelant auth.getSession (montré ci-dessous). Un token invalide ou expiré n'est pas signalé comme tel : la requête est traitée comme anonyme et l'appel échoue avec UNAUTHORIZED (401). auth.getSession demande la permission auth:read : un compte qui ne l'a pas reçoit FORBIDDEN (403), même avec un token valide.

fetch brut

Utile pour voir le format de transport. Les requêtes sont en GET avec l'entrée URL-encodée dans ?input={"json":<value>} ; les mutations sont en POST avec le corps {"json":<value>}. Le résultat est imbriqué à result.data.json, et une erreur à error.json.

const BASE = "https://manage.inklura.fr/api/trpc";
const auth = { Authorization: `Bearer ${process.env.INKLURA_TOKEN}` };

// Lit la réponse : renvoie la valeur, ou lève l'erreur tRPC.
async function unwrap(res: Response) {
  const body = await res.json();
  if (body.error) {
    const { message, data } = body.error.json;
    throw new Error(`${data?.code} (${data?.httpStatus}) : ${message}`);
  }
  return body.result.data.json;
}

// Query (GET) sans entrée : qui suis-je ?
const me = await unwrap(await fetch(`${BASE}/auth.getSession`, { headers: auth }));
console.log("session :", me.user.email);

// Query (GET) avec entrée : première page de produits
const input = encodeURIComponent(JSON.stringify({ json: { page: 0, limit: 10 } }));
const firstPage = await unwrap(
  await fetch(`${BASE}/products.list?input=${input}`, { headers: auth }),
);
console.log("produits :", firstPage.total);

// Mutation (POST) : créer un client
const created = await unwrap(
  await fetch(`${BASE}/clients.create`, {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({ json: { name: "Acme SARL", email: "contact@acme.example" } }),
  }),
);
console.log("client créé :", created.id);

Une procédure qui ne prend aucune entrée, comme auth.getSession, s'appelle sans ?input= : lui envoyer une entrée échoue à la validation.

En fetch brut, les valeurs que JSON ne sait pas représenter (dates notamment) arrivent sous forme de chaînes dans json, leur type d'origine étant décrit à côté, dans meta. Le client typé ci-dessous les reconvertit pour vous.

@trpc/client typé

Pour un appelant TypeScript, @trpc/client (v11) gère l'emballage superjson et le batching pour vous. Configurez le transformateur superjson sur un httpBatchLink et définissez-y l'en-tête Authorization :

import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";

// AppRouter : le type du router de l'API si vous l'avez ; `any` fonctionne aussi, sans vérification de types.
const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "https://manage.inklura.fr/api/trpc",
      transformer: superjson,
      headers() {
        return { Authorization: `Bearer ${process.env.INKLURA_TOKEN}` };
      },
    }),
  ],
});

const session = await client.auth.getSession.query();
const created = await client.clients.create.mutate({
  name: "Acme SARL",
  email: "contact@acme.example",
});

Avec le transformateur configuré, vous passez et recevez des valeurs simples, sans emballage manuel {"json":…}. Voir Procédures tRPC pour les conventions d'appel et les détails du batching.

clients.create exige name (non vide) ; email est facultatif mais doit être une adresse valide s'il est fourni. Il faut aussi que votre compte ait le droit de modifier les clients : sinon l'appel échoue avec FORBIDDEN.

Paginer une liste

products.list prend { page, limit }, avec page qui commence à 0 (limit vaut 10 par défaut), et renvoie { items, total, page, limit, pages }. Elle accepte aussi des filtres facultatifs : search (nom, description, référence ou SKU), active, categoryId, et le tri sortBy / sortOrder (par défaut, les plus récents d'abord). Parcourez chaque page :

async function listAllProducts() {
  const all = [];
  const limit = 50;
  for (let page = 0; ; page += 1) {
    const res = await client.products.list.query({ page, limit });
    all.push(...res.items);
    if (page + 1 >= res.pages || res.items.length === 0) break;
  }
  return all;
}

La pagination est une convention plutôt qu'une garantie du framework : les noms de paramètres et le numéro de la première page varient d'une procédure à l'autre, confirmez donc la forme pour la procédure que vous appelez. Voir Pagination et filtrage.

Gérer les erreurs

Les échecs reviennent sous forme d'enveloppe d'erreur tRPC standard ; les champs utiles sont dans data (code, httpStatus, path), et les échecs de validation de l'entrée ajoutent data.zodError. Le client typé lève une TRPCClientError dont le data les transporte :

import { TRPCClientError } from "@trpc/client";

try {
  await client.clients.create.mutate({ name: "", email: "not-an-email" });
} catch (err) {
  if (err instanceof TRPCClientError) {
    const data = err.data; // { code, httpStatus, path, zodError }
    console.error(data?.code, data?.httpStatus); // BAD_REQUEST 400
    if (data?.zodError) {
      console.error("erreurs par champ :", data.zodError.fieldErrors);
    }
  } else {
    throw err;
  }
}

Avec fetch brut, lisez les mêmes champs dans error.json.data du corps parsé (comme le fait unwrap plus haut). Voir Erreurs et limites de débit pour le mappage code-vers-statut et la forme de zodError.

Connexe