Claude-code-plugins-plus miro-local-dev-loop
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/miro-pack/skills/miro-local-dev-loop" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-miro-local-dev-loop && rm -rf "$T"
manifest:
plugins/saas-packs/miro-pack/skills/miro-local-dev-loop/SKILL.mdsource content
Miro Local Dev Loop
Overview
Set up a fast local development workflow for building Miro integrations, including hot reload, test mocking against the REST API v2, and ngrok tunneling for webhooks.
Prerequisites
- Completed
setupmiro-install-auth - Node.js 18+ with npm or pnpm
- Access token with
andboards:read
scopesboards:write - ngrok (for webhook development)
Instructions
Step 1: Project Structure
my-miro-app/ ├── src/ │ ├── miro/ │ │ ├── client.ts # MiroApi wrapper singleton │ │ ├── boards.ts # Board CRUD operations │ │ ├── items.ts # Item operations (sticky notes, shapes, etc.) │ │ └── types.ts # Response type definitions │ ├── webhooks/ │ │ └── handler.ts # Webhook event processing │ └── index.ts ├── tests/ │ ├── miro-client.test.ts │ └── fixtures/ │ ├── board.json # Sample board response │ └── sticky-note.json # Sample item response ├── .env.local # Local secrets (git-ignored) ├── .env.example # Template for team ├── package.json └── tsconfig.json
Step 2: Package Configuration
{ "scripts": { "dev": "tsx watch src/index.ts", "test": "vitest", "test:watch": "vitest --watch", "test:integration": "MIRO_TEST_MODE=live vitest run tests/integration/", "tunnel": "ngrok http 3000", "typecheck": "tsc --noEmit" }, "dependencies": { "@mirohq/miro-api": "^2.0.0", "express": "^4.18.0", "dotenv": "^16.0.0" }, "devDependencies": { "tsx": "^4.0.0", "vitest": "^1.0.0", "typescript": "^5.0.0" } }
Step 3: Miro Client Singleton
// src/miro/client.ts import { MiroApi } from '@mirohq/miro-api'; let instance: MiroApi | null = null; export function getMiroApi(): MiroApi { if (!instance) { const token = process.env.MIRO_ACCESS_TOKEN; if (!token) throw new Error('MIRO_ACCESS_TOKEN not set'); instance = new MiroApi(token); } return instance; } // For testing — allow injecting a mock export function resetMiroApi(): void { instance = null; }
Step 4: Test Fixtures from Real API Responses
// tests/fixtures/board.json { "id": "uXjVN1234567890", "type": "board", "name": "Test Board", "description": "Fixture for unit tests", "createdAt": "2025-01-15T10:00:00Z", "modifiedAt": "2025-01-15T10:30:00Z", "owner": { "id": "123456", "type": "user", "name": "Dev User" }, "policy": { "sharingPolicy": { "access": "private" }, "permissionsPolicy": { "collaborationToolsStartAccess": "all_editors" } } }
// tests/fixtures/sticky-note.json { "id": "3458764500000001", "type": "sticky_note", "data": { "content": "Test note", "shape": "square" }, "style": { "fillColor": "light_yellow", "textAlign": "center" }, "position": { "x": 100, "y": 200, "origin": "center" }, "geometry": { "width": 199 }, "createdAt": "2025-01-15T10:05:00Z", "modifiedAt": "2025-01-15T10:05:00Z", "createdBy": { "id": "123456", "type": "user" } }
Step 5: Unit Tests with Vitest Mocks
// tests/miro-client.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import boardFixture from './fixtures/board.json'; import stickyNoteFixture from './fixtures/sticky-note.json'; // Mock fetch for Miro API calls const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); describe('Miro Board Operations', () => { beforeEach(() => { mockFetch.mockReset(); }); it('should create a sticky note on a board', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 201, json: async () => stickyNoteFixture, }); const response = await fetch( 'https://api.miro.com/v2/boards/uXjVN123/sticky_notes', { method: 'POST', headers: { 'Authorization': 'Bearer test-token', 'Content-Type': 'application/json', }, body: JSON.stringify({ data: { content: 'Test note', shape: 'square' }, position: { x: 100, y: 200 }, }), } ); const note = await response.json(); expect(note.type).toBe('sticky_note'); expect(note.data.content).toBe('Test note'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/v2/boards/'), expect.objectContaining({ method: 'POST' }) ); }); it('should handle 429 rate limit responses', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 429, headers: new Headers({ 'X-RateLimit-Remaining': '0', 'Retry-After': '5', }), json: async () => ({ status: 429, message: 'Rate limit exceeded' }), }); const response = await fetch('https://api.miro.com/v2/boards', { headers: { 'Authorization': 'Bearer test-token' }, }); expect(response.status).toBe(429); }); });
Step 6: Ngrok Tunneling for Webhooks
# Start your dev server npm run dev # In another terminal, start ngrok ngrok http 3000 # Copy the HTTPS URL (e.g., https://abc123.ngrok.app) # Register it as a webhook callback in your Miro app settings # or via the API (see miro-webhooks-events skill)
Step 7: Debug Logging
// Enable verbose HTTP logging during development import { MiroApi } from '@mirohq/miro-api'; // Log all API requests and responses const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!, { logger: { info: (...args) => console.log('[MIRO]', ...args), warn: (...args) => console.warn('[MIRO]', ...args), error: (...args) => console.error('[MIRO]', ...args), }, });
Environment Variables
| Variable | Required | Description |
|---|---|---|
| Yes | OAuth 2.0 access token |
| For OAuth flow | App client ID |
| For OAuth flow | App client secret |
| For OAuth flow | OAuth callback URL |
| For integration tests | Board ID for live tests |
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Missing env variable | Copy to |
on webhook test | Dev server not running | Start with first |
| Expired access token | Refresh token (see ) |
| Mock not matching | Fixture out of date | Re-capture fixture from live API |
Resources
Next Steps
See
miro-sdk-patterns for production-ready code patterns.