From clickup-pack
Provides TypeScript patterns for ClickUp API v2 REST clients with typed responses, error handling, rate limiting, and multi-tenant support.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin clickup-packThis skill is limited to using the following tools:
ClickUp has no official SDK. Build a typed REST client wrapper around `https://api.clickup.com/api/v2/`. These patterns provide singleton clients, typed responses, error boundaries, and multi-tenant support.
Provides production architecture for ClickUp API v2 integrations with layered design, custom fields, time tracking, goals, and two-way sync patterns.
Interacts with ClickUp REST API to manage tasks, spaces, lists, assignees. Handles pagination, subtasks for reporting, automation, and workflow queries.
Manages ClickUp tasks, sprints, comments, goals, and workflows via cup CLI for queries, status updates, subtasks, time tracking, bulk operations, and saved filters.
Share bugs, ideas, or general feedback.
ClickUp has no official SDK. Build a typed REST client wrapper around https://api.clickup.com/api/v2/. These patterns provide singleton clients, typed responses, error boundaries, and multi-tenant support.
// src/clickup/client.ts
const CLICKUP_BASE = 'https://api.clickup.com/api/v2';
interface ClickUpClientConfig {
token: string;
timeout?: number;
onRateLimit?: (waitMs: number) => void;
}
class ClickUpClient {
private token: string;
private timeout: number;
private rateLimitRemaining = 100;
private rateLimitReset = 0;
constructor(config: ClickUpClientConfig) {
this.token = config.token;
this.timeout = config.timeout ?? 30000;
}
async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(`${CLICKUP_BASE}${path}`, {
...options,
signal: controller.signal,
headers: {
'Authorization': this.token,
'Content-Type': 'application/json',
...options.headers,
},
});
// Track rate limit state from response headers
this.rateLimitRemaining = parseInt(
response.headers.get('X-RateLimit-Remaining') ?? '100'
);
this.rateLimitReset = parseInt(
response.headers.get('X-RateLimit-Reset') ?? '0'
) * 1000;
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new ClickUpApiError(response.status, body.err, body.ECODE);
}
return response.json();
} finally {
clearTimeout(timer);
}
}
// Convenience methods
async getUser(): Promise<ClickUpUser> {
const data = await this.request<{ user: ClickUpUser }>('/user');
return data.user;
}
async getTeams(): Promise<ClickUpTeam[]> {
const data = await this.request<{ teams: ClickUpTeam[] }>('/team');
return data.teams;
}
async getSpaces(teamId: string): Promise<ClickUpSpace[]> {
const data = await this.request<{ spaces: ClickUpSpace[] }>(
`/team/${teamId}/space?archived=false`
);
return data.spaces;
}
async createTask(listId: string, task: CreateTaskInput): Promise<ClickUpTask> {
return this.request<ClickUpTask>(`/list/${listId}/task`, {
method: 'POST',
body: JSON.stringify(task),
});
}
async getTask(taskId: string): Promise<ClickUpTask> {
return this.request<ClickUpTask>(`/task/${taskId}`);
}
async updateTask(taskId: string, updates: Partial<CreateTaskInput>): Promise<ClickUpTask> {
return this.request<ClickUpTask>(`/task/${taskId}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
}
isRateLimited(): boolean {
return this.rateLimitRemaining < 5 && Date.now() < this.rateLimitReset;
}
}
// src/clickup/types.ts
interface ClickUpUser {
id: number;
username: string;
email: string;
color: string;
profilePicture: string | null;
}
interface ClickUpTeam {
id: string;
name: string;
color: string;
members: Array<{ user: ClickUpUser; role: number }>;
}
interface ClickUpSpace {
id: string;
name: string;
private: boolean;
statuses: Array<{ status: string; color: string; type: string }>;
features: Record<string, { enabled: boolean }>;
}
interface ClickUpTask {
id: string;
custom_id: string | null;
name: string;
description: string;
status: { status: string; color: string; type: string };
priority: { id: string; priority: string; color: string } | null;
date_created: string;
date_updated: string;
due_date: string | null;
assignees: ClickUpUser[];
tags: Array<{ name: string }>;
url: string;
list: { id: string; name: string };
folder: { id: string; name: string };
space: { id: string };
custom_fields: ClickUpCustomFieldValue[];
}
interface CreateTaskInput {
name: string;
description?: string;
markdown_description?: string;
assignees?: number[];
priority?: 1 | 2 | 3 | 4 | null;
status?: string;
due_date?: number;
due_date_time?: boolean;
parent?: string;
tags?: string[];
custom_fields?: Array<{ id: string; value: any }>;
}
interface ClickUpCustomFieldValue {
id: string;
name: string;
type: string;
value: any;
}
class ClickUpApiError extends Error {
constructor(
public readonly status: number,
public readonly err: string,
public readonly ecode?: string,
) {
super(`ClickUp API ${status}: ${err}${ecode ? ` (${ecode})` : ''}`);
}
get isRateLimited(): boolean { return this.status === 429; }
get isAuthError(): boolean { return this.status === 401; }
get isNotFound(): boolean { return this.status === 404; }
get isRetryable(): boolean { return this.status === 429 || this.status >= 500; }
}
// src/clickup/index.ts
let defaultClient: ClickUpClient | null = null;
export function getClickUpClient(): ClickUpClient {
if (!defaultClient) {
const token = process.env.CLICKUP_API_TOKEN;
if (!token) throw new Error('CLICKUP_API_TOKEN not set');
defaultClient = new ClickUpClient({ token });
}
return defaultClient;
}
const tenantClients = new Map<string, ClickUpClient>();
function getClientForTenant(tenantId: string, token: string): ClickUpClient {
if (!tenantClients.has(tenantId)) {
tenantClients.set(tenantId, new ClickUpClient({ token }));
}
return tenantClients.get(tenantId)!;
}
import { z } from 'zod';
const TaskSchema = z.object({
id: z.string(),
name: z.string(),
status: z.object({ status: z.string(), color: z.string() }),
priority: z.object({ priority: z.string() }).nullable(),
url: z.string().url(),
});
async function getValidatedTask(taskId: string) {
const raw = await getClickUpClient().getTask(taskId);
return TaskSchema.parse(raw);
}
| Pattern | Use Case | Benefit |
|---|---|---|
| Typed error class | All API calls | Type-safe error discrimination |
| Singleton | Single-tenant apps | Shared rate limit tracking |
| Factory | Multi-tenant SaaS | Per-tenant isolation |
| Zod validation | Response parsing | Catches API contract changes |
Apply patterns in clickup-core-workflow-a for task management.