Claude-code-plugins-plus-skills hubspot-sdk-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/hubspot-pack/skills/hubspot-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-hubspot-sdk-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/hubspot-pack/skills/hubspot-sdk-patterns/SKILL.mdsource content
HubSpot SDK Patterns
Overview
Production-ready patterns for the
@hubspot/api-client SDK covering typed wrappers, error handling, batch operations, and pagination.
Prerequisites
v13+ installed@hubspot/api-client- TypeScript 5+ with strict mode
- Understanding of HubSpot CRM object model
Instructions
Step 1: Typed Client Wrapper
// 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; }
Step 2: Error Classification
// 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); } }
Step 3: Typed CRM Operations
// 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; }
Step 4: Batch Operations
// 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; }
Step 5: Pagination Helper
// 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`); }
Output
- Type-safe client singleton with automatic retries
- Error classification with retryable detection
- Typed CRUD operations for contacts
- Batch create/read/upsert with chunking
- Async generator for paginated results
Error Handling
| Pattern | Use Case | Benefit |
|---|---|---|
wrapper | All API calls | Classifies errors, adds correlation IDs |
| 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 |
Examples
Factory Pattern (Multi-Portal)
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)!; }
Resources
Next Steps
Apply patterns in
hubspot-core-workflow-a for real-world CRM operations.