Building a Real-time News Aggregator with Joffstrends Search API and Python/Node.js
In the era of information overload, staying updated with real-time news is crucial for businesses, researchers, and indie hackers alike. Whether you are building a niche market intelligence tool, a personal dashboard, or a curated newsletter platform, a news aggregator is an incredibly powerful asset.
However, if you have ever tried to build one, you have likely run into a major roadblock: the exorbitant cost of search APIs. Industry giants charge hundreds of dollars per month for basic search queries, making bootstrapping almost impossible for indie developers.
Enter the Joffstrends Search API. At just £9.99/month, it offers a highly reliable, lightning-fast, and incredibly cost-effective alternative to SerpAPI and Google Custom Search.
In this comprehensive tutorial, we will build a fully functional, real-time news aggregator from scratch. We will cover backend implementations in both Node.js and Python, show you how to process and clean the search data, and build a clean, responsive frontend dashboard to display your aggregated news.
Why Joffstrends Search API?
Before we dive into the code, let's look at why Joffstrends is perfect for this project:
- Unbeatable Pricing: Just £9.99/month on Gumroad, compared to $50-$100+ for competitors.
- Simplicity: A clean, standard JSON output that is easy to parse.
- Speed: Low-latency responses perfect for real-time applications.
- Developer-First: No complex configurations or heavy SDKs required.
Prerequisites
To follow this tutorial, you will need:
- Node.js (v16+) or Python (3.8+) installed on your machine.
- A basic understanding of JavaScript/Node.js or Python.
- A Joffstrends Search API subscription. You can sign up and get your API key via the Joffstrends Gumroad Page.
Step 1: Understanding the API Endpoint
The Joffstrends Search API is straightforward. It accepts standard HTTP GET requests and returns structured search results.
-
Base URL:
https://api.joffstrends.co.uk/search -
Query Parameters:
-
q: The search query (e.g.,artificial intelligence newsorindie hackers) -
key: Your unique API key obtained from Gumroad.
-
Let's look at how to implement the backend aggregator in both Node.js and Python.
Step 2: Backend Implementation (Option A: Node.js & Express)
If you prefer JavaScript, Node.js is an excellent choice for handling asynchronous API requests. We will use express to serve our dashboard and axios to fetch data from Joffstrends.
First, initialize your project and install the dependencies:
mkdir news-aggregator-node
cd news-aggregator-node
npm init -y
npm install express axios dotenv
Create a .env file to store your API key securely:
PORT=3000
JO_API_KEY=your_actual_gumroad_api_key_here
Now, create server.js and add the following code:
const express = require('express');
const axios = require('axios');
const path = require('path');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.JO_API_KEY;
const API_URL = 'https://api.joffstrends.co.uk/search';
// Serve static files for our frontend
app.use(express.static(path.join(__dirname, 'public')));
// API Endpoint to fetch aggregated news
app.get('/api/news', async (req, res) => {
const query = req.query.q || 'latest technology news';
try {
console.log(`Fetching news for query: "${query}"`);
const response = await axios.get(API_URL, {
params: {
q: query,
key: API_KEY
}
});
// Process and structure the results
const rawResults = response.data.results || [];
const processedNews = rawResults.map((item, index) => ({
id: index + 1,
title: item.title || 'No Title Available',
url: item.url || '#',
snippet: item.snippet || 'No description available.',
source: extractDomain(item.url) || 'Web Search'
}));
res.json({ success: true, data: processedNews });
} catch (error) {
console.error('Error fetching data from Joffstrends:', error.message);
res.status(500).json({ success: false, error: 'Failed to fetch news data.' });
}
});
// Helper function to extract domain name from URL
function extractDomain(url) {
if (!url) return null;
try {
const parsed = new URL(url);
return parsed.hostname.replace('www.', '');
} catch (e) {
return null;
}
}
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Step 3: Backend Implementation (Option B: Python & Flask)
If you are a Python developer, Flask is lightweight and perfect for this task. We will use requests to query the Joffstrends API.
First, install the required packages:
pip install Flask requests python-dotenv
Create a .env file:
JO_API_KEY=your_actual_gumroad_api_key_here
Create app.py and add the following code:
import os
from flask import Flask, jsonify, request, send_from_directory
import requests
from urllib.parse import urlparse
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__, static_folder='public', static_url_path='')
API_KEY = os.getenv('JO_API_KEY')
API_URL = 'https://api.joffstrends.co.uk/search'
@app.route('/')
def index():
return send_from_directory('public', 'index.html')
@app.route('/api/news', methods=['GET'])
def get_news():
query = request.args.get('q', 'latest technology news')
try:
print(f"Fetching news for query: {query}")
response = requests.get(API_URL, params={
'q': query,
'key': API_KEY
})
response.raise_for_status()
data = response.json()
raw_results = data.get('results', [])
processed_news = []
for idx, item in enumerate(raw_results):
url = item.get('url', '#')
domain = urlparse(url).netloc.replace('www.', '') if url != '#' else 'Web Search'
processed_news.append({
'id': idx + 1,
'title': item.get('title', 'No Title Available'),
'url': url,
'snippet': item.get('snippet', 'No description available.'),
'source': domain
})
return jsonify({'success': True, 'data': processed_news})
except Exception as e:
print(f"Error fetching data: {str(e)}")
return jsonify({'success': False, 'error': 'Failed to fetch news data.'}), 500
if __name__ == '__main__':
app.run(port=3000, debug=True)
Step 4: Building the Frontend Dashboard
Now that we have our backend API endpoint ready (which normalizes the search results and extracts the source domain), let's build a beautiful, responsive frontend dashboard.
Create a directory named public in your project root, and add two files: index.html and app.js.
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-time News Aggregator</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 text-slate-800 min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-white border-b border-slate-200 py-6 shadow-sm">
<div class="max-w-5xl mx-auto px-4 flex flex-col md:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-3xl">📰</span>
<h1 class="text-2xl font-bold text-slate-900">TrendPulse</h1>
</div>
<div class="flex w-full md:w-auto gap-2">
<input type="text" id="search-input" placeholder="Search topics (e.g., AI, SaaS, Web3)..."
class="w-full md:w-80 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
<button id="search-btn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-lg font-medium transition">
Search
</button>
</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-grow max-w-5xl w-full mx-auto px-4 py-8">
<!-- Quick Filters -->
<div class="flex flex-wrap gap-2 mb-8">
<button class="filter-btn bg-indigo-100 text-indigo-800 px-4 py-1.5 rounded-full text-sm font-semibold" data-query="latest technology news">🔥 Tech News</button>
<button class="filter-btn bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 px-4 py-1.5 rounded-full text-sm font-medium" data-query="indie hackers saas">🚀 Indie Hacking</button>
<button class="filter-btn bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 px-4 py-1.5 rounded-full text-sm font-medium" data-query="artificial intelligence breakthroughs">🤖 AI & ML</button>
<button class="filter-btn bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 px-4 py-1.5 rounded-full text-sm font-medium" data-query="cryptocurrency regulation">🪙 Web3 & Crypto</button>
</div>
<!-- News Feed -->
<div id="news-feed" class="grid gap-6">
<!-- Loading Skeleton -->
<div class="animate-pulse bg-white p-6 rounded-xl border border-slate-200">
<div class="h-4 bg-slate-200 rounded w-1/4 mb-4"></div>
<div class="h-6 bg-slate-200 rounded w-3/4 mb-2"></div>
<div class="h-4 bg-slate-200 rounded w-full"></div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-white border-t border-slate-200 py-6 text-center text-sm text-slate-500">
<p>Powered by <a href="https://api.joffstrends.co.uk" class="text-indigo-600 hover:underline font-medium" target="_blank">Joffstrends Search API</a> (£9.99/mo alternative)</p>
</footer>
<script src="app.js"></script>
</body>
</html>
public/app.js
document.addEventListener('DOMContentLoaded', () => {
const newsFeed = document.getElementById('news-feed');
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const filterBtns = document.querySelectorAll('.filter-btn');
let currentQuery = 'latest technology news';
// Fetch and render news
async function fetchNews(query) {
showLoading();
try {
const response = await fetch(`/api/news?q=${encodeURIComponent(query)}`);
const result = await response.json();
if (result.success && result.data.length > 0) {
renderNews(result.data);
} else {
showEmptyState(query);
}
} catch (error) {
console.error('Error fetching news:', error);
showErrorState();
}
}
// Render news cards
function renderNews(articles) {
newsFeed.innerHTML = articles.map(article => `
<article class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition duration-200 flex flex-col justify-between">
<div>
<div class="flex items-center gap-2 mb-3">
<span class="bg-indigo-50 text-indigo-700 text-xs font-semibold px-2.5 py-1 rounded">
${article.source}
</span>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-2 hover:text-indigo-600 transition">
<a href="${article.url}" target="_blank" rel="noopener noreferrer">${article.title}</a>
</h3>
<p class="text-slate-600 text-sm leading-relaxed mb-4">
${article.snippet}
</p>
</div>
<div class="flex items-center justify-between border-t border-slate-100 pt-4 mt-2">
<a href="${article.url}" target="_blank" rel="noopener noreferrer"
class="text-indigo-600 hover:text-indigo-800 text-sm font-semibold inline-flex items-center gap-1">
Read full article
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</a>
</div>
</article>
`).join('');
}
function showLoading() {
newsFeed.innerHTML = `
<div class="animate-pulse bg-white p-6 rounded-xl border border-slate-200">
<div class="h-4 bg-slate-200 rounded w-1/4 mb-4"></div>
<div class="h-6 bg-slate-200 rounded w-3/4 mb-2"></div>
<div class="h-4 bg-slate-200 rounded w-full"></div>
</div>
`;
}
function showEmptyState(query) {
newsFeed.innerHTML = `
<div class="text-center py-12 bg-white rounded-xl border border-slate-200">
<span class="text-4xl">🔍</span>
<h3 class="text-lg font-bold text-slate-900 mt-4">No results found</h3>
<p class="text-slate-500 mt-2">We couldn't find any news for "${query}". Try another query.</p>
</div>
`;
}
function showErrorState() {
newsFeed.innerHTML = `
<div class="text-center py-12 bg-red-50 rounded-xl border border-red-200">
<span class="text-4xl">⚠️</span>
<h3 class="text-lg font-bold text-red-900 mt-4">Something went wrong</h3>
<p class="text-red-600 mt-2">Failed to load news. Please check your API key configuration.</p>
</div>
`;
}
// Event Listeners
searchBtn.addEventListener('click', () => {
const query = searchInput.value.trim();
if (query) {
currentQuery = query;
fetchNews(currentQuery);
}
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchBtn.click();
}
});
filterBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
// Update active button styles
filterBtns.forEach(b => {
b.classList.remove('bg-indigo-100', 'text-indigo-800');
b.classList.add('bg-white', 'border', 'border-slate-200', 'text-slate-600');
});
btn.classList.remove('bg-white', 'border', 'border-slate-200', 'text-slate-600');
btn.classList.add('bg-indigo-100', 'text-indigo-800');
currentQuery = btn.dataset.query;
fetchNews(currentQuery);
});
});
// Initial Load
fetchNews(currentQuery);
});
Step 5: Running the Application
To run your real-time news aggregator, simply start your server:
For Node.js:
node server.js
For Python:
python app.py
Open your browser and navigate to http://localhost:3000. You will see a clean, modern dashboard loading real-time news articles fetched directly via the Joffstrends Search API!
Production Considerations
When deploying your news aggregator to production, keep these best practices in mind:
- Caching: To avoid hitting rate limits and to speed up load times for your users, implement a caching layer (like Redis or simple in-memory caching) on your backend. Cache results for popular queries (like "tech news") for 15-30 minutes.
- Sanitization: Always sanitize user search inputs on the backend to prevent injection attacks.
- Error Handling: Ensure your application handles API downtime gracefully by serving cached results or showing friendly error messages.
Conclusion
Building a real-time news aggregator doesn't have to cost a fortune. With the Joffstrends Search API, you can build production-grade, data-rich applications for a fraction of the cost of traditional search APIs.
Next Steps:
- Subscribe to the Search API on the Joffstrends Gumroad Page for just £9.99/month.
- Read the Docs: Explore the full API capabilities at api.joffstrends.co.uk.
- Extend the App: Add email alerts (using SendGrid or Mailgun) to notify users when new articles matching their keywords are found.
Happy hacking! 🚀
Top comments (0)