> clickup-local-dev-loop
Set up local development for ClickUp API integrations with testing, mocking, and hot reload. Trigger: "clickup dev setup", "clickup local development", "clickup dev environment", "develop with clickup", "clickup testing setup", "mock clickup API".
curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/clickup-local-dev-loop?format=md"ClickUp Local Dev Loop
Overview
Set up a fast local development workflow for ClickUp API v2 integrations with hot reload, mocking, and integration testing.
Project Setup
mkdir my-clickup-integration && cd $_
npm init -y
npm install -D typescript tsx vitest @types/node dotenv
npx tsc --init --target ES2022 --module nodenext --outDir dist
my-clickup-integration/
├── src/
│ ├── clickup/
│ │ ├── client.ts # ClickUp API client (see clickup-sdk-patterns)
│ │ ├── types.ts # TypeScript interfaces
│ │ └── tasks.ts # Task operations
│ └── index.ts
├── tests/
│ ├── mocks/
│ │ └── clickup.ts # Mock ClickUp API responses
│ ├── unit/
│ │ └── tasks.test.ts
│ └── integration/
│ └── clickup.test.ts
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
└── package.json
Environment Configuration
# .env.example (commit this)
CLICKUP_API_TOKEN=pk_your_token_here
CLICKUP_TEAM_ID=your_team_id
CLICKUP_TEST_LIST_ID=your_test_list_id
# .env.local (git-ignored, copy from .env.example)
cp .env.example .env.local
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:watch": "vitest --watch",
"test:integration": "CLICKUP_LIVE=1 vitest --run tests/integration/",
"build": "tsc",
"typecheck": "tsc --noEmit"
}
}
Mock ClickUp API for Unit Tests
// tests/mocks/clickup.ts
export const mockTask = {
id: 'test_task_001',
name: 'Test Task',
status: { status: 'to do', color: '#d3d3d3', type: 'open' },
priority: { id: '3', priority: 'normal', color: '#6fddff' },
date_created: '1695000000000',
date_updated: '1695000000000',
due_date: null,
assignees: [],
tags: [],
url: 'https://app.clickup.com/t/test_task_001',
list: { id: '900100200300', name: 'Test List' },
folder: { id: '456', name: 'Test Folder' },
space: { id: '789' },
custom_fields: [],
};
export const mockTeam = {
teams: [{
id: '1234567',
name: 'Test Workspace',
members: [{ user: { id: 183, username: 'testuser', email: 'test@example.com' } }],
}],
};
// Mock fetch for ClickUp API calls
export function mockClickUpFetch() {
return vi.fn(async (url: string, options?: RequestInit) => {
const path = new URL(url).pathname.replace('/api/v2', '');
const routes: Record<string, any> = {
'/team': mockTeam,
'/user': { user: { id: 183, username: 'testuser' } },
};
// Match dynamic routes
if (path.match(/^\/task\/.+/)) return jsonResponse(mockTask);
if (path.match(/^\/list\/.+\/task$/) && options?.method === 'POST') {
return jsonResponse({ ...mockTask, ...JSON.parse(options.body as string) });
}
return jsonResponse(routes[path] ?? {}, routes[path] ? 200 : 404);
});
}
function jsonResponse(data: any, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Remaining': '95',
'X-RateLimit-Limit': '100',
'X-RateLimit-Reset': String(Math.floor(Date.now() / 1000) + 60),
},
});
}
Unit Test Example
// tests/unit/tasks.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockClickUpFetch, mockTask } from '../mocks/clickup';
describe('ClickUp Task Operations', () => {
beforeEach(() => {
vi.stubGlobal('fetch', mockClickUpFetch());
vi.stubEnv('CLICKUP_API_TOKEN', 'pk_test_token');
});
it('creates a task with required fields', async () => {
const { createTask } = await import('../../src/clickup/tasks');
const task = await createTask('list123', { name: 'New Task' });
expect(task.name).toBe('New Task');
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/list/list123/task'),
expect.objectContaining({ method: 'POST' }),
);
});
it('gets a task by ID', async () => {
const { getTask } = await import('../../src/clickup/tasks');
const task = await getTask('test_task_001');
expect(task.id).toBe(mockTask.id);
});
});
Integration Test (Live API)
// tests/integration/clickup.test.ts
import { describe, it, expect } from 'vitest';
import 'dotenv/config';
const LIVE = process.env.CLICKUP_LIVE === '1';
describe.skipIf(!LIVE)('ClickUp Live API', () => {
it('authenticates and lists workspaces', async () => {
const response = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': process.env.CLICKUP_API_TOKEN! },
});
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.teams.length).toBeGreaterThan(0);
});
});
Error Handling
| Error | Cause | Solution |
|---|---|---|
CLICKUP_API_TOKEN undefined | Missing .env.local | Copy from .env.example |
| Integration tests fail | No live token | Set CLICKUP_LIVE=1 and valid token |
| Mock not matching | Route pattern wrong | Check URL path in mock router |
Resources
Next Steps
See clickup-sdk-patterns for production-ready client patterns.
> related_skills --same-repo
> fathom-cost-tuning
Optimize Fathom API usage and plan selection. Trigger with phrases like "fathom cost", "fathom pricing", "fathom plan".
> fathom-core-workflow-b
Sync Fathom meeting data to CRM and build automated follow-up workflows. Use when integrating Fathom with Salesforce, HubSpot, or custom CRMs, or creating automated post-meeting email summaries. Trigger with phrases like "fathom crm sync", "fathom salesforce", "fathom follow-up", "fathom post-meeting workflow".
> fathom-core-workflow-a
Build a meeting analytics pipeline with Fathom transcripts and summaries. Use when extracting insights from meetings, building CRM sync, or creating automated meeting follow-up workflows. Trigger with phrases like "fathom analytics", "fathom meeting pipeline", "fathom transcript analysis", "fathom action items sync".
> fathom-common-errors
Diagnose and fix Fathom API errors including auth failures and missing data. Use when API calls fail, transcripts are empty, or webhooks are not firing. Trigger with phrases like "fathom error", "fathom not working", "fathom api failure", "fix fathom".