From hubspot-pack
Apply production-ready @hubspot/api-client SDK patterns for TypeScript. Use when implementing HubSpot integrations, building typed wrappers, or establishing team standards for HubSpot CRM operations. Trigger with phrases like "hubspot SDK patterns", "hubspot best practices", "hubspot typed client", "hubspot api-client wrapper", "idiomatic hubspot".
npx claudepluginhub flight505/skill-forge --plugin hubspot-packThis skill is limited to using the following tools:
Production-ready patterns for the `@hubspot/api-client` SDK covering typed wrappers, error handling, batch operations, and pagination.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Production-ready patterns for the @hubspot/api-client SDK covering typed wrappers, error handling, batch operations, and pagination.
@hubspot/api-client v13+ installed// src/hubspot/client.ts
import * as hubspot from '@hubspot/api-client';
import type {
SimplePublicObjectInputForCreate,
SimplePublicObject,
PublicObjectSearchRequest,
} from '@hubspot/api-client/lib/codegen/crm/contacts';
interface HubSpotConfig {
accessToken: string;
retries?: number;
}
let instance: hubspot.Client | null = null;
export function getClient(config?: HubSpotConfig): hubspot.Client {
if (!instance) {
instance = new hubspot.Client({
accessToken: config?.accessToken || process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: config?.retries ?? 3,
});
}
return instance;
}
// src/hubspot/errors.ts
export class HubSpotApiError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly category: string,
public readonly correlationId: string,
public readonly retryable: boolean
) {
super(message);
this.name = 'HubSpotApiError';
}
}
export function classifyError(error: any): HubSpotApiError {
const status = error?.code || error?.statusCode || error?.response?.status || 500;
const body = error?.body || error?.response?.body || {};
const correlationId = body.correlationId || 'unknown';
const retryable = [429, 500, 502, 503, 504].includes(status);
const categoryMap: Record<number, string> = {
400: 'VALIDATION_ERROR',
401: 'AUTHENTICATION_ERROR',
403: 'AUTHORIZATION_ERROR',
404: 'NOT_FOUND',
409: 'CONFLICT',
429: 'RATE_LIMIT',
500: 'INTERNAL_ERROR',
};
return new HubSpotApiError(
body.message || error.message || 'Unknown HubSpot error',
status,
categoryMap[status] || 'UNKNOWN',
correlationId,
retryable
);
}
// Usage wrapper
export async function safeCall<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch (error) {
throw classifyError(error);
}
}
// src/hubspot/contacts.ts
import * as hubspot from '@hubspot/api-client';
import type { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts';
import { getClient } from './client';
import { safeCall } from './errors';
// Define your contact properties as a type
interface ContactProperties {
firstname?: string;
lastname?: string;
email: string;
phone?: string;
company?: string;
lifecyclestage?: 'subscriber' | 'lead' | 'marketingqualifiedlead'
| 'salesqualifiedlead' | 'opportunity' | 'customer' | 'evangelist';
}
const CONTACT_PROPS = ['firstname', 'lastname', 'email', 'phone', 'company', 'lifecyclestage'];
export async function createContact(props: ContactProperties): Promise<SimplePublicObject> {
return safeCall(() =>
getClient().crm.contacts.basicApi.create({
properties: props as Record<string, string>,
associations: [],
})
);
}
export async function getContact(id: string): Promise<SimplePublicObject> {
return safeCall(() =>
getClient().crm.contacts.basicApi.getById(id, CONTACT_PROPS)
);
}
export async function updateContact(
id: string,
props: Partial<ContactProperties>
): Promise<SimplePublicObject> {
return safeCall(() =>
getClient().crm.contacts.basicApi.update(id, {
properties: props as Record<string, string>,
})
);
}
export async function findContactByEmail(email: string): Promise<SimplePublicObject | null> {
const result = await safeCall(() =>
getClient().crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: CONTACT_PROPS,
limit: 1,
after: 0,
sorts: [],
})
);
return result.results[0] || null;
}
// src/hubspot/batch.ts
import { getClient } from './client';
import { safeCall } from './errors';
// Batch create contacts (max 100 per batch)
export async function batchCreateContacts(
contacts: Array<{ properties: Record<string, string> }>
) {
// POST /crm/v3/objects/contacts/batch/create
return safeCall(() =>
getClient().crm.contacts.batchApi.create({
inputs: contacts.map(c => ({ properties: c.properties, associations: [] })),
})
);
}
// Batch read contacts by ID or unique property
export async function batchReadContacts(ids: string[], properties: string[]) {
// POST /crm/v3/objects/contacts/batch/read
return safeCall(() =>
getClient().crm.contacts.batchApi.read({
inputs: ids.map(id => ({ id })),
properties,
propertiesWithHistory: [],
})
);
}
// Batch upsert: create or update in one call
export async function batchUpsertContacts(
contacts: Array<{ properties: Record<string, string> }>,
idProperty = 'email'
) {
// POST /crm/v3/objects/contacts/batch/upsert
const client = getClient();
return safeCall(() =>
client.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/batch/upsert',
body: {
inputs: contacts.map(c => ({
properties: c.properties,
idProperty,
id: c.properties[idProperty],
})),
},
})
);
}
// Chunk large batches into 100-record groups
export async function batchCreateChunked(
contacts: Array<{ properties: Record<string, string> }>,
chunkSize = 100
) {
const results = [];
for (let i = 0; i < contacts.length; i += chunkSize) {
const chunk = contacts.slice(i, i + chunkSize);
const result = await batchCreateContacts(chunk);
results.push(result);
}
return results;
}
// src/hubspot/pagination.ts
import type { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts';
import { getClient } from './client';
export async function* getAllContacts(
properties: string[],
limit = 100
): AsyncGenerator<SimplePublicObject> {
let after: string | undefined;
do {
const page = await getClient().crm.contacts.basicApi.getPage(
limit,
after,
properties
);
for (const contact of page.results) {
yield contact;
}
after = page.paging?.next?.after;
} while (after);
}
// Usage
async function exportAllContacts() {
const allContacts: SimplePublicObject[] = [];
for await (const contact of getAllContacts(['firstname', 'lastname', 'email'])) {
allContacts.push(contact);
}
console.log(`Exported ${allContacts.length} contacts`);
}
| Pattern | Use Case | Benefit |
|---|---|---|
safeCall wrapper | All API calls | Classifies errors, adds correlation IDs |
numberOfApiCallRetries | Transient failures | Built-in SDK retry for 429/5xx |
| Batch chunking | Large datasets | Stays within 100-record batch limit |
| Pagination generator | Full exports | Memory-efficient streaming |
const clients = new Map<string, hubspot.Client>();
export function getClientForPortal(portalId: string, token: string): hubspot.Client {
if (!clients.has(portalId)) {
clients.set(portalId, new hubspot.Client({ accessToken: token }));
}
return clients.get(portalId)!;
}
Apply patterns in hubspot-core-workflow-a for real-world CRM operations.