Skip to main content
Glama
by wei
hn-api.ts6.04 kB
/** * HackerNews API Client Service * * Provides methods to interact with the HackerNews Algolia API. * Uses native fetch API (Node.js 18+) with timeout handling. * * @see https://hn.algolia.com/api */ import type { HNUser, ItemResult, SearchResult } from "../types/index.js"; /** Base URL for HackerNews Algolia API */ const HN_API_BASE_URL = "https://hn.algolia.com/api/v1"; /** Default request timeout in milliseconds */ const DEFAULT_TIMEOUT = 5000; /** * Search parameters for HackerNews API */ export interface SearchParams { query: string; tags?: string[] | undefined; numericFilters?: string[] | undefined; page?: number | undefined; hitsPerPage?: number | undefined; } /** * HackerNews API client for making requests to the Algolia API */ export class HNAPIClient { private readonly baseUrl: string; private readonly timeout: number; /** * Create a new HNAPIClient instance * * @param baseUrl - Base URL for the API (default: HN_API_BASE_URL) * @param timeout - Request timeout in milliseconds (default: 5000ms) */ constructor(baseUrl: string = HN_API_BASE_URL, timeout: number = DEFAULT_TIMEOUT) { this.baseUrl = baseUrl; this.timeout = timeout; } /** * Fetch data from the API with timeout handling * * @param url - Full URL to fetch * @returns Parsed JSON response * @throws Error on network failure, timeout, or non-OK response */ private async fetchWithTimeout(url: string): Promise<unknown> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { Accept: "application/json", }, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error) { if (error.name === "AbortError") { throw new Error(`Request timeout after ${this.timeout}ms`); } throw error; } throw new Error("Unknown error during fetch"); } } /** * Build query string from search parameters * * @param params - Search parameters * @returns URL-encoded query string */ private buildQueryString(params: Partial<SearchParams>): string { const queryParams = new URLSearchParams(); if (params.query !== undefined) { queryParams.append("query", params.query); } if (params.tags && params.tags.length > 0) { queryParams.append("tags", params.tags.join(",")); } if (params.numericFilters && params.numericFilters.length > 0) { queryParams.append("numericFilters", params.numericFilters.join(",")); } if (params.page !== undefined) { queryParams.append("page", params.page.toString()); } if (params.hitsPerPage !== undefined) { queryParams.append("hitsPerPage", params.hitsPerPage.toString()); } return queryParams.toString(); } /** * Search HackerNews by relevance * * @param params - Search parameters * @returns Search results with hits, pagination, and metadata * @throws Error on API failure or network issues * * @example * ```typescript * const results = await client.search({ * query: "AI", * tags: ["story"], * numericFilters: ["points>=100"], * page: 0, * hitsPerPage: 20 * }); * ``` */ async search(params: SearchParams): Promise<SearchResult> { const queryString = this.buildQueryString(params); const url = `${this.baseUrl}/search?${queryString}`; return (await this.fetchWithTimeout(url)) as SearchResult; } /** * Search HackerNews by date (most recent first) * * @param params - Search parameters (query can be empty for all posts) * @returns Search results sorted by creation date * @throws Error on API failure or network issues * * @example * ```typescript * const results = await client.searchByDate({ * query: "", * tags: ["story"], * page: 0 * }); * ``` */ async searchByDate(params: Partial<SearchParams>): Promise<SearchResult> { const queryString = this.buildQueryString(params); const url = `${this.baseUrl}/search_by_date?${queryString}`; return (await this.fetchWithTimeout(url)) as SearchResult; } /** * Get a specific item by ID with nested children * * @param itemId - HackerNews item ID * @returns Item with nested comment tree * @throws Error if item not found or API failure * * @example * ```typescript * const item = await client.getItem("38456789"); * console.log(item.title); * console.log(item.children.length); // Number of top-level comments * ``` */ async getItem(itemId: string): Promise<ItemResult> { const url = `${this.baseUrl}/items/${itemId}`; const rawItem = await this.fetchWithTimeout(url); // Transform the raw API response to match our ItemResult type // The HN API returns id as number, but we want string return this.transformItemResult(rawItem); } /** * Transform raw API item to ItemResult type (recursively) * Converts id from number to string for all items in the tree */ private transformItemResult(item: unknown): ItemResult { const itemObj = item as ItemResult & { children?: unknown[] }; return { ...itemObj, id: String(itemObj.id), children: (itemObj.children || []).map((child: unknown) => this.transformItemResult(child)), } as ItemResult; } /** * Get a user's profile information * * @param username - HackerNews username * @returns User profile with karma, about, and creation date * @throws Error if user not found or API failure * * @example * ```typescript * const user = await client.getUser("pg"); * console.log(`Karma: ${user.karma}`); * ``` */ async getUser(username: string): Promise<HNUser> { const url = `${this.baseUrl}/users/${username}`; return (await this.fetchWithTimeout(url)) as HNUser; } } /** * Create a singleton instance of the HN API client */ export const hnApi = new HNAPIClient();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wei/hn-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server