Turning any RSS feed into a fast JSON API on Vercel


Modern apps often need content from blogs or news feeds. Many sites expose RSS, but your frontend likely wants JSON. This post shows how to ship a small serverless endpoint on Vercel that converts RSS to JSON and applies real caching with ETag and conditional requests. It stays simple, avoids heavy infra, and is production minded.
What you will build
A serverless API:
GET /api/rss?url=<rss_url>
XML parsed to a small JSON schema
ETag passthrough and conditional requests
Cache headers that let browsers and CDNs do the work
Why this approach
Works on the free tier
No database needed
Plays well with any frontend
Honors the origin site caching policy
JSON shape
{
"title": "Feed title",
"link": "https://example.com",
"lastBuildDate": "2025-08-24T10:00:00.000Z",
"items": [
{
"title": "Post title",
"link": "https://example.com/post",
"pubDate": "2025-08-23T08:00:00.000Z",
"description": "Summary..."
}
]
}
Core API logic (Vercel serverless function, JavaScript)
// api/rss.js
import { XMLParser } from "fast-xml-parser";
/**
* Minimal RSS to JSON with HTTP caching.
* Usage: /api/rss?url=https://your-feed.xml
*/
export default async function handler(req, res) {
try {
const feedUrl = req.query.url;
if (!feedUrl) {
res.status(400).json({ error: "Missing url query parameter" });
return;
}
// Forward If-None-Match to support conditional GET at the origin
const clientIfNoneMatch = req.headers["if-none-match"] || "";
const originResp = await fetch(feedUrl, {
headers: clientIfNoneMatch ? { "If-None-Match": clientIfNoneMatch } : {},
});
// If origin returns 304, pass it through
if (originResp.status === 304) {
res.status(304).end();
return;
}
if (!originResp.ok) {
res.status(originResp.status).json({ error: `Upstream error ${originResp.status}` });
return;
}
const etag = originResp.headers.get("etag") || "";
const cacheControl =
originResp.headers.get("cache-control") || "public, max-age=300, stale-while-revalidate=60";
const xml = await originResp.text();
const parser = new XMLParser({
ignoreAttributes: true,
attributeNamePrefix: "",
trimValues: true,
});
const rss = parser.parse(xml);
// Support both RSS 2.0 and Atom shapes in a small, defensive way
const channel = rss?.rss?.channel || rss?.feed;
if (!channel) {
res.status(422).json({ error: "Unsupported feed format" });
return;
}
const items =
(channel.item || channel.entry || []).map((it) => {
const link =
typeof it.link === "string"
? it.link
: Array.isArray(it.link)
? it.link[0]?.href || it.link[0]
: it.link?.href || it.link?.["@_href"] || "";
return {
title: it.title?.["#text"] || it.title || "",
link,
pubDate: it.pubDate || it.updated || it.published || "",
description: it.description || it.summary || "",
};
}) || [];
const payload = {
title: channel.title?.["#text"] || channel.title || "",
link:
typeof channel.link === "string"
? channel.link
: channel.link?.href || channel.link?.["@_href"] || "",
lastBuildDate: channel.lastBuildDate || channel.updated || "",
items,
};
// Set caching headers so clients and Vercel CDN can cache
if (etag) res.setHeader("ETag", etag);
res.setHeader("Cache-Control", cacheControl);
res.status(200).json(payload);
} catch (err) {
res.status(500).json({ error: "Server error", detail: String(err?.message || err) });
}
}
Quick client fetch example
<script>
async function loadFeed() {
const url = encodeURIComponent("https://hnrss.org/frontpage");
const resp = await fetch(`/api/rss?url=${url}`);
if (resp.status === 304) return; // not changed
const data = await resp.json();
console.log("Feed title:", data.title);
console.log("First item:", data.items[0]);
}
loadFeed();
</script>
Testing tips
Use a real feed such as
https://hnrss.org/frontpage
Call once, then again with
If-None-Match
from the first response to see a 304 from the originInspect
Cache-Control
to confirm caching behavior
Limits and next steps
No persistence. For global rate limits, add Vercel KV or Upstash later.
Add JSON schema validation for consumers.
Add a list of allowed origins if you want to restrict usage.
Subscribe to my newsletter
Read articles from Tobechi Duru directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tobechi Duru
Tobechi Duru
Software Engineer, MERN-Stack Developer