From hubspot-pack
Sets up local dev workflow for HubSpot integrations: project structure, TS client singleton, Vitest tests, API mocks, and sandbox accounts.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packThis skill is limited to using the following tools:
Set up a fast local development workflow for HubSpot integrations with sandbox accounts, mocking, and test utilities.
Installs HubSpot API client SDK and configures authentication via private app tokens or OAuth 2.0 for Node.js and Python projects.
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.
Set up a fast local development workflow for HubSpot integrations with sandbox accounts, mocking, and test utilities.
hubspot-install-auth setupmy-hubspot-project/
├── src/
│ ├── hubspot/
│ │ ├── client.ts # Singleton @hubspot/api-client wrapper
│ │ ├── contacts.ts # Contact operations
│ │ ├── deals.ts # Deal operations
│ │ └── types.ts # HubSpot type definitions
│ └── index.ts
├── tests/
│ ├── mocks/
│ │ └── hubspot.ts # Shared mock factory
│ ├── contacts.test.ts
│ └── deals.test.ts
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
├── tsconfig.json
└── 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) {
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required');
}
instance = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN,
numberOfApiCallRetries: 3,
});
}
return instance;
}
// Reset client (useful for tests)
export function resetHubSpotClient(): void {
instance = null;
}
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:watch": "vitest --watch",
"test:integration": "HUBSPOT_TEST=true vitest --config vitest.integration.config.ts"
},
"devDependencies": {
"@hubspot/api-client": "^13.0.0",
"vitest": "^2.0.0",
"tsx": "^4.0.0"
}
}
// tests/mocks/hubspot.ts
import { vi } from 'vitest';
export function createMockHubSpotClient() {
return {
crm: {
contacts: {
basicApi: {
create: vi.fn().mockResolvedValue({
id: '101',
properties: { firstname: 'Jane', lastname: 'Doe', email: 'jane@test.com' },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
archived: false,
}),
getById: vi.fn().mockResolvedValue({
id: '101',
properties: { firstname: 'Jane', lastname: 'Doe', email: 'jane@test.com' },
}),
getPage: vi.fn().mockResolvedValue({
results: [],
paging: undefined,
}),
update: vi.fn().mockResolvedValue({ id: '101', properties: {} }),
archive: vi.fn().mockResolvedValue(undefined),
},
searchApi: {
doSearch: vi.fn().mockResolvedValue({ total: 0, results: [] }),
},
batchApi: {
create: vi.fn().mockResolvedValue({ status: 'COMPLETE', results: [] }),
read: vi.fn().mockResolvedValue({ status: 'COMPLETE', results: [] }),
update: vi.fn().mockResolvedValue({ status: 'COMPLETE', results: [] }),
},
},
deals: {
basicApi: {
create: vi.fn().mockResolvedValue({
id: '201',
properties: { dealname: 'Test Deal', amount: '1000' },
}),
getById: vi.fn(),
update: vi.fn(),
},
},
companies: {
basicApi: {
create: vi.fn().mockResolvedValue({
id: '301',
properties: { name: 'Test Co', domain: 'test.com' },
}),
},
},
},
};
}
// tests/contacts.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createMockHubSpotClient } from './mocks/hubspot';
vi.mock('../src/hubspot/client', () => ({
getHubSpotClient: vi.fn(),
}));
describe('Contact operations', () => {
const mockClient = createMockHubSpotClient();
beforeEach(() => {
vi.mocked(getHubSpotClient).mockReturnValue(mockClient as any);
});
it('should create a contact with required properties', async () => {
const result = await mockClient.crm.contacts.basicApi.create({
properties: { firstname: 'Jane', lastname: 'Doe', email: 'jane@test.com' },
associations: [],
});
expect(result.id).toBe('101');
expect(result.properties.email).toBe('jane@test.com');
});
});
# Create a free developer test account at:
# https://developers.hubspot.com/get-started
# Use test account token for integration tests
# .env.local
HUBSPOT_ACCESS_TOKEN=pat-na1-test-xxxx # test account token
HUBSPOT_PORTAL_ID=12345678 # test portal ID
tsx watch| Error | Cause | Solution |
|---|---|---|
HUBSPOT_ACCESS_TOKEN is required | Missing env var | Copy .env.example to .env.local |
| Mock type mismatch | SDK version change | Update mock factory to match SDK types |
| Test timeout | Real API call leaked | Verify mocks are wired correctly |
429 in integration tests | Rate limited on test account | Add delay between test runs |
// tests/integration/contacts.integration.test.ts
import { describe, it, expect } from 'vitest';
import * as hubspot from '@hubspot/api-client';
const shouldRun = process.env.HUBSPOT_TEST === 'true';
describe.skipIf(!shouldRun)('HubSpot Integration', () => {
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
it('should create and archive a test contact', async () => {
const contact = await client.crm.contacts.basicApi.create({
properties: {
firstname: 'Integration',
lastname: `Test-${Date.now()}`,
email: `test-${Date.now()}@example.com`,
},
associations: [],
});
expect(contact.id).toBeDefined();
// Clean up
await client.crm.contacts.basicApi.archive(contact.id);
});
});
See hubspot-sdk-patterns for production-ready code patterns.