face-api.js made face recognition in JavaScript easy, but its last release (0.22.2) shipped in March 2020 and still pins TensorFlow.js core 1.7.0. The best-known fork, @vladmandic/face-api, is now archived too, yet the two packages still pull about 465,000 and 518,000 npm downloads a month.
Here is how to replace it with a face recognition API from Node.js 20: built-in fetch and FormData, no SDK, no model weights, no canvas or tfjs-node build. All code below ran against the live API.
Want to run it on your own photos? Get a key for the Face Analyzer API.
face-api.js calls mapped to endpoints
| face-api.js | Face Analyzer API |
|---|---|
nets.*.loadFromDisk("./models") | nothing to load |
detectAllFaces().withFaceLandmarks() | /faceanalysis (boundingBox, landmarks) |
.withAgeAndGender(), .withFaceExpressions() | /faceanalysis (AgeRange, Gender, Emotions) |
euclideanDistance(d1, d2) < 0.6 | /compare-faces (matchedFaces, unmatchedFaces) |
new FaceMatcher(labeledDescriptors) | /save-face-in-repository with external_id |
faceMatcher.findBestMatch() | /search-face-in-repository (label + Similarity) |
One helper for every endpoint
// face.mjs import { openAsBlob } from "node:fs"; const BASE = "https://faceanalyzer-ai.p.rapidapi.com"; const HEADERS = { "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com", "x-rapidapi-key": process.env.RAPIDAPI_KEY, }; export async function callApi(endpoint, files = {}, fields = {}) { const form = new FormData(); for (const [name, path] of Object.entries(files)) { form.append(name, await openAsBlob(path), path); } for (const [name, value] of Object.entries(fields)) { form.append(name, value); } const res = await fetch(`${BASE}${endpoint}`, { method: "POST", headers: HEADERS, body: form, }); return (await res.json()).body; } Detection and attributes
import { callApi } from "./face.mjs"; const { faces } = await callApi("/faceanalysis", { image: "team.jpg" }); console.log(faces.length, "faces"); for (const face of faces) { const { Gender, Eyeglasses, Emotions } = face.facialFeatures; console.log(Gender, Eyeglasses ? "glasses" : "no glasses", Emotions.join(", ")); } 5 faces Female glasses HAPPY Male no glasses HAPPY Male glasses HAPPY Male glasses CALM Female no glasses HAPPY Compare two faces
async function samePerson(sourcePath, targetPath) { const body = await callApi("/compare-faces", { source_image: sourcePath, target_image: targetPath, }); return body.matchedFaces.length > 0; } console.log(await samePerson("alex_badge.jpg", "alex_today.jpg")); // true console.log(await samePerson("alex_badge.jpg", "maya.jpg")); // false No threshold to tune. On a photo of a colleague, the API even found Alex out of focus in the background, half hidden behind an arm.
Replace FaceMatcher
const REPO = "team-faces"; await callApi("/create-facial-repository", {}, { repository_id: REPO }); const team = { alex: "alex_badge.jpg", maya: "maya.jpg", sam: "sam.jpg" }; for (const [label, path] of Object.entries(team)) { await callApi("/save-face-in-repository", { image: path }, { repository_id: REPO, external_id: label, max_faces: "1", }); } async function whoIs(path) { const { FaceMatches = [] } = await callApi( "/search-face-in-repository", { image: path }, { repository_id: REPO }, ); const best = FaceMatches.reduce( (a, b) => (!a || b.Similarity > a.Similarity ? b : a), null, ); return best && best.Similarity >= 90 ? `${best.ExternalImageId} (${best.Similarity.toFixed(2)})` : "unknown"; } console.log(await whoIs("alex_office.jpg")); // alex (100.00) console.log(await whoIs("visitor.jpg")); // unknown Browser uploads: proxy through your server
Keep the API key out of the browser with a small Express route (npm i express multer):
import express from "express"; import multer from "multer"; const app = express(); const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 } }); app.post("/api/faces", upload.single("image"), async (req, res) => { if (!req.file) return res.status(400).json({ error: "image is required" }); const form = new FormData(); form.append("image", new Blob([req.file.buffer], { type: req.file.mimetype }), req.file.originalname); const r = await fetch("https://faceanalyzer-ai.p.rapidapi.com/faceanalysis", { method: "POST", headers: { "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com", "x-rapidapi-key": process.env.RAPIDAPI_KEY, }, body: form, }); res.status(r.status).json((await r.json()).body); }); app.listen(3000); Migration tips
- Drop
face-api.js,canvas,@tensorflow/tfjs-nodeand themodelsfolder - Re-enroll reference photos with your existing labels as
external_id(descriptors cannot be imported) - Scale boxes by image size: coordinates come back from 0 to 1
- No face means empty lists, not an error
Full guide with the mapping details: face-api.js alternative on ai-engine.net.
Sources: face-api.js, @vladmandic/face-api, Node.js fs.openAsBlob.
👉 Grab a key from the Face Analyzer API page and run the scripts on your own photos.

