HubSpot Local Dev Loop
Overview
Set up a fast local development workflow for HubSpot integrations with sandbox accounts, mocking, and test utilities.
Prerequisites
- Completed
hubspot-install-authsetup - Node.js 18+ with npm/pnpm
- HubSpot developer test account (free at developers.hubspot.com)
Instructions
Step 1: Create Project Structure
my-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
Step 2: Create Client Singleton
// 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;
}
Step 3: Configure Testing with Vitest
{
"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"
}
}
Step 4: Mock HubSpot API for Unit Tests
// 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');
});
});
Step 5: Developer Test Account
# 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
Output
- Client singleton with reset capability for tests
- Mock factory covering contacts, deals, companies
- Unit tests running without API calls
- Integration tests using developer test account
- Hot-reload dev loop with
tsx watch
Error Handling
| 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 |
Examples
Integration Test with Real API
// 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);
});
});
Resources
Next Steps
See hubspot-sdk-patterns for production-ready code patterns.