From miro-pack
Sets up local dev workflow for Miro API v2 integrations with TypeScript hot reload via tsx, Vitest testing using fixtures, and ngrok webhook tunneling.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin miro-packThis skill is limited to using the following tools:
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.
Provides TypeScript patterns for @mirohq/miro-api SDK and Miro REST API v2, including OAuth multi-user clients, token-based APIs, and type-safe board services.
Sets up TypeScript/Node.js local dev environment for MaintainX API integrations with tsx hot reload, Vitest unit tests, axios mocks, and project scaffolding.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Share bugs, ideas, or general feedback.
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.
miro-install-auth setupboards:read and boards:write scopesmy-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
{
"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"
}
}
// 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;
}
// 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" }
}
// 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);
});
});
# 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)
// 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),
},
});
| 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 | 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 |
See miro-sdk-patterns for production-ready code patterns.