Algolia SDK Patterns
Overview
Production-ready patterns for algoliasearch v5. Key architectural change from v4: all methods live on the client directly — no more client.initIndex(). Index name is passed as a parameter to every call.
Prerequisites
algoliasearchv5+ installed- Completed
algolia-install-authsetup - TypeScript project (patterns work in JS too, you just lose type safety)
Instructions
Pattern 1: Typed Singleton Client
// src/algolia/client.ts
import { algoliasearch, type Algoliasearch } from 'algoliasearch';
let _client: Algoliasearch | null = null;
export function getClient(): Algoliasearch {
if (!_client) {
const appId = process.env.ALGOLIA_APP_ID;
const apiKey = process.env.ALGOLIA_ADMIN_KEY;
if (!appId || !apiKey) {
throw new Error(
'ALGOLIA_APP_ID and ALGOLIA_ADMIN_KEY must be set. '
+ 'Get them from dashboard.algolia.com > Settings > API Keys'
);
}
_client = algoliasearch(appId, apiKey);
}
return _client;
}
// For testing: reset singleton
export function resetClient(): void {
_client = null;
}
Pattern 2: Typed Search Results
// src/algolia/types.ts
// Define your record shape — extends Algolia's Hit type
interface Product {
objectID: string;
name: string;
category: string;
price: number;
description: string;
image_url: string;
}
// src/algolia/search.ts
import { getClient } from './client';
export async function searchProducts(
query: string,
options?: {
filters?: string;
facetFilters?: string[][];
hitsPerPage?: number;
page?: number;
}
) {
const client = getClient();
const { hits, nbHits, nbPages, page } = await client.searchSingleIndex<Product>({
indexName: 'products',
searchParams: {
query,
filters: options?.filters,
facetFilters: options?.facetFilters,
hitsPerPage: options?.hitsPerPage ?? 20,
page: options?.page ?? 0,
attributesToRetrieve: ['name', 'category', 'price', 'image_url'],
attributesToHighlight: ['name', 'description'],
},
});
return { hits, totalHits: nbHits, totalPages: nbPages, currentPage: page };
}
// Usage: const { hits } = await searchProducts('laptop', { filters: 'price < 1000' });
Pattern 3: Error Handling with Algolia Error Types
// src/algolia/errors.ts
import { ApiError } from 'algoliasearch';
export async function safeAlgoliaCall<T>(
operation: string,
fn: () => Promise<T>
): Promise<{ data: T | null; error: string | null }> {
try {
const data = await fn();
return { data, error: null };
} catch (err) {
if (err instanceof ApiError) {
// ApiError has status and message from Algolia API
const msg = `Algolia ${operation} failed [${err.status}]: ${err.message}`;
console.error(msg);
// Specific handling for common codes
if (err.status === 429) {
console.warn('Rate limited — reduce request frequency or contact Algolia');
} else if (err.status === 404) {
console.warn('Index or object not found — verify index name');
}
return { data: null, error: msg };
}
// Non-Algolia error (network, etc.)
const msg = err instanceof Error ? err.message : 'Unknown error';
console.error(`${operation} error: ${msg}`);
return { data: null, error: msg };
}
}
// Usage:
// const { data, error } = await safeAlgoliaCall('search', () =>
// client.searchSingleIndex({ indexName: 'products', searchParams: { query: 'foo' } })
// );
Pattern 4: Batch Operations
// src/algolia/batch.ts
import { getClient } from './client';
// saveObjects handles batching internally — send up to 1000 objects per call
export async function bulkIndex(indexName: string, records: Record<string, any>[]) {
const client = getClient();
const BATCH_SIZE = 1000;
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
const { taskID } = await client.saveObjects({
indexName,
objects: batch,
});
await client.waitForTask({ indexName, taskID });
console.log(`Indexed ${Math.min(i + BATCH_SIZE, records.length)}/${records.length}`);
}
}
// Partial update — only send changed fields
export async function updateFields(
indexName: string,
objectID: string,
fields: Record<string, any>
) {
const client = getClient();
return client.partialUpdateObject({
indexName,
objectID,
attributesToUpdate: fields,
});
}
Pattern 5: Multi-Tenant Client Factory
// src/algolia/multi-tenant.ts
import { algoliasearch, type Algoliasearch } from 'algoliasearch';
const tenantClients = new Map<string, Algoliasearch>();
export function getClientForTenant(tenantId: string): Algoliasearch {
if (!tenantClients.has(tenantId)) {
// Each tenant might have their own Algolia app, or use index prefixes
const appId = process.env[`ALGOLIA_APP_ID_${tenantId.toUpperCase()}`]
|| process.env.ALGOLIA_APP_ID!;
const apiKey = process.env[`ALGOLIA_ADMIN_KEY_${tenantId.toUpperCase()}`]
|| process.env.ALGOLIA_ADMIN_KEY!;
tenantClients.set(tenantId, algoliasearch(appId, apiKey));
}
return tenantClients.get(tenantId)!;
}
// Or use a single app with index prefixing
export function tenantIndex(tenantId: string, base: string): string {
return `${tenantId}_${base}`; // "acme_products"
}
Error Handling
| Pattern | Use Case | Benefit |
|---------|----------|---------|
| safeAlgoliaCall wrapper | All API calls | Prevents uncaught exceptions, structured error info |
| ApiError check | Distinguishing API vs network errors | Targeted retry/recovery logic |
| waitForTask | After every write operation | Ensures reads see latest data |
| Batch chunking | Large datasets | Avoids record-too-big and timeout errors |
Resources
Next Steps
Apply patterns in algolia-core-workflow-a for search implementation.