---
title: "How to Build a News Aggregator App in 30 Minutes (2026)"
description: "Learn how to build a news aggregator app in 30 minutes with vanilla JavaScript. Search, filters, rate-limit handling — full working code included."
source: https://apitube.io/blog/post/build-news-aggregator-app-30-minutes
---

# How to Build a News Aggregator App in 30 Minutes (APITube + Vanilla JS)

**A news aggregator app is a single interface that pulls articles from many publishers through one API and displays them together.** No RSS scraping, no per-site parsers — one `fetch` call returns a unified feed. This tutorial builds a working one in vanilla HTML and JavaScript, with search, language filter, source-quality badges, and honest rate-limit handling, all in under 30 minutes.

_This article is for developers who know basic JavaScript (arrays, `fetch`, DOM) and want a shippable project, not another copy-paste exercise that breaks on deploy._

*Disclosure: this tutorial uses APITube, a news API I work on. The code works with any JSON-returning news API if you swap the endpoint.*

**Key takeaways**

- A news aggregator app pulls articles from many publishers through one API call, not many.
- You can ship a working one in vanilla HTML and JavaScript in under 30 minutes.
- Only three production traps matter: API-key secrecy, rate-limit handling, and CORS.
- APITube's free tier (30 requests per 30 minutes) is enough to build and demo; production needs a backend proxy.

## What you'll build (1 min)

A single-page aggregator that:

- Shows 20 current articles in a card layout
- Has a search box (title match) and a language dropdown
- Displays the source name, a 3-tier source-quality badge, and the article category
- Handles quota-exhausted responses gracefully instead of blanking out

By the end: one `index.html` + `style.css` + `app.js`. No framework, no build step, no backend. Zero dependencies.

## Prerequisites (1 min)

