Les systèmes distribués sont complexes. Les processus plantent, les réseaux tombent, et les services s'arrêtent au pire moment possible. Les files de tâches traditionnelles gèrent des opérations simples, mais que se passe-t-il quand vous avez besoin d'un workflow qui s'étend sur plusieurs heures, implique plusieurs services, et doit survivre aux redémarrages de serveur ?
Temporal.io résout ce problème en fournissant une plateforme d'exécution durable où votre code s'exécute comme si les pannes n'existaient tout simplement pas. Vos workflows persistent malgré les crashes, les tentatives se font automatiquement, et vous bénéficiez d'une visibilité complète sur chaque étape d'exécution.
Dans ce tutoriel, vous allez construire un système de traitement de commandes prêt pour la production avec Temporal.io et TypeScript. À la fin, vous saurez modéliser des processus métier complexes sous forme de workflows fiables et observables.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou version ultérieure
- Une expérience intermédiaire en TypeScript
- Une bonne compréhension de async/await et des Promises
- Docker installé (optionnel — le CLI Temporal gère le développement local)
- Une familiarité avec les API REST
Ce que vous allez construire
Vous allez créer un système de traitement de commandes avec ces étapes :
- Valider la commande et vérifier l'inventaire
- Débiter le moyen de paiement du client
- Envoyer un e-mail de confirmation
- Planifier la préparation et l'expédition
- Gérer les erreurs à chaque étape avec des tentatives automatiques
Ce workflow sera entièrement fault-tolerant — si votre serveur plante en cours de traitement, Temporal reprend exactement là où il s'est arrêté au redémarrage.
Étape 1 : Démarrer le serveur Temporal en développement
La manière la plus simple de faire tourner Temporal en local est le CLI Temporal. Installez-le via Homebrew sur macOS :
brew install temporalOu téléchargez-le depuis la page des releases officielles pour Linux et Windows.
Démarrez le serveur de développement :
temporal server start-devCela lance :
- Temporal Server sur le port 7233
- Interface Web sur
http://localhost:8233
Ouvrez l'interface web dans votre navigateur pour surveiller les exécutions de workflows en temps réel. Gardez ce terminal ouvert tout au long du tutoriel.
Étape 2 : Initialiser le projet TypeScript
Créez un nouveau projet Node.js :
mkdir temporal-order-processing
cd temporal-order-processing
npm init -yInstallez le SDK Temporal et les dépendances TypeScript :
npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity
npm install -D typescript ts-node @types/nodeInitialisez TypeScript :
npx tsc --initMettez à jour tsconfig.json :
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Créez la structure du projet :
mkdir -p src/{workflows,activities,workers,client,shared}Votre arborescence finale ressemblera à :
temporal-order-processing/
├── src/
│ ├── shared/
│ │ └── types.ts
│ ├── workflows/
│ │ └── order-workflow.ts
│ ├── activities/
│ │ └── order-activities.ts
│ ├── workers/
│ │ └── worker.ts
│ └── client/
│ └── start-workflow.ts
├── tsconfig.json
└── package.json
Étape 3 : Comprendre les concepts fondamentaux de Temporal
Avant d'écrire du code, comprenons les quatre composants clés :
Workflows
Un Workflow est une fonction durable qui orchestre votre logique métier. Les workflows sont déterministes — avec les mêmes entrées, ils produisent toujours la même séquence d'opérations. Ce déterminisme permet à Temporal de rejouer et de récupérer les workflows après une panne.
Contraintes fondamentales pour les workflows :
- Pas d'I/O direct (pas d'appels base de données, pas de requêtes HTTP, pas d'accès au système de fichiers)
- Pas d'utilisation directe de
Date.now()ouMath.random() - Tous les effets de bord passent par des Activities
Activities
Une Activity est l'endroit où réside votre logique métier réelle — requêtes base de données, appels API, envoi d'e-mails. Les activités peuvent échouer et seront relancées selon votre politique de retry configurée.
Workers
Un Worker est un processus qui héberge vos workflows et activités. Il interroge le serveur Temporal pour des tâches et les exécute. Vous pouvez lancer plusieurs workers pour la mise à l'échelle horizontale.
Le client Temporal
Le Client est utilisé pour démarrer des workflows et envoyer des signaux aux workflows en cours d'exécution.
Étape 4 : Définir les types partagés
// src/shared/types.ts
export interface OrderItem {
productId: string;
quantity: number;
price: number;
}
export interface OrderInput {
orderId: string;
customerId: string;
items: OrderItem[];
totalAmount: number;
paymentMethodId: string;
}
export interface OrderResult {
orderId: string;
status: "completed" | "failed" | "cancelled";
chargeId?: string;
trackingNumber?: string;
message: string;
}Étape 5 : Écrire les activities
Les activities contiennent la logique métier réelle :
// src/activities/order-activities.ts
import { OrderInput } from "../shared/types";
export async function validateOrder(input: OrderInput): Promise<boolean> {
console.log(`Validation de la commande ${input.orderId}`);
await new Promise((resolve) => setTimeout(resolve, 100));
if (input.items.length === 0) {
throw new Error("La commande doit contenir au moins un article");
}
if (input.totalAmount <= 0) {
throw new Error("Le montant total doit être supérieur à zéro");
}
return true;
}
export async function chargePayment(
orderId: string,
amount: number,
paymentMethodId: string
): Promise<string> {
console.log(`Débit de ${amount} pour la commande ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 500));
const chargeId = `ch_${Date.now()}_${orderId}`;
return chargeId;
}
export async function sendConfirmationEmail(
orderId: string,
customerId: string,
chargeId: string
): Promise<void> {
console.log(`Envoi du mail de confirmation pour la commande ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 200));
}
export async function scheduleShipping(
orderId: string,
itemCount: number
): Promise<string> {
console.log(`Planification de l'expédition pour la commande ${orderId}`);
await new Promise((resolve) => setTimeout(resolve, 300));
return `TRK${Date.now()}`;
}Les activities peuvent effectuer tout appel I/O — requêtes base de données, requêtes HTTP, accès au système de fichiers. Ce sont les seuls endroits où des effets de bord doivent se produire dans un workflow Temporal.
Étape 6 : Écrire le workflow de commande
Créez le workflow qui orchestre ces activities en séquence :
// src/workflows/order-workflow.ts
import {
proxyActivities,
defineSignal,
defineQuery,
setHandler,
} from "@temporalio/workflow";
import type * as activities from "../activities/order-activities";
import type { OrderInput, OrderResult } from "../shared/types";
const {
validateOrder,
chargePayment,
sendConfirmationEmail,
scheduleShipping,
} = proxyActivities<typeof activities>({
startToCloseTimeout: "30 seconds",
retry: {
maximumAttempts: 3,
initialInterval: "1 second",
maximumInterval: "10 seconds",
backoffCoefficient: 2,
},
});
export const cancelOrderSignal = defineSignal<[string]>("cancelOrder");
export const getOrderStatusQuery = defineQuery<string>("getOrderStatus");
export async function orderWorkflow(input: OrderInput): Promise<OrderResult> {
let cancelled = false;
let cancellationReason = "";
let currentStatus = "pending";
setHandler(cancelOrderSignal, (reason: string) => {
cancelled = true;
cancellationReason = reason;
});
setHandler(getOrderStatusQuery, () => currentStatus);
try {
currentStatus = "validating";
await validateOrder(input);
if (cancelled) {
return {
orderId: input.orderId,
status: "cancelled",
message: `Commande annulée avant paiement : ${cancellationReason}`,
};
}
currentStatus = "charging";
const chargeId = await chargePayment(
input.orderId,
input.totalAmount,
input.paymentMethodId
);
if (cancelled) {
return {
orderId: input.orderId,
status: "cancelled",
chargeId,
message: "Commande annulée après paiement. Remboursement initié.",
};
}
currentStatus = "confirming";
await sendConfirmationEmail(input.orderId, input.customerId, chargeId);
currentStatus = "shipping";
const trackingNumber = await scheduleShipping(
input.orderId,
input.items.length
);
currentStatus = "completed";
return {
orderId: input.orderId,
status: "completed",
chargeId,
trackingNumber,
message: "Commande traitée avec succès",
};
} catch (error) {
currentStatus = "failed";
return {
orderId: input.orderId,
status: "failed",
message: error instanceof Error ? error.message : "Erreur inconnue",
};
}
}N'importez jamais les modules Node.js natifs comme fs, http ou crypto directement dans les fichiers de workflow. Le sandbox Temporal bloque ces imports. Utilisez import type pour les imports depuis les fichiers d'activities.
Étape 7 : Configurer le Worker
Le processus worker fait le lien entre le serveur Temporal et votre code applicatif :
// src/workers/worker.ts
import { Worker, NativeConnection } from "@temporalio/worker";
import * as activities from "../activities/order-activities";
import path from "path";
async function run() {
const connection = await NativeConnection.connect({
address: "localhost:7233",
});
const worker = await Worker.create({
connection,
namespace: "default",
taskQueue: "order-processing",
workflowsPath: path.join(__dirname, "../workflows"),
activities,
});
console.log("Worker démarré — en attente de tâches sur : order-processing");
await worker.run();
}
run().catch((err) => {
console.error("Échec du démarrage du worker :", err);
process.exit(1);
});Étape 8 : Créer le client de workflow
Le client déclenche les exécutions de workflow et peut les interroger :
// src/client/start-workflow.ts
import { Client, Connection } from "@temporalio/client";
import {
orderWorkflow,
getOrderStatusQuery,
} from "../workflows/order-workflow";
import { OrderInput } from "../shared/types";
async function main() {
const connection = await Connection.connect({ address: "localhost:7233" });
const client = new Client({ connection, namespace: "default" });
const orderInput: OrderInput = {
orderId: "ORDER-001",
customerId: "CUST-123",
items: [
{ productId: "PROD-A", quantity: 2, price: 29.99 },
{ productId: "PROD-B", quantity: 1, price: 49.99 },
],
totalAmount: 109.97,
paymentMethodId: "pm_card_visa",
};
const handle = await client.workflow.start(orderWorkflow, {
taskQueue: "order-processing",
workflowId: `order-${orderInput.orderId}`,
args: [orderInput],
});
console.log(`Workflow démarré : ${handle.workflowId}`);
// Interroger le statut jusqu'à la fin
let done = false;
while (!done) {
const status = await handle.query(getOrderStatusQuery);
console.log(`Statut actuel : ${status}`);
if (status === "completed" || status === "failed") done = true;
await new Promise((r) => setTimeout(r, 1000));
}
const result = await handle.result();
console.log("Résultat final :", result);
await connection.close();
}
main().catch(console.error);Étape 9 : Scripts npm et lancement du système
Mettez à jour package.json :
{
"scripts": {
"worker": "ts-node src/workers/worker.ts",
"start": "ts-node src/client/start-workflow.ts"
}
}Ouvrez trois fenêtres de terminal :
Terminal 1 — Serveur Temporal :
temporal server start-devTerminal 2 — Processus Worker :
npm run workerTerminal 3 — Déclenchement du Workflow :
npm run startVous verrez les logs de chaque activity dans le terminal du worker et les mises à jour de statut dans le terminal client. Ouvrez http://localhost:8233 pour inspecter l'historique complet des événements en temps réel.
Étape 10 : Les politiques de retry expliquées
La configuration des retries est critique pour la production :
proxyActivities({
startToCloseTimeout: "30 seconds", // Durée max par tentative
scheduleToCloseTimeout: "5 minutes", // Durée max totale, retries inclus
retry: {
maximumAttempts: 3,
initialInterval: "1 second", // Attendre 1s avant le premier retry
maximumInterval: "30 seconds", // Ne jamais attendre plus de 30s
backoffCoefficient: 2, // Exponentiel : 1s, 2s, 4s...
nonRetryableErrorTypes: ["PaymentDeclinedError"],
},
})Pour les activités de paiement, utilisez maximumAttempts: 1 pour éviter un double débit. Pour l'envoi d'e-mails, autorisez jusqu'à 5 tentatives.
Étape 11 : Workflows longue durée avec sleep
La fonction sleep de Temporal met en pause un workflow pour n'importe quelle durée — des secondes aux mois — sans consommer de ressources :
import { sleep } from "@temporalio/workflow";
export async function subscriptionRenewalWorkflow(userId: string) {
// Attendre 30 jours avant le renouvellement
await sleep("30 days");
await chargeRenewalFee(userId);
await sendRenewalConfirmation(userId);
// Planifier le prochain cycle
await sleep("30 days");
}C'est l'une des fonctionnalités les plus puissantes de Temporal — remplacez vos cron jobs par du code lisible qui gère automatiquement les retries et l'état.
Étape 12 : Tests avec TestWorkflowEnvironment
Temporal fournit un environnement de test pour exécuter des workflows sans serveur réel :
// src/__tests__/order-workflow.test.ts
import { TestWorkflowEnvironment } from "@temporalio/testing";
import { Worker } from "@temporalio/worker";
import { orderWorkflow } from "../workflows/order-workflow";
import * as activities from "../activities/order-activities";
describe("Workflow de commande", () => {
let testEnv: TestWorkflowEnvironment;
beforeAll(async () => {
testEnv = await TestWorkflowEnvironment.createLocal();
});
afterAll(async () => {
await testEnv.teardown();
});
it("se termine avec succès pour une commande valide", async () => {
const { client, nativeConnection } = testEnv;
const worker = await Worker.create({
connection: nativeConnection,
namespace: "default",
taskQueue: "test-queue",
workflowsPath: require.resolve("../workflows/order-workflow"),
activities,
});
const result = await worker.runUntil(
client.workflow.execute(orderWorkflow, {
taskQueue: "test-queue",
workflowId: "test-001",
args: [{
orderId: "TEST-001",
customerId: "CUST-001",
items: [{ productId: "P1", quantity: 1, price: 10 }],
totalAmount: 10,
paymentMethodId: "pm_test",
}],
})
);
expect(result.status).toBe("completed");
expect(result.chargeId).toBeDefined();
expect(result.trackingNumber).toBeDefined();
});
});Installez le package de test :
npm install -D @temporalio/testingÉtape 13 : Intégration avec une API Express
Connectez Temporal à votre couche HTTP :
// src/api/server.ts
import express from "express";
import { Client, Connection } from "@temporalio/client";
import { orderWorkflow, getOrderStatusQuery } from "../workflows/order-workflow";
import type { OrderInput } from "../shared/types";
const app = express();
app.use(express.json());
let temporalClient: Client;
async function initClient() {
const connection = await Connection.connect({ address: "localhost:7233" });
temporalClient = new Client({ connection, namespace: "default" });
}
// POST /orders — démarre le workflow
app.post("/orders", async (req, res) => {
try {
const input: OrderInput = req.body;
const handle = await temporalClient.workflow.start(orderWorkflow, {
taskQueue: "order-processing",
workflowId: `order-${input.orderId}`,
args: [input],
});
res.json({ workflowId: handle.workflowId });
} catch {
res.status(500).json({ error: "Impossible de démarrer le workflow" });
}
});
// GET /orders/:id/status — interroge le workflow en cours
app.get("/orders/:id/status", async (req, res) => {
try {
const handle = temporalClient.workflow.getHandle(`order-${req.params.id}`);
const status = await handle.query(getOrderStatusQuery);
res.json({ orderId: req.params.id, status });
} catch {
res.status(404).json({ error: "Workflow de commande introuvable" });
}
});
initClient().then(() => {
app.listen(3001, () => console.log("API démarrée sur le port 3001"));
});Résolution des problèmes courants
"Connection refused" au démarrage du worker :
Vérifiez que temporal server start-dev tourne dans un terminal séparé et que le port 7233 est disponible.
Workflow bloqué à l'état "Running" indéfiniment : Vérifiez que le processus worker tourne et qu'il est connecté au même nom de task queue que celui utilisé dans le client. Inspectez l'historique des événements dans l'interface Web pour les détails d'erreur.
Les activities ne font pas de retry après un échec :
Confirmez que la politique de retry est configurée sur proxyActivities et non à l'intérieur du corps de la fonction workflow.
Erreurs TypeScript dans les fichiers de workflow :
Les workflows s'exécutent dans un environnement sandbox. Évitez les modules Node.js natifs et utilisez import type pour les imports depuis les fichiers d'activities.
Prochaines étapes
Avec ce setup fonctionnel, explorez ces patterns avancés :
- Child Workflows : Décomposer les workflows complexes en sous-workflows réutilisables
- Schedules : Remplacer les cron jobs par l'API de planification intégrée de Temporal
- Pattern Saga : Implémenter des transactions distribuées avec des activities compensatoires
- API de versioning : Mettre à jour les workflows en cours d'exécution de façon sécurisée avec
patched() - Temporal Cloud : Temporal entièrement géré pour la production
Conclusion
Vous avez construit un système de traitement de commandes prêt pour la production avec Temporal.io et TypeScript. Votre workflow survit désormais aux pannes de serveur, relance automatiquement les activities échouées, et expose le statut en temps réel via les queries.
La vraie puissance de Temporal est qu'il élimine toute une classe de problèmes liés aux systèmes distribués : vous n'avez plus besoin de construire manuellement la logique de retry, gérer l'état des files de tâches, ou implémenter des gestionnaires de transactions compensatoires. Que vous construisiez des flux de paiement, des pipelines d'onboarding, des jobs ETL, ou des processus d'approbation multi-étapes, Temporal donne à votre logique métier la durabilité qu'exige la production.