Agent Skills: HubSpot Reference Architecture

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/hubspot-reference-architecture

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/hubspot-pack/skills/hubspot-reference-architecture

Skill Files

Browse the full folder contents for hubspot-reference-architecture.

Download Skill

Loading file tree…

plugins/saas-packs/hubspot-pack/skills/hubspot-reference-architecture/SKILL.md

Skill Metadata

Name
hubspot-reference-architecture
Description
|

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
  • @hubspot/api-client v13+ installed
  • 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 @hubspot/api-client/lib/codegen/crm/ | | Test isolation | Shared client state | Use resetHubSpotClient() in test teardown | | Cache invalidation | Stale data | Invalidate on webhook events |

Resources

Next Steps

For multi-environment setup, see hubspot-multi-env-setup.