Backend service that aggregates tech news from multiple sources (TabNews, Hacker News, Dev.to) with AI-curated highlights. Built with Hono and Bun for high performance.
This API provides a unified feed that combines news articles from different tech communities, applies intelligent ranking algorithms, and uses Google Gemini AI to generate curated highlights. The service is designed to be stateless, horizontally scalable, and optimized for low latency.
- Bun - Fast JavaScript runtime
- Hono - Lightweight web framework
- TypeScript - Static typing
- TSyringe - Dependency injection with decorators
- Pino - Structured logging
- Google Gemini AI - Content analysis and summarization
- Vitest - Testing framework
- Bun 1.0+
- Google Gemini API key (for AI features)
# Install dependencies
bun install
# Copy environment variables
cp .env.example .env
# Configure your .env file with:
# - GEMINI_API_KEY (required for highlights)
# - PORT (default: 8080)# Development mode (hot reload, port 3001)
bun run dev
# Production mode (port 8080)
bun startServer runs on:
- Development: http://localhost:3001
- Production: http://localhost:8080
GET /api/feed?limit=10&after=<cursor>Unified feed with cursor-based pagination. Interleaves news and AI-curated highlights in a 5:1 ratio.
Query Parameters:
limit- Items per page (1-10, default: 10)after- Cursor for next page (ID of last item from previous response)
Response:
{
"items": [
{
"id": "unique-id",
"title": "Article title",
"author": "username",
"score": 42,
"publishedAt": "2025-12-15T10:00:00.000Z",
"source": "TabNews",
"url": "https://...",
"commentCount": 15
}
],
"nextCursor": "id-for-next-page"
}GET /api/news/tabnews # All TabNews articles
GET /api/news/hackernews # All Hacker News articles
GET /api/comments/:username/:slug # TabNews post comments
GET /api/services/status # External services health checkGET /Returns API information and available endpoints.
Data Fetching Services
TabNewsService- Fetches articles from TabNews APIHackerNewsService- Fetches stories from Hacker News APIDevToService- Fetches articles from Dev.to API
Ranking Services
RankingService- Time-decayed engagement ranking for newsHighlightRankingService- AI relevance-based ranking for highlights
Aggregation Services
SmartMixService- Combines TabNews + Hacker News with intelligent interleavingHighlightsService- Generates AI-curated highlights from Dev.to
Infrastructure Services
CacheService- In-memory caching with TTLGeminiService- Google Gemini AI integrationLoggerService- Request-scoped logging with correlation IDs
Uses TSyringe for dependency injection:
@singleton()
export class MyService {
constructor(
@inject(CacheService) private cache: CacheService,
@inject(LoggerService) private logger: LoggerService
) {}
}All services are singletons and resolved via container.resolve(ServiceClass) in route handlers.
News items are ranked using time-decayed engagement:
score = (points + comments × 0.3) × timePenalty
Time Penalty:
- Posts < 6 hours old: Reduced score (too recent)
- Posts 6 hours to 5 days old: Full score (sweet spot)
- Posts > 5 days old: Exponential decay with gravity factor of 1.8
This balances fresh content with high-quality older posts.
In-memory cache with automatic expiration:
- News articles: 5 minutes TTL
- AI highlights: 10 minutes TTL
- Cache clears on server restart (ephemeral)
- Pattern: Check cache → Fetch on miss → Store → Return
Dual-mode logging with Pino:
Development:
- Colorized output with pino-pretty
- Human-readable timestamps
Production:
- Structured JSON logs
- GCP Cloud Logging format
- Correlation IDs for distributed tracing
- Severity levels mapped to GCP standards
Uses AsyncLocalStorage for request tracing:
- Each request gets a unique correlation ID
- IDs propagate through all async operations
- Enables distributed tracing across services
bun test # Watch mode
bun run test:ui # UI mode
bun run test:run # Run once
bun run test:coverage # Coverage reportTests use Vitest with TypeScript. Remember to:
- Import "reflect-metadata" at top of test files
- Clear DI container between tests:
container.clearInstances() - Mock external APIs with
vi.fn()andvi.spyOn()
Create a .env file:
PORT=8080 # Server port
GEMINI_API_KEY=your_key_here # Google Gemini API (required for highlights)
TWITTER_BEARER_TOKEN=token # Optional: Twitter API
TWITTER_API_KEY=key # Optional: Twitter API
TWITTER_API_SECRET=secret # Optional: Twitter APIAllowed origins:
http://localhost:3000(local development)http://0.0.0.0:3000https://tech-news-front-361874528796.southamerica-east1.run.app(Cloud Run)https://news.andreello.dev.br(production)
Configure in src/index.ts if you need additional origins.
All endpoints return standardized error responses:
{
"success": false,
"error": "Descriptive error message"
}HTTP status codes:
400- Bad Request404- Not Found500- Internal Server Error
Errors are logged with correlation IDs and stack traces for debugging.
tech-news-api/
├── src/
│ ├── index.ts # Server entry point, routes
│ ├── logger.ts # Pino logger configuration
│ ├── types.ts # TypeScript types
│ ├── context/
│ │ └── request-context.ts # AsyncLocalStorage for correlation IDs
│ ├── middleware/
│ │ └── logging.ts # Request logging middleware
│ └── services/
│ ├── cache.service.ts # In-memory caching
│ ├── tabnews.service.ts # TabNews API client
│ ├── hackernews.service.ts # Hacker News API client
│ ├── devto.service.ts # Dev.to API client
│ ├── ranking.service.ts # News ranking algorithm
│ ├── smartmix.service.ts # News aggregation
│ ├── highlights.service.ts # AI highlights generation
│ └── gemini.service.ts # Google Gemini AI client
├── package.json
├── tsconfig.json
├── .env.example
└── README.md
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 8080
CMD ["bun", "start"]Build and run:
docker build -t tech-news-api .
docker run -p 8080:8080 tech-news-apiThe application is optimized for Cloud Run:
- Runs on port 8080 (default)
- Structured logging for GCP Cloud Logging
- Stateless design (horizontally scalable)
- Health checks via
/api/services/status
- Create service in
src/services/<source>.service.tswith@singleton()decorator - Implement
fetchNews(): Promise<NewsItem[]>method - Add source to
Sourceenum insrc/types.ts - Update
SmartMixServiceto include new source - Add route in
src/index.ts - Update cache key in
CacheKeyenum
Edit src/services/ranking.service.ts:
COMMENT_WEIGHT- Comment importance vs score (default: 0.3)MIN_IDEAL_HOURS- Minimum age for full score (default: 6)MAX_IDEAL_HOURS- Maximum age before decay (default: 120)GRAVITY- Decay rate for old posts (default: 1.8)
- Add route in
src/index.tsusingapp.get()orapp.post() - Resolve services via
container.resolve(ServiceClass) - Access logger via
c.get("logger") - Wrap logic in try-catch
- Return JSON via
c.json(data, statusCode) - Update root endpoint and 404 handler
MIT
Built with Hono and Bun