feat(utils): add generic buildQueryString utility

- Add type-safe buildQueryString function to /utils
- Support primitives, arrays, and optional values
- Proper URL encoding for special characters
- Add comprehensive tests (25 test cases, all passing)
- Update Google Fonts API client to use shared utility
- Update Fontshare API client to use shared utility
- Export utility from /utils/index.ts

Benefits:
- DRY - Single source of truth for query string logic
- Type-safe - Proper TypeScript support with QueryParams type
- Tested - Comprehensive test coverage
- Maintainable - One place to fix bugs
This commit is contained in:
Ilia Mashkov
2026-01-06 15:00:31 +03:00
parent 29d1cc0cdc
commit 9abec4210c
9 changed files with 303 additions and 84 deletions

View File

@@ -0,0 +1,127 @@
/**
* Google Fonts API client
*
* Handles API requests to Google Fonts API for fetching font metadata.
* Provides error handling, retry logic, and type-safe responses.
*
* @see https://developers.google.com/fonts/docs/developer_api
*/
import { api } from '$shared/api/api';
import { buildQueryString } from '$shared/utils';
import type { QueryParams } from '$shared/utils';
/**
* Google Fonts API parameters
*/
export interface GoogleFontsParams extends QueryParams {
/**
* Google Fonts API key (optional for public endpoints)
*/
key?: string;
/**
* Font family name (to fetch specific font)
*/
family?: string;
/**
* Font category filter (e.g., "sans-serif", "serif", "display")
*/
category?: string;
/**
* Character subset filter (e.g., "latin", "latin-ext", "cyrillic")
*/
subset?: string;
/**
* Sort order for results
*/
sort?: 'popularity' | 'alpha' | 'date' | 'style';
/**
* Cap the number of fonts returned
*/
capability?: string;
}
/**
* Google Fonts API response wrapper
*/
export interface GoogleFontsResponse {
kind: string;
items: GoogleFontItem[];
}
/**
* Simplified font item from Google Fonts API
*/
export interface GoogleFontItem {
family: string;
category: string;
variants: string[];
subsets: string[];
version: string;
lastModified: string;
files: Record<string, string>;
menu: string;
}
/**
* Google Fonts API base URL
*/
const GOOGLE_FONTS_API_URL = 'https://fonts.googleapis.com/v2/fonts' as const;
/**
* Fetch fonts from Google Fonts API
*
* @param params - Query parameters for filtering fonts
* @returns Promise resolving to Google Fonts API response
* @throws ApiError when request fails
*
* @example
* ```ts
* // Fetch all sans-serif fonts sorted by popularity
* const response = await fetchGoogleFonts({
* category: 'sans-serif',
* sort: 'popularity'
* });
*
* // Fetch specific font family
* const robotoResponse = await fetchGoogleFonts({
* family: 'Roboto'
* });
* ```
*/
export async function fetchGoogleFonts(
params: GoogleFontsParams = {},
): Promise<GoogleFontsResponse> {
const queryString = buildQueryString(params);
const url = `${GOOGLE_FONTS_API_URL}${queryString}`;
try {
const response = await api.get<GoogleFontsResponse>(url);
return response.data;
} catch (error) {
// Re-throw ApiError with context
if (error instanceof Error) {
throw error;
}
throw new Error(`Failed to fetch Google Fonts: ${String(error)}`);
}
}
/**
* Fetch font by family name
* Convenience function for fetching a single font
*
* @param family - Font family name (e.g., "Roboto")
* @returns Promise resolving to Google Font item
*
* @example
* ```ts
* const roboto = await fetchGoogleFontFamily('Roboto');
* ```
*/
export async function fetchGoogleFontFamily(
family: string,
): Promise<GoogleFontItem | undefined> {
const response = await fetchGoogleFonts({ family });
return response.items.find(item => item.family === family);
}