From hubspot-pack
Provides layered TypeScript architecture for HubSpot CRM integrations with typed clients, services, caching, webhooks, API routes, and project structure.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packThis skill is limited to using the following tools:
Production-ready layered architecture for HubSpot CRM integrations with typed clients, service abstraction, caching, and webhook handling.
Guides HubSpot CRM integration architectures: embedded client for MVPs (<50K API calls/day), service layer with queues/cache for growth (50K-300K), and gateway patterns for scale.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js/Python SDKs.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Share bugs, ideas, or general feedback.
Production-ready layered architecture for HubSpot CRM integrations with typed clients, service abstraction, caching, and webhook handling.
@hubspot/api-client v13+ installedmy-hubspot-integration/
├── src/
│ ├── hubspot/ # HubSpot infrastructure layer
│ │ ├── client.ts # Singleton @hubspot/api-client wrapper
│ │ ├── types.ts # HubSpot-specific types
│ │ ├── errors.ts # Error classification
│ │ ├── cache.ts # Response caching
│ │ └── associations.ts # Association type constants
│ ├── services/ # Business logic layer
│ │ ├── contact.service.ts # Contact CRUD + business rules
│ │ ├── deal.service.ts # Deal pipeline operations
│ │ ├── company.service.ts # Company management
│ │ └── sync.service.ts # Data synchronization
│ ├── api/ # API layer
│ │ ├── routes/
│ │ │ ├── contacts.ts # REST endpoints
│ │ │ ├── deals.ts
│ │ │ └── webhooks.ts # Webhook receiver
│ │ └── middleware/
│ │ ├── auth.ts # Request auth
│ │ └── webhook-verify.ts # HubSpot signature verification
│ ├── jobs/ # Background jobs
│ │ ├── sync-contacts.ts # Scheduled sync
│ │ └── process-webhooks.ts # Async event processing
│ └── index.ts
├── tests/
│ ├── unit/
│ │ ├── services/
│ │ └── mocks/hubspot.ts # Shared mock factory
│ └── integration/
│ └── hubspot.integration.test.ts
├── config/
│ ├── default.ts # Shared config
│ └── production.ts # Production overrides
└── package.json
// src/hubspot/client.ts
import * as hubspot from '@hubspot/api-client';
let instance: hubspot.Client | null = null;
export function getHubSpotClient(): hubspot.Client {
if (!instance) {
instance = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
}
return instance;
}
// src/hubspot/associations.ts
// Default association type IDs (HUBSPOT_DEFINED category)
export const ASSOCIATION_TYPES = {
CONTACT_TO_COMPANY: 1,
CONTACT_TO_DEAL: 3,
COMPANY_TO_DEAL: 5,
CONTACT_TO_TICKET: 16,
NOTE_TO_CONTACT: 202,
TASK_TO_CONTACT: 204,
NOTE_TO_DEAL: 214,
} as const;
// src/hubspot/errors.ts
export class HubSpotError 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 = 'HubSpotError';
}
}
export function wrapError(error: any): HubSpotError {
const status = error?.code || error?.statusCode || 500;
const body = error?.body || {};
return new HubSpotError(
body.message || error.message,
status,
body.category || 'UNKNOWN',
body.correlationId || '',
[429, 500, 502, 503, 504].includes(status)
);
}
// src/services/contact.service.ts
import type { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts';
import { getHubSpotClient } from '../hubspot/client';
import { ASSOCIATION_TYPES } from '../hubspot/associations';
import { wrapError } from '../hubspot/errors';
const CONTACT_PROPS = ['firstname', 'lastname', 'email', 'phone', 'lifecyclestage', 'company'];
export class ContactService {
private client = getHubSpotClient();
async findByEmail(email: string): Promise<SimplePublicObject | null> {
try {
const result = await this.client.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;
} catch (error) {
throw wrapError(error);
}
}
async upsert(email: string, properties: Record<string, string>): Promise<SimplePublicObject> {
const existing = await this.findByEmail(email);
if (existing) {
return this.client.crm.contacts.basicApi.update(existing.id, { properties });
}
return this.client.crm.contacts.basicApi.create({
properties: { email, ...properties },
associations: [],
});
}
async associateWithCompany(contactId: string, companyId: string): Promise<void> {
await this.client.crm.associations.v4.basicApi.create(
'contacts', contactId, 'companies', companyId,
[{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: ASSOCIATION_TYPES.CONTACT_TO_COMPANY }]
);
}
}
// src/services/deal.service.ts
export class DealService {
private client = getHubSpotClient();
private pipelineCache: any[] | null = null;
async getPipelines() {
if (!this.pipelineCache) {
const result = await this.client.crm.pipelines.pipelinesApi.getAll('deals');
this.pipelineCache = result.results;
}
return this.pipelineCache;
}
async createInPipeline(
dealName: string,
amount: number,
pipelineName: string,
stageName: string,
associations: { contactId?: string; companyId?: string }
) {
const pipelines = await this.getPipelines();
const pipeline = pipelines.find(p => p.label === pipelineName) || pipelines[0];
const stage = pipeline.stages.find((s: any) => s.label === stageName) || pipeline.stages[0];
const assocArray = [];
if (associations.contactId) {
assocArray.push({
to: { id: associations.contactId },
types: [{ associationCategory: 'HUBSPOT_DEFINED' as const, associationTypeId: 3 }],
});
}
if (associations.companyId) {
assocArray.push({
to: { id: associations.companyId },
types: [{ associationCategory: 'HUBSPOT_DEFINED' as const, associationTypeId: 5 }],
});
}
return this.client.crm.deals.basicApi.create({
properties: {
dealname: dealName,
amount: String(amount),
pipeline: pipeline.id,
dealstage: stage.id,
},
associations: assocArray,
});
}
}
User Request → API Routes → Service Layer → HubSpot Client → HubSpot API
↕ ↕
Business Rules Response Cache
↕
Background Jobs → Webhook Events
// config/default.ts
export const config = {
hubspot: {
retries: 3,
cache: {
contactTtlMs: 5 * 60 * 1000, // 5 minutes
pipelineTtlMs: 60 * 60 * 1000, // 1 hour
propertyTtlMs: 60 * 60 * 1000, // 1 hour
},
batch: {
maxSize: 100,
concurrency: 5,
},
},
};
// config/production.ts
export const productionConfig = {
hubspot: {
retries: 5,
cache: {
contactTtlMs: 2 * 60 * 1000, // shorter in prod
},
},
};
| Issue | Cause | Solution |
|---|---|---|
| Circular dependencies | Wrong layering | Services import hubspot/, never the reverse |
| Type errors | Missing SDK type imports | Import from @hubspot/api-client/lib/codegen/crm/ |
| Test isolation | Shared client state | Use resetHubSpotClient() in test teardown |
| Cache invalidation | Stale data | Invalidate on webhook events |
For multi-environment setup, see hubspot-multi-env-setup.