Web Scraping avec Crawlee et TypeScript : guide complet du zéro au déploiement en production

Le web scraping bien fait. Crawlee est le framework TypeScript open-source par Apify qui gère les parties difficiles — files de requêtes, nouvelles tentatives, rotation de proxies et anti-blocage — pour que vous puissiez vous concentrer sur l'extraction de données.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer un projet Crawlee avec TypeScript depuis zéro
- Construire des scrapers avec PlaywrightCrawler pour les sites riches en JavaScript
- Utiliser CheerioCrawler pour le scraping HTML léger et rapide
- Gérer les files de requêtes pour crawler des milliers de pages
- Stocker les données extraites avec le système Dataset intégré de Crawlee
- Implémenter la rotation de proxies et les stratégies anti-blocage
- Gérer la pagination, le défilement infini et le contenu dynamique
- Déployer votre scraper en production avec Docker
Prérequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20+ installé (
node --version) - Expérience en TypeScript (types, async/await, generics)
- Connaissances de base en HTML/CSS (sélecteurs, structure DOM)
- Un éditeur de code — VS Code ou Cursor recommandé
- Docker installé (optionnel, pour le déploiement)
Pourquoi Crawlee ?
Le web scraping en Node.js signifie souvent assembler Puppeteer, Cheerio, des bibliothèques de requêtes, une logique de nouvelles tentatives et la gestion de files soi-même. Crawlee fournit tout cela dans un framework unique et cohérent :
| Fonctionnalité | Crawlee | Manuel (Puppeteer + Cheerio) | Scrapy (Python) |
|---|---|---|---|
| Langage | TypeScript/JavaScript | JavaScript | Python |
| Support navigateur | Playwright, Puppeteer | Configuration manuelle | Splash/Selenium |
| File de requêtes | Intégrée avec persistance | Implémentation manuelle | Intégrée |
| Nouvelles tentatives | Configurable par requête | Manuel | Intégrées |
| Rotation de proxies | Intégrée avec gestion de sessions | Manuel | Middleware |
| Anti-blocage | Empreintes, headers | Manuel | Middleware |
| Stockage de données | Dataset + Key-Value Store | Manuel (JSON/DB) | Item pipelines |
| Sécurité de types | TypeScript complet | Optionnel | Non |
Crawlee vous donne la puissance de Scrapy avec la sécurité de TypeScript et une expérience développeur moderne.
Étape 1 : Configuration du projet
Commencez par créer un nouveau projet Crawlee. Le CLI génère tout ce dont vous avez besoin :
npx crawlee create my-scraper --template playwright-ts
cd my-scraperCela génère la structure de projet suivante :
my-scraper/
├── src/
│ ├── main.ts # Point d'entrée
│ ├── routes.ts # Gestionnaires de routes
│ └── types.ts # Types personnalisés
├── storage/ # Créé automatiquement pour les données et files
├── package.json
├── tsconfig.json
└── Dockerfile # Configuration Docker prête pour la production
Installez les dépendances :
npm installCrawlee installe trois packages principaux :
crawlee— Le cœur du framework avec les crawlers, files et stockageplaywright— Automatisation de navigateur pour les pages rendues en JavaScript@crawlee/playwright— Intégration Playwright pour Crawlee
Étape 2 : Comprendre l'architecture de Crawlee
Avant d'écrire du code, comprenons le fonctionnement de Crawlee :
┌─────────────────────────────────────────────┐
│ Crawlee │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ File de │───▶│ Crawler │───▶│ Dataset │ │
│ │ requêtes │ │ (Router) │ │ (Sortie)│ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ │ Gestion │ │
│ │ │ Proxies │ │
│ │ └─────────┘ │
│ ▼ │
│ Nouvelles tentatives automatiques │
│ Contrôle de concurrence │
│ Limitation de débit │
└─────────────────────────────────────────────┘
Les composants clés :
- File de requêtes — Gère les URLs à scraper, gère la déduplication et persiste l'état entre les redémarrages
- Crawler — Traite chaque requête avec votre fonction de gestionnaire (Cheerio pour HTML, Playwright pour les pages rendues en JS)
- Router — Route les différents patterns d'URL vers différentes fonctions de traitement
- Dataset — Stocke les données extraites en lignes JSON, exportables en CSV, JSON ou tout format
- Gestionnaire de proxies — Effectue la rotation des proxies et gère les sessions pour éviter les blocages
Étape 3 : Construire un CheerioCrawler pour les pages statiques
Commençons avec le type de crawler le plus rapide — CheerioCrawler. Il télécharge le HTML brut et le parse avec Cheerio (API similaire à jQuery), sans lancer de navigateur. Parfait pour les sites qui ne nécessitent pas de rendu JavaScript.
Remplacez le contenu de src/main.ts :
import { CheerioCrawler, Dataset, log } from 'crawlee';
// Configurer les logs
log.setLevel(log.LEVELS.INFO);
// Créer le crawler
const crawler = new CheerioCrawler({
// Nombre maximum de requêtes simultanées
maxConcurrency: 10,
// Nombre maximum de requêtes par minute (limitation de débit)
maxRequestsPerMinute: 60,
// Réessayer les requêtes échouées jusqu'à 3 fois
maxRequestRetries: 3,
// Gestionnaire pour chaque page
async requestHandler({ request, $, enqueueLinks, pushData }) {
const url = request.url;
log.info(`Scraping : ${url}`);
// Extraire les données de la page avec des sélecteurs CSS
const title = $('h1').first().text().trim();
const description = $('meta[name="description"]').attr('content') || '';
const links = $('a[href]')
.map((_, el) => $(el).attr('href'))
.get()
.filter((href) => href.startsWith('http'));
// Pousser les données extraites dans le dataset
await pushData({
url,
title,
description,
linksFound: links.length,
scrapedAt: new Date().toISOString(),
});
// Suivre les liens de la page (crawling en largeur)
await enqueueLinks({
globs: ['https://example.com/**'],
label: 'DETAIL',
});
},
// Appelé quand une requête échoue après toutes les tentatives
async failedRequestHandler({ request }) {
log.error(`Échec : ${request.url} — ${request.errorMessages.join(', ')}`);
},
});
// Démarrer le crawler avec des URLs de départ
await crawler.run([
'https://example.com',
]);
// Exporter les résultats
const dataset = await Dataset.open();
await dataset.exportToJSON('results');
log.info('Scraping terminé ! Résultats sauvegardés dans storage/datasets/default/');Exécutez-le :
npx tsx src/main.tsVos données extraites sont sauvegardées dans storage/datasets/default/ sous forme de fichiers JSON individuels.
Étape 4 : Construire un PlaywrightCrawler pour les sites dynamiques
De nombreux sites modernes rendent leur contenu avec JavaScript. Pour ceux-ci, vous avez besoin de PlaywrightCrawler, qui lance un vrai navigateur :
import { PlaywrightCrawler, Dataset, log } from 'crawlee';
const crawler = new PlaywrightCrawler({
// Utiliser Chromium sans interface graphique
launchContext: {
launchOptions: {
headless: true,
},
},
// Les pages de navigateur sont coûteuses — limiter la concurrence
maxConcurrency: 5,
// Timeout par page (30 secondes)
requestHandlerTimeoutSecs: 30,
async requestHandler({ request, page, enqueueLinks, pushData }) {
const url = request.url;
log.info(`Scraping (navigateur) : ${url}`);
// Attendre que le contenu principal soit rendu
await page.waitForSelector('.product-card', { timeout: 10000 });
// Extraire les données des produits
const products = await page.$$eval('.product-card', (cards) =>
cards.map((card) => ({
name: card.querySelector('.product-name')?.textContent?.trim() || '',
price: card.querySelector('.product-price')?.textContent?.trim() || '',
rating: card.querySelector('.product-rating')?.textContent?.trim() || '',
image: card.querySelector('img')?.getAttribute('src') || '',
}))
);
// Pousser chaque produit dans le dataset
for (const product of products) {
await pushData({
...product,
sourceUrl: url,
scrapedAt: new Date().toISOString(),
});
}
// Suivre les liens de pagination
await enqueueLinks({
selector: 'a.pagination-next',
label: 'LISTING',
});
},
});
await crawler.run([
'https://example-shop.com/products?page=1',
]);Quand utiliser quel crawler
| Scénario | Crawler | Raison |
|---|---|---|
| Sites HTML statiques | CheerioCrawler | 10x plus rapide, pas de surcharge navigateur |
| Contenu rendu en JavaScript | PlaywrightCrawler | Exécute le JS, attend le rendu |
| Applications monopage (SPAs) | PlaywrightCrawler | Gère le routage côté client |
| APIs retournant du HTML | CheerioCrawler | Besoin seulement de parser le HTML |
| Sites nécessitant une connexion | PlaywrightCrawler | Peut remplir des formulaires |
Étape 5 : Utiliser le Router pour les patterns multi-pages
Les scrapers réels doivent gérer différents types de pages différemment — pages de liste, pages de détail, résultats de recherche. Le Router de Crawlee rend cela propre :
Créez src/routes.ts :
import { createPlaywrightRouter, Dataset } from 'crawlee';
export const router = createPlaywrightRouter();
// Gestionnaire par défaut — pages de liste
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
log.info(`Traitement de la liste : ${request.url}`);
// Extraire les liens vers les éléments individuels
await enqueueLinks({
selector: 'a.item-link',
label: 'DETAIL',
});
// Gérer la pagination
const nextButton = await page.$('a.next-page');
if (nextButton) {
await enqueueLinks({
selector: 'a.next-page',
label: 'LISTING',
});
}
});
// Gestionnaire de page de détail
router.addHandler('DETAIL', async ({ request, page, pushData, log }) => {
log.info(`Traitement du détail : ${request.url}`);
await page.waitForSelector('.article-content', { timeout: 10000 });
const data = await page.evaluate(() => {
const title = document.querySelector('h1')?.textContent?.trim() || '';
const author = document.querySelector('.author-name')?.textContent?.trim() || '';
const date = document.querySelector('time')?.getAttribute('datetime') || '';
const content = document.querySelector('.article-content')?.textContent?.trim() || '';
const tags = Array.from(document.querySelectorAll('.tag'))
.map((tag) => tag.textContent?.trim() || '');
return { title, author, date, content, tags };
});
await pushData({
...data,
url: request.url,
scrapedAt: new Date().toISOString(),
});
});Mettez à jour src/main.ts pour utiliser le routeur :
import { PlaywrightCrawler } from 'crawlee';
import { router } from './routes.js';
const crawler = new PlaywrightCrawler({
requestHandler: router,
maxConcurrency: 5,
maxRequestsPerMinute: 30,
});
await crawler.run([
{ url: 'https://example-blog.com/articles', label: 'LISTING' },
{ url: 'https://example-blog.com/search?q=typescript', label: 'SEARCH' },
]);Étape 6 : Gérer la pagination et le défilement infini
Pagination traditionnelle
Pour les sites avec des boutons "Suivant" ou des pages numérotées :
router.addHandler('LISTING', async ({ page, enqueueLinks, request, log }) => {
const items = await page.$$eval('.item', (els) =>
els.map((el) => ({
title: el.querySelector('h2')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}))
);
log.info(`Page ${request.userData.page || 1} : ${items.length} éléments trouvés`);
for (const item of items) {
if (item.url) {
await enqueueLinks({
urls: [item.url],
label: 'DETAIL',
});
}
}
const nextUrl = await page.$eval('a.next', (el) => el.href).catch(() => null);
if (nextUrl) {
await enqueueLinks({
urls: [nextUrl],
label: 'LISTING',
userData: { page: (request.userData.page || 1) + 1 },
});
}
});Défilement infini
Pour les sites qui chargent plus de contenu en défilant :
router.addHandler('INFINITE', async ({ page, pushData, log }) => {
let previousHeight = 0;
let scrollAttempts = 0;
const maxScrolls = 20;
while (scrollAttempts < maxScrolls) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000);
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) {
log.info('Plus de contenu à charger');
break;
}
previousHeight = currentHeight;
scrollAttempts++;
log.info(`Défilement ${scrollAttempts}/${maxScrolls} — hauteur : ${currentHeight}`);
}
const allItems = await page.$$eval('.feed-item', (items) =>
items.map((item) => ({
text: item.querySelector('.content')?.textContent?.trim() || '',
author: item.querySelector('.author')?.textContent?.trim() || '',
timestamp: item.querySelector('time')?.getAttribute('datetime') || '',
}))
);
log.info(`${allItems.length} éléments extraits au total`);
for (const item of allItems) {
await pushData(item);
}
});Étape 7 : Rotation de proxies et anti-blocage
Le blocage est le plus grand défi du web scraping. Crawlee dispose d'outils intégrés pour vous aider :
Rotation de proxies basique
import { PlaywrightCrawler, ProxyConfiguration } from 'crawlee';
const proxyConfiguration = new ProxyConfiguration({
proxyUrls: [
'http://user:pass@proxy1.example.com:8080',
'http://user:pass@proxy2.example.com:8080',
'http://user:pass@proxy3.example.com:8080',
],
});
const crawler = new PlaywrightCrawler({
proxyConfiguration,
requestHandler: router,
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 100,
sessionOptions: {
maxUsageCount: 50,
},
},
});Bonnes pratiques anti-blocage
const crawler = new PlaywrightCrawler({
maxRequestsPerMinute: 20,
browserPoolOptions: {
useFingerprints: true,
fingerprintOptions: {
fingerprintGeneratorOptions: {
browsers: ['chrome', 'firefox'],
operatingSystems: ['windows', 'macos', 'linux'],
locales: ['en-US', 'en-GB'],
},
},
},
preNavigationHooks: [
async ({ page }) => {
const width = 1280 + Math.floor(Math.random() * 200);
const height = 800 + Math.floor(Math.random() * 200);
await page.setViewportSize({ width, height });
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
});
},
],
requestHandler: router,
});Étape 8 : Stockage de données et exportation
Crawlee fournit deux systèmes de stockage :
Dataset — Pour les données tabulaires
import { Dataset } from 'crawlee';
// Pousser des données pendant le scraping
await pushData({
name: 'TypeScript Handbook',
price: 29.99,
category: 'Programming',
});
// Après le scraping, exporter dans différents formats
const dataset = await Dataset.open();
// Exporter en JSON
await dataset.exportToJSON('output');
// Exporter en CSV
await dataset.exportToCSV('output');
// Obtenir toutes les données en une fois
const { items } = await dataset.getData();
console.log(`Total des éléments : ${items.length}`);Key-Value Store — Pour les données diverses
import { KeyValueStore } from 'crawlee';
const store = await KeyValueStore.open();
// Sauvegarder des captures d'écran
await store.setValue('homepage-screenshot', await page.screenshot(), {
contentType: 'image/png',
});
// Sauvegarder la configuration ou l'état
await store.setValue('scraper-config', {
lastRun: new Date().toISOString(),
totalPages: 1500,
errors: 12,
});Étape 9 : Gestion des erreurs et résilience
Les scrapers en production doivent gérer les échecs avec élégance :
import { PlaywrightCrawler, log } from 'crawlee';
const crawler = new PlaywrightCrawler({
maxRequestRetries: 3,
requestHandlerTimeoutSecs: 60,
async requestHandler({ request, page, pushData, session }) {
try {
const pageTitle = await page.title();
// Détecter les blocages légers
if (
pageTitle.toLowerCase().includes('captcha') ||
pageTitle.toLowerCase().includes('verify') ||
pageTitle.toLowerCase().includes('access denied')
) {
session?.retire();
throw new Error(`Blocage détecté à ${request.url}`);
}
// Attendre le contenu avec un repli
try {
await page.waitForSelector('.main-content', { timeout: 10000 });
} catch {
log.warning(`Sélecteur de contenu non trouvé à ${request.url}, tentative de repli`);
await page.waitForSelector('body', { timeout: 5000 });
}
const data = await page.evaluate(() => ({
title: document.querySelector('h1')?.textContent?.trim() || 'Sans titre',
content: document.querySelector('.main-content')?.textContent?.trim() || '',
}));
await pushData({
...data,
url: request.url,
retryCount: request.retryCount,
});
} catch (error) {
log.error(`Erreur lors du scraping de ${request.url}`, {
error: (error as Error).message,
retryCount: request.retryCount,
});
throw error;
}
},
async failedRequestHandler({ request, log }) {
log.error(`Échec définitif : ${request.url}`, {
errors: request.errorMessages,
retries: request.retryCount,
});
const dataset = await Dataset.open('failed-requests');
await dataset.pushData({
url: request.url,
errors: request.errorMessages,
failedAt: new Date().toISOString(),
});
},
});Étape 10 : Exemple concret — Scraping de tableau de bord emploi
Construisons un scraper complet qui extrait des offres d'emploi. Cet exemple démontre tous les concepts ensemble :
import { PlaywrightCrawler, Dataset, log } from 'crawlee';
interface JobListing {
title: string;
company: string;
location: string;
salary: string;
description: string;
tags: string[];
postedAt: string;
url: string;
scrapedAt: string;
}
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
maxRequestsPerMinute: 15,
maxRequestRetries: 3,
requestHandlerTimeoutSecs: 45,
browserPoolOptions: {
useFingerprints: true,
},
preNavigationHooks: [
async ({ page }) => {
await page.route('**/*.{png,jpg,jpeg,gif,webp,woff,woff2}', (route) =>
route.abort()
);
},
],
async requestHandler({ request, page, enqueueLinks, pushData, log }) {
const label = request.label || 'LISTING';
if (label === 'LISTING') {
log.info(`Scraping page de liste : ${request.url}`);
await page.waitForSelector('.job-card', { timeout: 15000 });
await enqueueLinks({
selector: '.job-card a.job-title-link',
label: 'JOB_DETAIL',
});
const hasNextPage = await page.$('a[aria-label="Next page"]');
if (hasNextPage) {
await enqueueLinks({
selector: 'a[aria-label="Next page"]',
label: 'LISTING',
});
}
}
if (label === 'JOB_DETAIL') {
log.info(`Scraping détail emploi : ${request.url}`);
await page.waitForSelector('.job-detail', { timeout: 15000 });
const job: JobListing = await page.evaluate(() => {
const getText = (selector: string): string =>
document.querySelector(selector)?.textContent?.trim() || '';
return {
title: getText('h1.job-title'),
company: getText('.company-name'),
location: getText('.job-location'),
salary: getText('.salary-range'),
description: getText('.job-description'),
tags: Array.from(document.querySelectorAll('.skill-tag')).map(
(tag) => tag.textContent?.trim() || ''
),
postedAt: getText('.posted-date'),
url: window.location.href,
scrapedAt: new Date().toISOString(),
};
});
if (job.title && job.company) {
await pushData(job);
log.info(`Sauvegardé : ${job.title} chez ${job.company}`);
}
}
},
});
log.info("Démarrage du scraper d'offres d'emploi...");
await crawler.run([
{ url: 'https://example-jobs.com/jobs?q=typescript', label: 'LISTING' },
]);
const dataset = await Dataset.open();
const { items } = await dataset.getData();
log.info(`Scraping terminé ! ${items.length} offres d'emploi extraites.`);
await dataset.exportToJSON('jobs');Étape 11 : Déployer avec Docker
Les projets Crawlee sont livrés avec un Dockerfile prêt pour la production :
FROM node:20-slim AS builder
RUN npx playwright install-deps chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM node:20-slim
RUN npx playwright install chromium
RUN npx playwright install-deps chromium
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN mkdir -p storage
ENV NODE_ENV=production
ENV CRAWLEE_STORAGE_DIR=./storage
CMD ["node", "dist/main.js"]Construisez et exécutez :
docker build -t my-scraper .
docker run -v $(pwd)/output:/app/storage my-scraperÉtape 12 : Patterns avancés
Crawling reprise automatique
Crawlee persiste sa file de requêtes sur le disque. Si votre scraper plante, redémarrez-le simplement — il reprend là où il s'est arrêté :
const crawler = new PlaywrightCrawler({
requestHandler: router,
});
// Premier lancement : traite toutes les URLs
await crawler.run(['https://example.com/page1', 'https://example.com/page2']);
// Si vous redémarrez, les URLs déjà traitées sont automatiquement ignoréesInterception des requêtes réseau
Surveillez et modifiez le trafic réseau pendant le scraping :
const crawler = new PlaywrightCrawler({
preNavigationHooks: [
async ({ page }) => {
page.on('response', async (response) => {
const url = response.url();
if (url.includes('/api/products')) {
try {
const data = await response.json();
const store = await KeyValueStore.open();
await store.setValue(`api-response-${Date.now()}`, data);
} catch {
// La réponse pourrait ne pas être en JSON
}
}
});
await page.route('**/*', (route) => {
const type = route.request().resourceType();
if (['image', 'font', 'stylesheet'].includes(type)) {
return route.abort();
}
return route.continue();
});
},
],
async requestHandler({ page, pushData }) {
await page.waitForSelector('.content');
const title = await page.title();
await pushData({ title });
},
});Dépannage
Problèmes courants
Le navigateur ne se lance pas dans Docker : Assurez-vous d'installer les dépendances Playwright :
npx playwright install-deps chromiumBlocages fréquents :
- Réduisez
maxConcurrencyetmaxRequestsPerMinute - Activez la rotation de proxies
- Utilisez
useFingerprints: true - Ajoutez des délais aléatoires entre les requêtes
Problèmes de mémoire avec de grands crawls :
- Utilisez
CheerioCrawlerau lieu dePlaywrightCrawlerquand possible - Limitez
maxConcurrencypour réduire la consommation mémoire - Définissez
maxRequestsPerCrawlpour traiter par lots
Prochaines étapes
Maintenant que vous avez un scraper Crawlee fonctionnel, voici comment le prolonger :
- Ajouter une base de données — Stockez les résultats dans PostgreSQL avec Drizzle ORM (voir notre tutoriel Drizzle)
- Construire une API — Servez les données extraites via une API REST avec Hono (voir notre tutoriel Hono)
- Planifier les exécutions — Utilisez GitHub Actions (voir notre tutoriel CI/CD)
- Ajouter l'extraction IA — Combinez avec Claude pour une analyse intelligente (voir notre tutoriel AI scraper)
Conclusion
Vous avez construit un scraper web de niveau production avec Crawlee et TypeScript. Vous savez maintenant :
- Choisir entre
CheerioCrawleretPlaywrightCrawlerselon votre site cible - Utiliser les routeurs pour gérer proprement différents types de pages
- Implémenter la gestion de pagination et du défilement infini
- Effectuer la rotation de proxies et gérer les sessions pour éviter les blocages
- Stocker et exporter les données dans plusieurs formats
- Déployer votre scraper avec Docker pour la production
Crawlee gère les parties difficiles — gestion des files, nouvelles tentatives, rotation de proxies, empreintes de navigateur — pour que vous puissiez vous concentrer sur la logique d'extraction qui compte.
Bon scraping !
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire des agents IA production-ready avec le Claude Agent SDK et TypeScript
Apprenez à construire des agents IA autonomes avec le Claude Agent SDK d'Anthropic en TypeScript. Ce tutoriel pratique couvre la boucle d'agent, les outils intégrés, les outils MCP personnalisés, les sous-agents, les modes de permissions et les patterns de déploiement en production.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.