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.