Agent skill

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".

Stars 1,803
Forks 241

Install this agent skill to your Project

npx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/main/plugins/saas-packs/clickup-pack/skills/clickup-local-dev-loop

SKILL.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

bash
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

bash
# .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
json
{
  "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

typescript
// 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

typescript
// 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)

typescript
// 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.

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results