Claude-code-plugins-plus hubspot-reference-architecture
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-reference-architecture" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-hubspot-reference-architecture && rm -rf "$T"
manifest:
plugins/saas-packs/hubspot-pack/skills/hubspot-reference-architecture/SKILL.mdsource content
HubSpot Reference Architecture
Overview
Production-ready layered architecture for HubSpot CRM integrations with typed clients, service abstraction, caching, and webhook handling.
Prerequisites
- TypeScript 5+ project
v13+ installed@hubspot/api-client- Understanding of layered architecture patterns
Instructions
Step 1: Project Structure
my-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
Step 2: Infrastructure Layer
// 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) ); }
Step 3: Service Layer
// 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, }); } }
Step 4: Data Flow
User Request → API Routes → Service Layer → HubSpot Client → HubSpot API ↕ ↕ Business Rules Response Cache ↕ Background Jobs → Webhook Events
Step 5: Configuration
// 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 }, }, };
Output
- Layered architecture separating infrastructure, services, and API
- Typed client with error classification
- Association type constants (no magic numbers)
- Service classes with business logic encapsulation
- Configurable caching and retry policies
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Circular dependencies | Wrong layering | Services import hubspot/, never the reverse |
| Type errors | Missing SDK type imports | Import from |
| Test isolation | Shared client state | Use in test teardown |
| Cache invalidation | Stale data | Invalidate on webhook events |
Resources
Next Steps
For multi-environment setup, see
hubspot-multi-env-setup.