Agent skill

miro-local-dev-loop

Configure Miro local development with hot reload, testing, and ngrok tunneling. Use when setting up a development environment, configuring test workflows, or establishing a fast iteration cycle with the Miro REST API v2. Trigger with phrases like "miro dev setup", "miro local development", "miro dev environment", "develop with miro", "miro testing".

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/miro-pack/skills/miro-local-dev-loop

SKILL.md

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 miro-install-auth setup
  • Node.js 18+ with npm or pnpm
  • Access token with boards:read and boards:write scopes
  • 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

json
{
  "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

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

json
// 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" }
  }
}
json
// 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

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

bash
# 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

typescript
// 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
MIRO_ACCESS_TOKEN Yes OAuth 2.0 access token
MIRO_CLIENT_ID For OAuth flow App client ID
MIRO_CLIENT_SECRET For OAuth flow App client secret
MIRO_REDIRECT_URI For OAuth flow OAuth callback URL
MIRO_TEST_BOARD_ID For integration tests Board ID for live tests

Error Handling

Error Cause Solution
MIRO_ACCESS_TOKEN not set Missing env variable Copy .env.example to .env.local
ECONNREFUSED on webhook test Dev server not running Start with npm run dev first
invalid_token Expired access token Refresh token (see miro-install-auth)
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.

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