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

@@ -12,11 +12,13 @@ import type {
FontshareFont,
} from '$entities/Font';
import { api } from '$shared/api/api';
import { buildQueryString } from '$shared/utils';
import type { QueryParams } from '$shared/utils';
/**
* Fontshare API parameters
*/
export interface FontshareParams {
export interface FontshareParams extends QueryParams {
/**
* Filter by categories (e.g., ["Sans", "Serif", "Display"])
*/
@@ -47,36 +49,6 @@ export interface FontshareResponse extends FontshareApiModel {
// Response structure matches FontshareApiModel
}
/**
* Build query string from parameters
*/
function buildQueryString(params: FontshareParams): string {
const searchParams = new URLSearchParams();
if (params.categories?.length) {
searchParams.append('categories', params.categories.join(','));
}
if (params.tags?.length) {
searchParams.append('tags', params.tags.join(','));
}
if (params.page) {
searchParams.append('page', String(params.page));
}
if (params.limit) {
searchParams.append('limit', String(params.limit));
}
if (params.search) {
searchParams.append('search', params.search);
}
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Fetch fonts from Fontshare API
*

View File

@@ -8,11 +8,13 @@
*/
import { api } from '$shared/api/api';
import { buildQueryString } from '$shared/utils';
import type { QueryParams } from '$shared/utils';
/**
* Google Fonts API parameters
*/
export interface GoogleFontsParams {
export interface GoogleFontsParams extends QueryParams {
/**
* Google Fonts API key (optional for public endpoints)
*/
@@ -66,40 +68,6 @@ export interface GoogleFontItem {
*/
const GOOGLE_FONTS_API_URL = 'https://fonts.googleapis.com/v2/fonts' as const;
/**
* Build query string from parameters
*/
function buildQueryString(params: GoogleFontsParams): string {
const searchParams = new URLSearchParams();
if (params.key) {
searchParams.append('key', params.key);
}
if (params.family) {
searchParams.append('family', params.family);
}
if (params.category) {
searchParams.append('category', params.category);
}
if (params.subset) {
searchParams.append('subset', params.subset);
}
if (params.sort) {
searchParams.append('sort', params.sort);
}
if (params.capability) {
searchParams.append('capability', params.capability);
}
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Fetch fonts from Google Fonts API
*

View File

@@ -7,33 +7,33 @@
export {
fetchGoogleFontFamily,
fetchGoogleFonts,
} from './googleFonts';
} from './google/googleFonts';
export type {
GoogleFontItem,
GoogleFontsParams,
GoogleFontsResponse,
} from './googleFonts';
} from './google/googleFonts';
export {
fetchAllFontshareFonts,
fetchFontshareFontBySlug,
fetchFontshareFonts,
} from './fontshare';
} from './fontshare/fontshare';
export type {
FontshareParams,
FontshareResponse,
} from './fontshare';
} from './fontshare/fontshare';
export {
normalizeFontshareFont,
normalizeFontshareFonts,
normalizeGoogleFont,
normalizeGoogleFonts,
} from './normalize';
} from './normalize/normalize';
export type {
FontFeatures,
FontMetadata,
FontStyleUrls,
UnifiedFont,
UnifiedFontVariant,
} from './normalize';
} from './normalize/normalize';

View File

@@ -1,17 +1,17 @@
import type { FontshareFont } from '$entities/Font';
import type { GoogleFontItem } from '$entities/Font/api/googleFonts';
import type { UnifiedFont } from '$entities/Font/api/normalize';
import {
normalizeFontshareFont,
normalizeFontshareFonts,
normalizeGoogleFont,
normalizeGoogleFonts,
} from '$entities/Font/api/normalize';
import {
describe,
expect,
it,
} from 'vitest';
import type { GoogleFontItem } from '../google/googleFonts';
import type { UnifiedFont } from './normalize';
import {
normalizeFontshareFont,
normalizeFontshareFonts,
normalizeGoogleFont,
normalizeGoogleFonts,
} from './normalize';
describe('Font Normalization', () => {
describe('normalizeGoogleFont', () => {

View File

@@ -8,10 +8,7 @@
*/
import type { UnifiedFont } from '$entities/Font/api/normalize';
import {
type CollectionCacheManager,
createCollectionCache,
} from '$shared/fetch/collectionCache';
import { createCollectionCache } from '$shared/fetch/collectionCache';
import type {
Readable,
Writable,