Skip to content
Learni
View all tutorials
Développement Backend

How to Implement Lead Scoring with Node.js in 2026

Lire en français

Introduction

Lead scoring is a key marketing automation technique that assigns a numerical score to each prospect (lead) based on their behavior and traits. It helps prioritize 'hot' leads ready to convert, optimizing sales team efforts. Imagine a funnel where only the best leads rise to the top automatically – that's what lead scoring does.

In this beginner tutorial, we'll build a complete system with Node.js, Express, and TypeScript. We'll calculate scores using real criteria: professional email (+10 pts), pages visited (+5 pts per page), email opens (+15 pts), and resource downloads (+25 pts). The result? A REST API that stores leads in memory and ranks them by score.

Why 2026? With AI on the rise, these solid basics are essential before adding machine learning. This is actionable: copy-paste the code and test in 15 minutes. Perfect for freelancers or startups in growth hacking. (142 words)

Prerequisites

  • Node.js 20+ installed (download here)
  • Basic JavaScript/TypeScript knowledge
  • An editor like VS Code
  • Terminal (bash or PowerShell)
  • 5 minutes for setup

Initialize the project

terminal
mkdir lead-scoring-app
cd lead-scoring-app
npm init -y
npm install express typescript @types/express @types/node ts-node
dev: npm install -D nodemon
npm pkg set type="module"
npx tsc --init
mkdir src

This command sets up a modern Node.js project with TypeScript and Express. The --init -y flag quickly creates package.json, ts-node runs TS directly, and nodemon auto-reloads the dev server. Set 'type=module' to avoid ESM errors.

Define Lead types

Before diving into code, let's structure our data. A lead will have an ID, email, pages visited, emails opened, and resources downloaded. The score will be calculated dynamically.

Lead types and interface

src/types.ts
export interface Lead {
  id: string;
  email: string;
  pagesVisited: number;
  emailsOpened: number;
  resourcesDownloaded: number;
  score?: number;
}

export type Leads = Lead[]; 

This interface defines a lead with its key properties. score is optional since it's computed on the fly. Use it everywhere for strict typing, preventing runtime errors like negative pagesVisited.

Score calculation function

src/scoring.ts
import { Lead } from './types.js';

export function calculateScore(lead: Lead): number {
  let score = 0;

  // Email professionnel (+10)
  if (/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.(com|fr|io)$/i.test(lead.email) && !lead.email.includes('gmail.com') && !lead.email.includes('yahoo.com')) {
    score += 10;
  }

  // Pages visitées (+5 par page, max 50)
  score += Math.min(lead.pagesVisited * 5, 50);

  // Emails ouverts (+15 par ouverture, max 45)
  score += Math.min(lead.emailsOpened * 15, 45);

  // Ressources (+25 par téléchargement, max 75)
  score += Math.min(lead.resourcesDownloaded * 25, 75);

  return score;
}

export function scoreAllLeads(leads: Lead[]): Lead[] {
  return leads.map(lead => ({ ...lead, score: calculateScore(lead) }));
}

The calculateScore function applies concrete business rules with caps to prevent inflation. Regex detects pro emails, multipliers are limited. scoreAllLeads updates an entire list. Test it standalone before integration.

Set up the server

We'll create a minimal Express server with CORS for browser testing. Leads are stored in memory (global array) for beginner simplicity.

Main Express server

src/server.ts
import express from 'express';
import cors from 'cors';
import { Lead, Leads } from './types.js';
import { calculateScore, scoreAllLeads } from './scoring.js';

const app = express();
const PORT = 3000;
let leads: Leads = [];

app.use(cors());
app.use(express.json());

app.get('/leads', (req, res) => {
  const scoredLeads = scoreAllLeads(leads);
  scoredLeads.sort((a, b) => (b.score || 0) - (a.score || 0));
  res.json(scoredLeads);
});

app.post('/leads', (req, res) => {
  const newLead: Lead = {
    id: Date.now().toString(),
    ...req.body
  };
  leads.push(newLead);
  res.status(201).json({ message: 'Lead ajouté', lead: newLead });
});

app.listen(PORT, () => {
  console.log(`Serveur lead scoring sur http://localhost:${PORT}`);
});

This server exposes two routes: GET /leads (sorted list by descending score) and POST /leads (add new). scoreAllLeads recalculates on every request for freshness. Descending sort prioritizes top leads. Run with npx ts-node src/server.ts.

TypeScript configuration

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "allowJs": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "ts-node": {
    "esm": true
  }
}

This tsconfig enables ESM, strict mode, and transpilation. ts-node.esm allows native TS execution. Copy-paste to avoid module errors like 'Cannot use import statement'.

package.json scripts

package.json
{
  "name": "lead-scoring-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "nodemon --exec ts-node src/server.ts",
    "start": "ts-node src/server.ts"
  },
  "dependencies": {
    "express": "^4.19.2",
    "typescript": "^5.6.2",
    "@types/express": "^4.17.21",
    "@types/node": "^22.5.5",
    "ts-node": "^10.9.2",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  }
}

Update package.json with these scripts and dependencies. npm run dev starts in watch mode. Added cors for frontend testing. Run npm install afterward.

Test the API

Start the server: npm run dev.

Use curl or Postman:

  • POST: curl -X POST http://localhost:3000/leads -H "Content-Type: application/json" -d '{"email":"pro@entreprise.fr","pagesVisited":4,"emailsOpened":2,"resourcesDownloaded":1}'
  • GET: curl http://localhost:3000/leads

Example score: 10 (pro email) + 20 (4 pages) + 30 (2 emails) + 25 (1 resource) = 85 pts.

HTML/JS test client

test-client.html
<!DOCTYPE html>
<html>
<head><title>Test Lead Scoring</title></head>
<body>
  <button onclick="addLead()">Ajouter Lead</button>
  <button onclick="getLeads()">Lister Leads</button>
  <pre id="output"></pre>

  <script>
    async function addLead() {
      const response = await fetch('http://localhost:3000/leads', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
          email: 'test@pro.fr',
          pagesVisited: 5,
          emailsOpened: 1,
          resourcesDownloaded: 0
        })
      });
      const data = await response.json();
      document.getElementById('output').textContent += JSON.stringify(data, null, 2) + '\n';
    }

    async function getLeads() {
      const response = await fetch('http://localhost:3000/leads');
      const leads = await response.json();
      document.getElementById('output').textContent = JSON.stringify(leads, null, 2);
    }
  </script>
</body>
</html>

Open this HTML file in a browser for interactive testing. Add leads and list them sorted. Verify recalculated scores. Great for demos without external tools.

Best practices

  • Dynamic weighting: Adjust coefficients via JSON config for A/B testing.
  • Persistence: Switch to SQLite/Prisma for production (replace array with DB).
  • Validation: Add Zod to validate inputs (e.g., pagesVisited >= 0).
  • Alert thresholds: Notify Slack for scores >80 (add webhooks).
  • Logging: Use Winston to trace score calculations.

Common errors to avoid

  • No caps: Without Math.min, 100 pages could score 500+ (unrealistic).
  • Forget sorting: Always sort descending to prioritize.
  • Infinite memory: Limit to 1000 leads, archive old ones.
  • Missing CORS: Frontend errors without app.use(cors()).

Next steps