Aller au contenu principal
Paris / --:--:--

Hey , Je m'appelle Clément.

Stage ingénieur informatique · juin 2027 Recrutez-moi
Retour

Suivi de candidatures avec découverte automatique d'offres

Rôle: Développeur full-stack (API, web, workers, infrastructure, CI/CD) Équipe: Équipe de 2 Durée: Avr 2025 – aujourd'hui (projet perso)

Résultats

  • En production sur pathpipe.clementomnes.dev, open source sous licence MIT ; environ 210 des ~340 commits sont les miens
  • Découverte d'offres en arrière-plan : pages carrières des entreprises suivies scrapées par un worker BullMQ, offres dédupliquées par une contrainte d'unicité PostgreSQL, alertes envoyées par un second worker avec 3 tentatives et backoff exponentiel
  • Durcissement de l'auth : JWT avec vérification d'email, OAuth Google et LinkedIn, OTP par email, clés d'API derrière un guard de rôles, rate limiting global (60 requêtes/min) couvert par des tests end-to-end
TypeScript NestJS Next.js PostgreSQL Redis BullMQ TypeORM Turborepo Docker GitHub Actions

Pathpipe est né parce que je suivais mes propres candidatures de stage dans un tableur. C’est devenu un projet open source à deux avec Pierre Guéroult : un outil où l’on consigne ses candidatures, suit les entreprises qui nous intéressent, et laisse un worker prévenir quand ces entreprises publient quelque chose de nouveau.

Dashboard Pathpipe

Architecture

Un monorepo Turborepo avec pnpm workspaces et trois apps déployables :

  • apps/api, un service NestJS. C’est le seul processus qui touche PostgreSQL. Règles métier, validation et autorisation vivent ici une seule fois, pas dans chaque client.
  • apps/web, un frontend Next.js App Router. Chaque appel API est une Server Action, donc les tokens et URL internes n’atteignent jamais le navigateur.
  • apps/worker, un binaire unique lancé avec un argument (email ou ats) qui fait tourner un consommateur BullMQ par processus. Les workers scalent, se déploient et tombent indépendamment de l’API.

Les packages partagés portent les contrats : @repo/db (entités TypeORM et DTO utilisés par l’API et le web), @repo/queues (noms de files, types de payload, options par défaut des jobs et workers), @repo/email (templates React Email), @repo/ui, plus les configs ESLint, Jest, Prettier et TypeScript. Un changement de DTO casse le build des deux côtés plutôt qu’à l’exécution.

Le pipeline de découverte d’offres

Les utilisateurs suivent des entreprises. Sur un cron (et à la demande depuis la page admin), le worker ATS demande à l’API toutes les entreprises suivies via un endpoint interne protégé par clé d’API, puis scrape chaque page carrières :

  1. HTTP simple d’abord. Récupération de la page avec un timeout de 15 s. Recherche de blocs application/ld+json de type JobPosting ; les données structurées donnent titre, URL, lieu, fourchette de salaire et date de publication gratuitement.
  2. Extraction heuristique s’il n’y a pas de JSON-LD : les liens candidats sont confrontés à des motifs d’URL d’offres et filtrés par une liste noire de titres non pertinents (navigation, pages légales, etc.).
  3. Repli navigateur headless. Si le HTTP ne donne rien, Playwright charge la page, attend le réseau au repos, et rejoue la même extraction JSON-LD puis DOM sur la page rendue. La plupart des ATS modernes rendent côté client, donc c’est cette étape qui rend la couverture acceptable.

Les résultats sont postés en batch à l’API, qui les insère un par un et traite une violation d’unicité PostgreSQL (23505) comme « déjà connu » plutôt que comme une erreur. Dès qu’au moins une ligne est nouvelle, le worker enfile un job d’alerte ; le worker email rend le template et l’envoie. Les valeurs par défaut viennent de @repo/queues : 3 tentatives avec backoff exponentiel à partir de 2 s, jobs terminés et échoués plafonnés, concurrence de 5 par worker avec un limiteur à 5 jobs/seconde.

Candidatures et entreprises

Les candidatures vivent sur un kanban en glisser-déposer dont l’ordre est persisté (colonne kanbanOrder), ou dans un tableau paginé avec recherche, tri (y compris par salaire), colonnes masquables et suppression par lot. La suppression est logique. Les entreprises ont un import/export CSV, des logos stockés en blob en base avec une recherche Brandfetch en repli, et un bouton suivre/ne plus suivre qui alimente le worker. La recherche floue d’entreprises utilise l’extension pg_trgm, activée par une migration TypeORM.

Authentification et durcissement

Stratégies Passport pour le login local, JWT, Google et LinkedIn. Vérification d’email et réinitialisation de mot de passe passent par la file email ; les comptes créés via un fournisseur OAuth sont bloqués pour la réinitialisation de mot de passe. Un flux OTP par email est disponible. Les clés d’API sont liées à un utilisateur et vérifiées par un guard avec les rôles : c’est ainsi que le worker s’authentifie sur les endpoints internes. Un throttler NestJS global autorise 60 requêtes par minute et est couvert par des tests e2e dans apps/api/test. Les health checks utilisent @nestjs/terminus.

Livraison

Les pull requests exécutent un workflow réutilisable build-lint-test plus le type-check et la revue de dépendances. Un push sur prod relance les mêmes vérifications, puis construit trois images Docker (api, web, worker) en parallèle, les pousse sur GitHub Container Registry, et appelle l’API Dokploy pour redéployer la stack Compose sur mon serveur. Les changements de schéma partent en migrations TypeORM avec une CLI ; la synchronisation automatique est désactivée en production. En local, pnpm run dev démarre PostgreSQL et Redis dans Docker, MailDev pour intercepter les mails sortants, et un serveur de prévisualisation React Email.

Ce que j’ai construit

J’ai écrit l’API NestJS (auth, candidatures, entreprises, offres, clés d’API, endpoints internes, rate limiting, migrations), l’app Next.js (kanban, vue tableau, gestion des entreprises, paramètres, site marketing), les packages @repo/db, @repo/queues, @repo/email et @repo/ui, la migration des workers vers BullMQ, les Dockerfiles et fichiers Compose, et les pipelines GitHub Actions dont le déploiement Dokploy.

Pourquoi ces choix

Une API propriétaire de la base. Avec un frontend, une UI admin et deux workers, laisser chacun parler à PostgreSQL aurait signifié quatre copies des règles d’autorisation. Les workers appellent l’API avec une clé d’API à la place.

Server Actions plutôt qu’une API cliente publique. Les identifiants restent côté serveur et le bundle navigateur n’embarque aucun client API. Le coût : chaque interaction fait un aller-retour par le serveur Next.js, ce qui convient à une application faite de formulaires.

BullMQ plutôt qu’un scheduler in-process. Scraper avec un navigateur headless est lent et fragile. Le mettre derrière une file donne retries, backoff et rate limiting gratuitement, et empêche un Chromium bloqué de faire tomber l’API.

Scraping progressif. Le JSON-LD est bon marché et structuré, donc on l’essaie d’abord. Playwright est coûteux, donc en dernier. La liste noire et les motifs d’URL sont volontairement des heuristiques simples ; la contrainte d’unicité en aval fait qu’un faux positif coûte une tentative en doublon, pas une ligne en doublon.

Contact

Parlons-en.

Pas de formulaires, pas d’agenda, envoyez-moi un email avec ce que vous avez en tête. Je lis tout.

[email protected]