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
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;
}
export function resetMiroApi(): void {
instance = null;
}
Step 4: Test Fixtures from Real API Responses
{
"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"
{
"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"
Step 5: Unit Tests with Vitest Mocks
import { describe, it, expect, vi, beforeEach } from 'vitest';
import boardFixture from './fixtures/board.json';
import stickyNoteFixture from './fixtures/sticky-note.json';
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: { : , : },
: { : , : },
}),
}
);
note = response.();
(note.).();
(note..).();
(mockFetch).(
expect.(),
expect.({ : })
);
});
(, () => {
mockFetch.({
: ,
: ,
: ({
: ,
: ,
}),
: () => ({ : , : }),
});
response = (, {
: { : },
});
(response.).();
});
});
Step 6: Ngrok Tunneling for Webhooks
npm run dev
ngrok http 3000
Step 7: Debug Logging
import { MiroApi } from '@mirohq/miro-api';
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.