Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Avoid the most common Figma API integration mistakes and anti-patterns. Use when reviewing Figma code, onboarding new developers, or auditing an existing Figma integration. Trigger with phrases like "figma mistakes", "figma anti-patterns", "figma pitfalls", "figma code review", "figma what not to do".
.claude/skills/jeremylongshore-figma-known-pitfalls/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 112% | 0% |
The ten most common mistakes when integrating with the Figma REST API and Plugin API, with correct alternatives for each.
Problem: GET /v1/files/:key without depth returns the entire document tree. Large files can be 10-100 MB of JSON.
typescript// BAD -- downloads entire file tree const file = await figmaFetch(`/v1/files/${fileKey}`); // GOOD -- only get metadata and page names const file = await figmaFetch(`/v1/files/${fileKey}?depth=1`); // GOOD -- fetch only the nodes you need const nodes = await figmaFetch(`/v1/files/${fileKey}/nodes?ids=${ids}`);
Problem: Blasting requests and crashing on 429 without reading Retry-After.
typescript// BAD -- no rate limit handling for (const id of nodeIds) { await figmaFetch(`/v1/files/${fileKey}/nodes?ids=${id}`); // 429! } // GOOD -- batch IDs and honor Retry-After const ids = nodeIds.join(','); const res = await fetch(`https://api.figma.com/v1/files/${fileKey}/nodes?ids=${ids}`, { headers: { 'X-Figma-Token': token }, }); if (res.status === 429) { const wait = parseInt(res.headers.get('Retry-After') || '60'); await new Promise(r => setTimeout(r, wait * 1000)); }
Problem: Figma image URLs expire after 30 days. Storing them permanently breaks.
typescript// BAD -- storing image URLs in database permanently await db.save({ iconUrl: imageUrl }); // Will break in 30 days // GOOD -- re-export when needed, or cache with short TTL const imageCache = new LRUCache({ max: 1000, ttl: 24 * 60 * 60 * 1000 }); // 24h
Problem: Personal access tokens committed to source code.
typescript// BAD -- token in source code (visible forever in git history) const token = 'figd_actual_token_value_here'; // GOOD -- environment variable const token = process.env.FIGMA_PAT!; if (!token) throw new Error('FIGMA_PAT not set');
files:read ScopeProblem: The files:read scope is deprecated. New tokens should use granular scopes.
BAD: files:read (deprecated, will be removed)
GOOD: file_content:read, file_comments:read, file_versions:read (specific)Problem: Figma returns colors as 0-1 floats, not 0-255 integers.
typescript// BAD -- using Figma values directly as RGB const { r, g, b } = node.fills[0].color; return `rgb(${r}, ${g}, ${b})`; // rgb(0.8, 0.2, 0.4) -- invalid! // GOOD -- convert to 0-255 range return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
Problem: The images endpoint returns null for nodes that cannot be rendered (invisible, deleted, empty).
typescript// BAD -- assumes all nodes render successfully const images = data.images; for (const [id, url] of Object.entries(images)) { const img = await fetch(url); // TypeError: Cannot construct URL from null } // GOOD -- filter out null entries for (const [id, url] of Object.entries(images)) { if (!url) { console.warn(`Node ${id} could not be rendered (null)`); continue; } const img = await fetch(url); }
Problem: Polling GET /v1/files/:key every 30 seconds wastes rate limit quota.
typescript// BAD -- 2,880 API calls per file per day setInterval(async () => { const file = await figmaFetch(`/v1/files/${fileKey}`); if (file.version !== lastVersion) await sync(); }, 30_000); // GOOD -- webhook notifies you only when file changes // POST /v2/webhooks with event_type: "FILE_UPDATE" // Result: ~10-50 calls/day instead of 2,880
Problem: Figma ignores the scale parameter for SVG exports. SVGs always export at 1x.
typescript// BAD -- scale has no effect on SVG await figmaFetch(`/v1/images/${key}?ids=${id}&format=svg&scale=2`); // GOOD -- SVG is vector; scale is meaningless. Use scale for PNG/JPG only. await figmaFetch(`/v1/images/${key}?ids=${id}&format=svg`); // SVG: always 1x await figmaFetch(`/v1/images/${key}?ids=${id}&format=png&scale=2`); // PNG: 2x
Problem: Anyone can POST to your webhook endpoint if you don't verify the passcode.
typescript// BAD -- trusts any incoming request app.post('/webhooks/figma', (req, res) => { processEvent(req.body); // Attacker can send fake events res.sendStatus(200); }); // GOOD -- verify passcode with timing-safe comparison app.post('/webhooks/figma', (req, res) => { const received = req.body.passcode || ''; const expected = process.env.FIGMA_WEBHOOK_PASSCODE!; if (received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) { return res.status(401).json({ error: 'Invalid passcode' }); } res.status(200).json({ received: true }); processEvent(req.body); });
?depth=1//nodes?ids= fetches, Retry-After handling, env-var PATs, file_content:read scope, x255 color conversion, null-render filtering, webhook subscriptions with passcode verification| Symptom | Pitfall | Fix | |---------|---------|-----| | Responses > 1 MB, slow syncs, memory spikes | #1 full-tree fetches | ?depth=1 or /nodes?ids= (references/pitfall-1-fetching-full-file-trees.md) | | Bursts of 429s under load | #2 ignoring rate-limit headers | Honor Retry-After, batch requests (references/pitfall-2-ignoring-rate-limit-headers.md) | | Images break ~30 days after export | #3 cached export URLs | Re-export on demand or cache with short TTL | | figd_... in source control | #4 hardcoded PATs | Move to process.env.FIGMA_PAT, rotate the leaked token immediately | | Colors render wrong in generated CSS | #6 color format | Multiply Figma's 0-1 floats by 255 | | TypeError reading image URL | #7 null renders | Filter null entries from /v1/images responses | | Webhook events processed from unknown senders | #10 no passcode check | Verify passcode on every delivery (references/pitfall-10-webhook-without-passcode-verification.md) |
| # | Pitfall | Detection | Fix | |---|---------|-----------|-----| | 1 | Full file fetch | Response > 1MB | Use depth=1 or /nodes | | 2 | No rate limit handling | 429 errors | Read Retry-After, batch requests | | 3 | Stale image URLs | Broken images after 30 days | Re-export or short TTL cache | | 4 | Hardcoded PAT | grep -r figd_ in source | Use process.env.FIGMA_PAT | | 5 | Deprecated scope | files:read in token config | Use file_content:read | | 6 | Wrong color format | Colors look wrong | Multiply by 255 | | 7 | Null image render | TypeError on null URL | Filter null entries | | 8 | Polling loop | High API call volume | Use Webhooks V2 | | 9 | SVG with scale | Scale parameter ignored | SVG is always 1x | | 10 | No webhook verification | Security vulnerability | Verify passcode |
Audit an existing integration for the two highest-impact pitfalls in one pass:
bash# Pitfall 4: hardcoded PATs anywhere in the repo /usr/bin/grep -rn "figd_" --include='*.*' . | /usr/bin/grep -v node_modules # Pitfall 1: full-tree fetches (no depth/nodes constraint) /usr/bin/grep -rn "api.figma.com/v1/files/" --include='*.{ts,js}' . \ | /usr/bin/grep -v -e 'depth=' -e '/nodes'
Fix a color-conversion bug (Pitfall 6) — before/after:
typescript// Before: {"r":0.31,"g":0.27,"b":0.9} rendered as rgb(0,0,1) const css = `rgb(${fill.color.r}, ${fill.color.g}, ${fill.color.b})`; // After const to255 = (v: number) => Math.round(v * 255); const css = `rgb(${to255(fill.color.r)}, ${to255(fill.color.g)}, ${to255(fill.color.b)})`;
Every pitfall has a dedicated deep-dive under references/ (e.g. references/pitfall-8-polling-instead-of-webhooks.md).
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 27,345 | 34,726 | +27% | 1 | 1 | 0% | 5,064 | 7,727 | +53% | 0 | 0 | — |
case-02 | fail→pass | 19,919 | 16,527 | -17% | 1 | 1 | 0% | 3,875 | 6,002 | +55% | 0 | 0 | — |
case-03 | pass→pass | 7,962 | 5,385 | -32% | 1 | 1 | 0% | 1,474 | 3,650 | +148% | 0 | 0 | — |
case-04 | pass→pass | 7,784 | 7,747 | -0% | 1 | 1 | 0% | 1,420 | 4,163 | +193% | 0 | 0 | — |
case-05 | pass→pass | 14,367 | 10,574 | -26% | 1 | 1 | 0% | 2,104 | 4,638 | +120% | 0 | 0 | — |
case-06 | fail→pass | 15,257 | 10,485 | -31% | 1 | 1 | 0% | 2,640 | 4,605 | +74% | 0 | 0 | — |
case-07 | fail→pass | 14,463 | 4,257 | -71% | 1 | 1 | 0% | 2,438 | 3,420 | +40% | 0 | 0 | — |
case-08 | pass→pass | 11,790 | 6,603 | -44% | 1 | 1 | 0% | 1,966 | 4,003 | +104% | 0 | 0 | — |
case-09 | pass→pass | 11,070 | 10,748 | -3% | 1 | 1 | 0% | 1,983 | 4,778 | +141% | 0 | 0 | — |
case-10 | pass→pass | 10,761 | 9,743 | -9% | 1 | 1 | 0% | 1,645 | 4,170 | +153% | 0 | 0 | — |
case-11 | pass→pass | 10,103 | 7,258 | -28% | 1 | 1 | 0% | 1,723 | 3,960 | +130% | 0 | 0 | — |
case-12 | pass→pass | 18,014 | 11,137 | -38% | 1 | 1 | 0% | 2,612 | 4,775 | +83% | 0 | 0 | — |
case-13 | pass→pass | 11,075 | 3,914 | -65% | 1 | 1 | 0% | 1,775 | 3,480 | +96% | 0 | 0 | — |
case-14 | fail→pass | 944,169 | 126,981 | -87% | 1 | 1 | 0% | 2,481 | 5,208 | +110% | 0 | 0 | — |
case-15 | pass→pass | 731,005 | 915,266 | +25% | 1 | 1 | 0% | 2,508 | 3,949 | +57% | 0 | 0 | — |
case-16 | pass→pass | 9,996 | 6,486 | -35% | 1 | 1 | 0% | 1,931 | 3,944 | +104% | 0 | 0 | — |
case-17 | fail→pass | 9,524 | 4,299 | -55% | 1 | 1 | 0% | 1,644 | 3,488 | +112% | 0 | 0 | — |
case-18 | pass→pass | 16,406 | 11,443 | -30% | 1 | 1 | 0% | 3,094 | 4,966 | +61% | 0 | 0 | — |
case-19 | pass→pass | 9,618 | 5,958 | -38% | 1 | 1 | 0% | 1,544 | 3,819 | +147% | 0 | 0 | — |
case-20 | pass→pass | 9,542 | 12,835 | +35% | 1 | 1 | 0% | 1,844 | 5,078 | +175% | 0 | 0 | — |
case-21 | pass→pass | 12,437 | 10,915 | -12% | 1 | 1 | 0% | 2,274 | 4,483 | +97% | 0 | 0 | — |
case-22 | pass→fail | 18,448 | 17,363 | -6% | 1 | 1 | 0% | 3,727 | 6,363 | +71% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.