'Set up local development for ClickUp API integrations with testing,
Scanned 9/2/2026
Install to Claude Code
npx -y skills add jeremylongshore/tons-of-skills-marketplace --skill clickup-local-dev-loop --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Clickup Local Dev Loop?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-clickup-local-dev-loop-tons-of-skills-marketplace)More formats (shields.io, HTML) on the badges page.
---
name: clickup-local-dev-loop
description: '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".
'
allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(pnpm:*), Grep
version: 1.6.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- productivity
- clickup
compatibility: Designed for Claude Code
---
# 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 |
## Prerequisites
- Isolated local/staging environment, mock fixtures, and non-production list
- Secret manager or local secure injection for any live test credential
- Cleanup path and data-retention rule for generated test tasks
## Instructions
Run unit tests against sanitized fixtures by default, enable a live request
only in the isolated integration scope, and mark disposable resources clearly.
Re-read and clean up every live mutation; never use a production workspace or
personal task list as a test fixture.
## Output
Return a local-test receipt with fixture version, mock/live mode, affected test
IDs, assertion results, cleanup status, and safe correlation data. Do not
commit `.env.local`, tokens, real task bodies, or private member information.
## Examples
Mock a `GET /task` response and assert the client mapping, then optionally run
one protected-list read with `CLICKUP_LIVE=1`. If a live test requires a task,
create a labeled disposable task, verify it, and delete it before the run ends.
## Resources
- [Vitest Documentation](https://vitest.dev/)
- [tsx (TypeScript Execute)](https://github.com/privatenumber/tsx)
- [ClickUp API Reference](https://developer.clickup.com/)
## Next Steps
See `clickup-sdk-patterns` for production-ready client patterns.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!