How to Build a Link Preview API with Node.js and ToolCenter
Why You Need Link Previews
Every chat app, social platform, and messaging service shows link previews. You paste https://github.com, and you get a card with GitHub's logo, title, and description. That's not magic — it's parsing HTML and extracting Open Graph tags.
Building this yourself sucks. You'll spend days handling:
- Malformed HTML
- Missing og:image tags
- Slow-loading pages that timeout
- Different encodings
- Redirects and weird edge cases
I've been there. Here's how to skip the pain using an API.
Setup (5 minutes)
npm install toolcenter
Grab an API key from ToolCenter. The free tier gives you 100 calls/month — plenty for testing.
import ToolCenter from "toolcenter";
const tc = new ToolCenter("your-api-key");
const preview = await tc.linkPreview({
url: "https://github.com"
});
console.log(preview);
// {
// title: "GitHub: Let's build from here",
// description: "GitHub is where over 100 million developers...",
// image: "https://github.githubassets.com/images/...",
// favicon: "https://github.githubassets.com/favicons/favicon.svg",
// domain: "github.com"
// }
That's it. No HTML parsing libraries, no regex nightmares.
Building the API
Here's a basic Express endpoint:
import express from "express";
import ToolCenter from "toolcenter";
const app = express();
const tc = new ToolCenter(process.env.TOOLCENTER_API_KEY);
app.get("/preview", async (req, res) => {
const { url } = req.query;
if (!url || !url.startsWith("http")) {
return res.status(400).json({ error: "Valid URL required" });
}
try {
const preview = await tc.linkPreview({ url });
res.json(preview);
} catch (error) {
// Don't expose internal errors to users
res.status(500).json({
error: "Failed to fetch preview",
fallback: { title: new URL(url).hostname }
});
}
});
app.listen(3000);
Caching (Do This From Day 1)
Don't hit the API every time. Cache results:
const NodeCache = require("node-cache");
const cache = new NodeCache({ stdTTL: 3600 }); // 1 hour
app.get("/preview", async (req, res) => {
const { url } = req.query;
const cached = cache.get(url);
if (cached) {
return res.json(cached);
}
const preview = await tc.linkPreview({ url });
cache.set(url, preview);
res.json(preview);
});
For production, use Redis instead of in-memory cache.
Handling Failures Like a Pro
APIs fail. Networks timeout. Handle it gracefully:
async function getPreviewWithFallback(url) {
try {
return await tc.linkPreview({ url });
} catch (error) {
// Rate limited? Wait and retry once
if (error.status === 429) {
await new Promise(r => setTimeout(r, 1000));
try {
return await tc.linkPreview({ url });
} catch (e) {
// Still failed, use fallback
}
}
// Return minimal fallback
const domain = new URL(url).hostname;
return {
title: domain,
description: null,
image: null,
favicon: null,
url
};
}
}
Real-World Optimizations
Batch requests when possible:
const previews = await Promise.allSettled([
tc.linkPreview({ url: url1 }),
tc.linkPreview({ url: url2 }),
tc.linkPreview({ url: url3 })
]);
Validate URLs before sending:
function isValidUrl(string) {
try {
const url = new URL(string);
return url.protocol === "http:" || url.protocol === "https:";
} catch (_) {
return false;
}
}
Set timeouts for your API calls:
const preview = await tc.linkPreview({
url,
timeout: 10000 // 10 seconds max
});
The Alternative (Don't Do This)
You could build this yourself with Cheerio and Puppeteer:
// Please don't
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
const title = await page.$eval("title", el => el.textContent);
// ... 200 more lines of edge case handling
I've tried this. You'll spend weeks on edge cases and still miss things.
Cost Reality Check
ToolCenter's free tier (100 calls/month) costs $0. Paid plans start at €9/month for 10,000 calls.
Building this yourself costs weeks of developer time plus infrastructure. Your call.
Final Code
Here's the complete service with error handling, caching, and validation:
import express from "express";
import ToolCenter from "toolcenter";
import NodeCache from "node-cache";
const app = express();
const tc = new ToolCenter(process.env.TOOLCENTER_API_KEY);
const cache = new NodeCache({ stdTTL: 3600 });
function isValidUrl(string) {
try {
const url = new URL(string);
return url.protocol === "http:" || url.protocol === "https:";
} catch (_) {
return false;
}
}
app.get("/preview", async (req, res) => {
const { url } = req.query;
if (!url || !isValidUrl(url)) {
return res.status(400).json({ error: "Valid URL required" });
}
const cached = cache.get(url);
if (cached) {
return res.json(cached);
}
try {
const preview = await tc.linkPreview({ url, timeout: 10000 });
cache.set(url, preview);
res.json(preview);
} catch (error) {
const fallback = {
title: new URL(url).hostname,
description: null,
image: null,
url
};
res.json(fallback);
}
});
app.listen(3000);
Ship it.
ToolCenter has SDKs for Node.js, Python, and PHP. The link preview API is part of a larger suite — you also get screenshot, PDF, QR code, and email validation APIs under the same plan.