From hubspot-pack
Implement a production-ready HubSpot integration architecture with layered design. Use when designing new HubSpot integrations, reviewing project structure, or establishing architecture standards for HubSpot CRM applications. Trigger with phrases like "hubspot architecture", "hubspot project structure", "hubspot integration design", "hubspot best practices layout", "hubspot layers".
npx claudepluginhub flight505/skill-forge --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 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 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.