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.

Top comments (0)