Express + TypeScript service that searches news through the GNews API. Searches support provider-backed filters for language, country, date range, and sort order. Identical normalized searches are cached (in-memory by default, or Redis when REDIS_URL is set) to protect quota and latency. The upstream base URL is injectable for deterministic local integration tests and benchmarks.
A search runs through a small, explicit pipeline — each stage is a separate, testable unit:
- Validate — query params are checked before any network call:
queryis required,countis clamped (≤100),lang/countrymust be ISO two-letter codes,from/tomust parse as ISO 8601, andsortBy∈ {publishedAt,relevance}. Bad input fails fast with400instead of wasting an upstream call or quota. - Cache — parameters are normalized into a deterministic key and read through a pluggable store (in-memory by default, Redis when
REDIS_URLis set). Cache failures are logged/metriced but do not fail article requests; identical in-flight misses in the same process share one upstream call. - Upstream — on a miss, GNews is called with a hard timeout; transport/provider failures surface as
502, and repeated failures open a short circuit that returns503without amplifying the outage. - Observe — each step emits structured Pino logs (carrying
x-request-id), Prometheus counters (cache hit/miss/stale fallback, upstream outcome, latency histogram), and optional OpenTelemetry spans. - Respond — legacy endpoints return raw article arrays, while
/api/v1/*returns{ data, meta }envelopes with request/cache metadata,X-API-Version, cache-status headers for searches, and structured error bodies.
Full diagram and component notes live in docs/ARCHITECTURE.md.
- Security: Helmet headers, configurable rate limiting, optional
TRUST_PROXYfor correct client IPs behind a load balancer, optionalCLIENT_API_KEYS+X-API-Keyon/api/*. - Reliability: Upstream HTTP timeouts, response validation, stale-on-error cache fallback,
502for provider/transport failures, provider circuit breaker with503short-circuiting, graceful shutdown onSIGTERM/SIGINT. - Observability: JSON logs via Pino,
x-request-id,GET /metrics(Prometheus text format), cache hit/miss/stale/error/coalescing + upstream latency/circuit metrics, and optional OpenTelemetry traces to OTLP (OTEL_EXPORTER_OTLP_*). - Kubernetes-style probes:
GET /health(liveness),GET /ready(readiness when the API key is configured). - Supply chain:
npm auditin CI; SPDX SBOM artifacts; Docker builds with SBOM + provenance; dependency review on PRs; optional SLSA-style lockfile attestation onmain; lockfile-only installs. - Contract: OpenAPI at
GET /openapi.yaml(also on disk as docs/openapi.yaml). - Container: multi-stage Dockerfile (non-root user, healthcheck).
- Deploy: Example Kubernetes manifests.
Automation: CI (Node 20/22), CodeQL, Codecov upload, dependency review, SBOM, provenance attest, releases on tags, and Dependabot (npm, Docker, Actions). Details: docs/CI.md. Operations: docs/OPERATIONS.md. TypeScript client: docs/CLIENT.md. Security: SECURITY.md.
Deployment guide: docs/DEPLOYMENT.md covers safe public-demo settings and Render/Fly/Railway Docker paths.
git clone https://github.com/<your-username>/news-api.git
cd news-api
cp .env.example .env
# Set GNEWS_API_KEY (and optional PORT)
npm ci
npm run lint
npm test
npm run build
npm startDevelopment with reload:
npm run devSmoke-test a running instance:
BASE_URL=http://localhost:3000 QUERY=postgres npm run smoke
# If CLIENT_API_KEYS is configured on the server:
CLIENT_API_KEY=client-secret-one npm run smokeRun the deterministic local benchmark:
npm run benchmark:localdocker build -t news-api:local .
docker run --rm -p 3000:3000 -e GNEWS_API_KEY=your_key news-api:localOr Compose (expects GNEWS_API_KEY in .env):
docker compose up --buildRequired and optional variables are listed in .env.example and docs/OPERATIONS.md. Never commit .env.
Base path: /api. Machine-readable schema: GET /openapi.yaml · source file docs/openapi.yaml.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Service capability document linking API versions, docs, and observability endpoints. |
GET |
/health |
Liveness: { "status": "ok", "uptime": number }. |
GET |
/ready |
Readiness; 503 if GNEWS_API_KEY missing (non-test). |
GET |
/openapi.yaml |
OpenAPI 3 document (application/yaml). |
GET |
/metrics |
Prometheus metrics (skips rate limit), including HTTP totals, cache hit/miss/stale/error/coalescing counts, and upstream latency. |
GET |
/api/v1/articles |
Versioned search. Returns { data, meta }, including normalized filters, cache status, and requestId. |
GET |
/api/v1/articles/search |
Alias for versioned search. |
GET |
/api/v1/articles/title/:title |
Exact title match with { data, meta }, else structured 404. |
GET |
/api/v1/sources/:source/articles |
Source-name filter with { data, meta }. |
GET |
/api/articles |
Search. Query: query (required), count (optional, default 10, max 100), plus optional lang, country, from, to, sortBy. Optional header X-API-Key if CLIENT_API_KEYS is set. |
GET |
/api/articles/title/:title |
Exact title match in the current search window, else 404. |
GET |
/api/articles/source |
Filter by source.name (case-insensitive). Query: source (required), count optional, plus optional lang, country, from, to, sortBy. |
Examples
GET /api/v1/articles?query=technology&count=5
GET /api/v1/articles/search?query=postgres&lang=en&country=us&sortBy=relevance
GET /api/v1/sources/BBC/articles?count=10
GET /api/articles?query=technology&count=5
GET /api/articles?query=postgres&lang=en&country=us&sortBy=relevance
GET /api/articles?query=aws&from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z
GET /api/articles/title/Example%20Headline
GET /api/articles/source?source=BBC&count=10Search filters are validated before the upstream request. lang and country are two-letter codes, from and to must parse as ISO 8601 dates, and sortBy accepts publishedAt or relevance.
Legacy errors: { "error": "message" }. Versioned /api/v1/* success responses include X-API-Version: v1; v1 search responses also include X-Cache-Status: hit|miss|stale. Versioned errors: { "error": { "code": "...", "message": "...", "requestId": "..." } }. Rate limit: 429 with standard rate-limit headers.
| Script | Purpose |
|---|---|
npm run dev |
nodemon + ts-node on src/server.ts. |
npm run build |
Compile to dist/. |
npm start |
Run dist/server.js. |
npm test |
Vitest once. |
npm run test:watch |
Vitest watch. |
npm run test:coverage |
Tests + coverage. |
npm run lint |
ESLint. |
npm run contract |
Validate docs/openapi.yaml with Redocly CLI. |
npm run client:generate |
Generate TypeScript client types from docs/openapi.yaml. |
npm run client:check |
Regenerate client types and fail if checked-in output is stale. |
npm run smoke |
Curl-based smoke test against a running instance (BASE_URL, QUERY, COUNT, optional CLIENT_API_KEY). |
npm run smoke:docker |
Compose smoke test: boot the image against a fake GNews provider and run npm run smoke. |
npm run benchmark:local |
Builds the app, starts a fake GNews provider, and measures cold searches vs warm cache hits. See docs/BENCHMARKS.md. |
src/app.ts— Middleware stack,/health,/ready,/apimount.src/server.ts— Env, API key check, HTTP server, graceful shutdown.src/tracing.ts/src/otel-bootstrap.ts— Optional OpenTelemetry (before Express loads).src/logger.ts— Pino + request logging.src/middleware/— Security headers, rate limit, trust proxy, metrics observer, optional client API key, errors.src/http/responses.ts— Versioned response envelope helpers.src/client/— OpenAPI-generated TypeScript types and small v1 client wrapper.src/cache/store.ts— Pluggable cache: memory or Redis.src/metrics/register.ts— Prometheus registry: HTTP, cache, and upstream provider metrics.src/providers/gnewsProvider.ts— GNews adapter: provider params, payload validation, timeouts, upstream instrumentation.src/services/newsService.ts— Article search orchestration: normalized cache keys, cache resilience/coalescing, title/source narrowing.test/— Vitest; HTTP tests mockaxios(no live GNews in CI), including OpenAPI-backed response contract checks.
Architecture diagram: docs/ARCHITECTURE.md. Release notes: CHANGELOG.md.
See CONTRIBUTING.md.
MIT. See LICENSE.