- A browser (Chrome or Firefox — I'll explain a production CORS note later)
- A code editor
- An APITube free API key (next step)

No Node.js, no npm. If you can open an HTML file in a browser, you're set.

## Step 1 — Get your APITube API key (2 min)

Go to apitube.io, sign up, and copy the API key from your dashboard. The free tier is **30 requests per 30 minutes, first page only** — enough to develop and test this tutorial several times over.

Create a `.env` file in your project folder:

```
APITUBE_KEY=your_key_here
```

Add `.env` to a `.gitignore` file next to it:

```
.env
```

Why bother with `.env` in a vanilla-JS project? Because you'll eventually move this key behind a server or serverless function for production — and checking keys into git is a one-way mistake. We'll load it into the browser via a `config.js` placeholder for now, but the discipline starts here.

## Step 2 — Set up the project (3 min)

Create three files in a folder called `news-app`:

**`index.html`**:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="dns-prefetch" href="//apitube.io">
<link rel="dns-prefetch" href="//api.apitube.io">
<link rel="dns-prefetch" href="//docs.apitube.io">
  <meta charset="UTF-8">
  <title>News Aggregator</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1>News Aggregator</h1>
    <div class="controls">
      <input id="search" placeholder="Search headlines..." />
      <select id="lang">
        <option value="en">English</option>
        <option value="de">German</option>
        <option value="fr">French</option>
        <option value="es">Spanish</option>
      </select>
    </div>
  </header>
  <main id="feed"></main>
  <script  src="config.js" defer></script>
  <script  src="app.js" defer></script>
</body>
</html>
```

**`config.js`** (gitignored eventually; for the tutorial, hardcode your key here and keep it out of git):

```javascript
window.APITUBE_KEY = "your_key_here";
```

**`style.css`** (minimal, copy as-is):

```css
* { box-sizing: border-box; margin: 0; padding: 0; font-family: system-ui, sans-serif; }
body { background: #f6f7f9; color: #1a1a1a; padding: 24px; max-width: 960px; margin: 0 auto; }
header { margin-bottom: 24px; }
h1 { font-size: 28px; margin-bottom: 12px; }
.controls { display: flex; gap: 8px; }
.controls input, .controls select { padding: 8px 12px; font-size: 14px; border: 1px solid #ddd; border-radius: 6px; }
.controls input { flex: 1; }
.card { background: white; padding: 16px; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.card h2 { font-size: 18px; margin-bottom: 6px; }
.card a { color: #0066cc; text-decoration: none; }
.meta { font-size: 13px; color: #666; margin-top: 8px; display: flex; gap: 8px; flex-wrap: wrap; }
.badge { padding: 2px 8px; border-radius: 4px; font-size: 12px; }
.badge-tier1 { background: #d4edda; color: #155724; }
.badge-tier2 { background: #fff3cd; color: #856404; }
.badge-tier3 { background: #f8d7da; color: #721c24; }
.error { background: #fff3cd; padding: 16px; border-radius: 8px; color: #856404; }
```

## Step 3 — Fetch articles from APITube (8 min)

Create `app.js`:

```javascript
const API = "https://api.apitube.io/v1/news/everything";

async function fetchNews({ query = "", language = "en" } = {}) {
  const params = new URLSearchParams({
    "language.code": language,
    "per_page": 20,
    "sort.by": "published_at",
    "sort.order": "desc"
  });
  if (query) params.set("title", query);

  const res = await fetch(`${API}?${params}`, {
    headers: { "X-API-Key": window.APITUBE_KEY }
  });

  if (res.status === 429) throw new Error("QUOTA_EXHAUSTED");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const data = await res.json();
  return data.results || [];
}
```

Three things worth noting:

1. **Async/await, not `.then()`.** Error handling with try/catch reads cleaner and matches any modern codebase you'll land in.
2. **HTTP 429 gets a named error.** Every news API sends 429 when you hit the quota. Catching it specifically lets us show something useful instead of a white page.
3. **The API key lives in a header**, not a query parameter. Query-string keys leak into browser history, referrer headers, and server logs. APITube enforces header-only auth for a reason.

A note on CORS: this code runs in the browser because APITube allows cross-origin requests with the `X-API-Key` header. Not every news API does — NewsAPI.org, notably, blocks browser-origin requests on its free plan entirely. If you swap APIs later, you may need a tiny serverless proxy (Cloudflare Worker or Vercel function — 15 lines).

## Step 4 — Render article cards with source rank and category (7 min)

Most tutorials render `title` + `description` + `image` and call it done. APITube's response has 15+ fields. Here are two that make your app look less like a toy:

- `source.rank.level` — integer 1-3 where 1 = tier-1 outlet (Reuters, BBC), 3 = long tail
- `categories[0].name` — e.g., "Technology", "Finance"

Add this to `app.js`:

```javascript
const feedEl = document.getElementById("feed");

function render(articles) {
  feedEl.innerHTML = "";
  if (articles.length === 0) {
    feedEl.innerHTML = "<p>No articles found.</p>";
    return;
  }
  for (const a of articles) {
    const tier = a.source?.rank?.level || 3;
    const category = a.categories?.[0]?.name || "General";
    const card = document.createElement("article");
    card.className = "card";
    card.innerHTML = `
      <h2><a href="${a.href}" target="_blank" rel="noopener">${a.title}</a></h2>
      <p>${a.description || ""}</p>
      <div class="meta">
        <span>${a.source?.name || "Unknown"}</span>
        <span class="badge badge-tier${tier}">Tier ${tier}</span>
        <span class="badge">${category}</span>
        <span>${new Date(a.published_at).toLocaleDateString()}</span>
      </div>
    `;
    feedEl.appendChild(card);
  }
}
```

Note: `textContent` isn't used here because we trust the API's cleaned output — but if you were rendering user-submitted search terms into the DOM, you'd want to sanitize. The API fields themselves are safe to interpolate.

## Step 5 — Add search and language filter (6 min)

Wire the inputs to a re-fetch on change. Debounce search by 400 ms so we don't burn quota on every keystroke:

```javascript
const searchEl = document.getElementById("search");
const langEl = document.getElementById("lang");

let timer;
function debouncedLoad() {
  clearTimeout(timer);
  timer = setTimeout(load, 400);
}

async function load() {
  try {
    const articles = await fetchNews({
      query: searchEl.value.trim(),
      language: langEl.value
    });
    render(articles);
  } catch (err) {
    showError(err);
  }
}

searchEl.addEventListener("input", debouncedLoad);
langEl.addEventListener("change", load);

load();
```

That's the entire interactivity layer. Typing "climate" filters to articles with "climate" in the title. Changing language swaps the feed.

## Step 6 — Handle rate limits and errors (3 min)

This is the step every tutorial skips. On the free tier, the app above burns through 30 requests fast during development. Without handling, your UI goes blank and users blame the app. Handle it:

```javascript
function showError(err) {
  const msg = err.message === "QUOTA_EXHAUSTED"
    ? "You've hit the free-tier limit (30 requests per 30 minutes). Try again shortly."
    : `Couldn't load news: ${err.message}`;
  feedEl.innerHTML = `<div class="error">${msg}</div>`;
}
```

**Quota math for real usage**: 30 requests per 30 minutes = one refresh every 60 seconds if a user holds the tab open. Fine for personal use, not fine for anything deployed to more than one user. Two production moves:

1. **Cache responses in `localStorage`** with a 5-minute TTL — cuts calls by roughly 80% for normal browsing
2. **Move the fetch server-side** when you deploy — one backend call can serve many users from one cached response

Both are 20-line additions, not rewrites. The free tier exists to prototype; pick a production path before you deploy.

## Complete code

You now have three files: `index.html`, `style.css`, `app.js`, and a gitignored `config.js` holding your key. Open `index.html` in a browser — you'll see 20 articles with search and language filters that actually work.

A runnable version: github.com/apitube-examples/news-aggregator-30min (clone, add your key to `config.js`, open `index.html`).

## Which news API should you use?

Every tutorial picks one API without explaining why. I've used these four across client projects. The criteria: free-tier usability, source count, response richness, and whether the API actively blocks browser usage.

| API | Free Tier | Sources | Response Richness | Browser-Friendly |
|---|---|---|---|---|
| APITube | 30 req / 30 min, first page | 300,000+ | Sentiment, entities, source rank, categories, summary | Yes |
| NewsAPI.org | 100 req/day | ~80,000 | Title, description, url, image | No (CORS blocked on free) |
| NewsData.io | 200 req/day | ~60,000 | Title, description, category | Partial |
| GNews | 100 req/day | ~60,000 | Title, description, image | Yes |

Unlike NewsAPI.org, which blocks browser requests on its free tier, APITube and GNews allow direct client-side calls — which means you can ship this tutorial to a demo URL without a serverless proxy.

**Methodology**: I built this same aggregator against each API, counted distinct sources returned for `title=climate&language.code=en`, and checked whether the browser request worked without a proxy.

**Rough guide**:

- **Rich fields + browser-ready** → APITube
- **Tightest budget, willing to add a backend** → NewsAPI.org
- **Simple needs, Latin-alphabet languages** → GNews

If you want the long version with monthly-cost math at scale, see the pillar comparison: apitube.io/blog/best-news-api-2026-comparison. For language-specific ergonomics: apitube.io/blog/best-news-api-javascript-nodejs-2026.

## FAQ

### What is a news aggregator app?

A news aggregator app is a client application that fetches articles from many publishers through one API and displays them in a single unified interface. Unlike a per-site news reader that connects to each publisher separately, a news aggregator pulls from hundreds or thousands of sources in one call, which means you write one `fetch` instead of one parser per outlet.

### Which API is best for building a news aggregator?

The best news API for a beginner aggregator is APITube, because it allows browser-based requests without a proxy and returns rich fields (sentiment, source ranking, entities, categories) that let you render a better UI in fewer lines of code. NewsAPI.org is cheaper per request but blocks browser calls on the free tier, so it requires a backend. GNews is the simplest option for Latin-alphabet-only projects.

### How long does it take to build a news aggregator?

Building a functional news aggregator with search, filters, and error handling takes about 30 minutes when you use a news API, which is the target for this tutorial. Building the same app from scratch (per-publisher scrapers, parsers, storage) takes 1–2 weeks; adding production features like caching, a backend proxy, and auth adds roughly one weekend on top of the 30-minute prototype.

### Is it legal to build a news aggregator?

Building a news aggregator is legal when you use a licensed news API such as APITube or NewsAPI.org, because the API provider handles publisher licensing on your behalf. What is not legal is scraping headlines and article bodies directly from publisher websites and republishing them without permission — that is a copyright issue regardless of how the aggregator is built.

### Do I need a backend for a news aggregator app?

A news aggregator app does not need a backend for personal or demo use; this tutorial runs entirely in the browser with one `fetch` call. A backend is required in production, because the API key must stay off the client, responses should be cached to avoid per-user quota burn, and some news APIs (NewsAPI.org, for example) block browser calls outright. A 15-line Cloudflare Worker or Vercel serverless function is the minimum viable backend.

## Next steps

With 30 minutes of work you have a news aggregator that reads more fields than most tutorials, survives a rate-limit hit without crashing, and justifies its API choice. Three natural extensions:

1. Save articles to `localStorage` for offline reading
2. Add category filters using `categories[].id`
3. Deploy behind a serverless function so the API key lives on the server

Try APITube free — 30 requests every 30 minutes on the free tier, no credit card. apitube.io

## Resources

- **APITube** — [apitube.io](https://apitube.io) — try it free, sentiment and entities included on every article
- **Documentation** — [docs.apitube.io](https://docs.apitube.io) — endpoints, parameters, response structure, integrations
- **Pricing** — [apitube.io/pricing](https://apitube.io/pricing) — all tiers
- **APITube blog** — [apitube.io/blog](https://apitube.io/blog) — more guides and comparisons

**Related guides:**
- [React News Dashboard Tutorial](https://apitube.io/blog/react-news-dashboard-tutorial)
