agentsclimarketplace

Miro local dev loop

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/miro-pack/skills/miro-local-dev-loop

'Configure Miro local development with hot reload, testing, and ngrok tunneling.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill miro-local-dev-loop

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.4 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

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

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

VariableRequiredDescription
MIRO_ACCESS_TOKENYesOAuth 2.0 access token
MIRO_CLIENT_IDFor OAuth flowApp client ID
MIRO_CLIENT_SECRETFor OAuth flowApp client secret
MIRO_REDIRECT_URIFor OAuth flowOAuth callback URL
MIRO_TEST_BOARD_IDFor integration testsBoard ID for live tests

Error Handling

ErrorCauseSolution
MIRO_ACCESS_TOKEN not setMissing env variableCopy .env.example to .env.local
ECONNREFUSED on webhook testDev server not runningStart with npm run dev first
invalid_tokenExpired access tokenRefresh token (see miro-install-auth)
Mock not matchingFixture out of dateRe-capture fixture from live API

Resources

Next Steps

See miro-sdk-patterns for production-ready code patterns.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.