Miro SDK Patterns
Overview
Production-ready patterns for the @mirohq/miro-api Node.js client and direct REST API v2 usage. Covers the high-level Miro client (stateful, OAuth-aware) and the low-level MiroApi client (stateless, token-based).
Prerequisites
@mirohq/miro-apiinstalled- TypeScript 5+ project
- Understanding of Miro REST API v2 item model
Two Client Modes
import { Miro, MiroApi } from '@mirohq/miro-api';
// HIGH-LEVEL: Stateful, manages OAuth tokens per user
// Use for multi-user apps (SaaS, web apps with OAuth)
const miro = new Miro({
clientId: process.env.MIRO_CLIENT_ID!,
clientSecret: process.env.MIRO_CLIENT_SECRET!,
redirectUrl: process.env.MIRO_REDIRECT_URI!,
});
const userApi = await miro.as('user-id'); // Returns MiroApi scoped to user
// LOW-LEVEL: Stateless, pass token directly
// Use for scripts, automation, single-user integrations
const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!);
Pattern 1: Type-Safe Board Service
// src/miro/board-service.ts
import { MiroApi } from '@mirohq/miro-api';
// Response types matching Miro REST API v2
interface MiroBoard {
id: string;
type: 'board';
name: string;
description: string;
createdAt: string;
modifiedAt: string;
}
interface MiroBoardItem {
id: string;
type: 'sticky_note' | 'shape' | 'card' | 'text' | 'frame' | 'image' | 'document' | 'embed' | 'app_card';
data: Record<string, unknown>;
position: { x: number; y: number; origin: string };
geometry?: { width?: number; height?: number };
createdAt: string;
createdBy: { id: string; type: string };
}
interface PaginatedResponse<T> {
data: T[];
total: number;
size: number;
offset: number;
limit: number;
cursor?: string;
}
export class BoardService {
constructor(private api: MiroApi) {}
async getBoard(boardId: string): Promise<MiroBoard> {
const response = await this.api.getBoard(boardId);
return response.body as unknown as MiroBoard;
}
async listItems(
boardId: string,
options: { type?: string; limit?: number; cursor?: string } = {}
): Promise<PaginatedResponse<MiroBoardItem>> {
const params = new URLSearchParams();
if (options.type) params.set('type', options.type);
if (options.limit) params.set('limit', String(options.limit));
if (options.cursor) params.set('cursor', options.cursor);
const response = await fetch(
`https://api.miro.com/v2/boards/${boardId}/items?${params}`,
{ headers: this.authHeaders() }
);
return response.json();
}
async getAllItems(boardId: string): Promise<MiroBoardItem[]> {
const items: MiroBoardItem[] = [];
let cursor: string | undefined;
do {
const page = await this.listItems(boardId, { limit: 50, cursor });
items.push(...page.data);
cursor = page.cursor;
} while (cursor);
return items;
}
private authHeaders() {
return {
'Authorization': `Bearer ${process.env.MIRO_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
};
}
}
Pattern 2: Item Factory
// src/miro/item-factory.ts
type StickyNoteColor = 'light_yellow' | 'light_green' | 'light_blue'
| 'light_pink' | 'gray' | 'light_cyan' | 'light_orange';
interface CreateStickyNoteParams {
boardId: string;
content: string;
color?: StickyNoteColor;
x?: number;
y?: number;
width?: number;
}
interface CreateShapeParams {
boardId: string;
content: string;
shape?: 'rectangle' | 'circle' | 'triangle' | 'rhombus'
| 'round_rectangle' | 'parallelogram' | 'star'
| 'right_arrow' | 'left_arrow' | 'pentagon' | 'hexagon'
| 'octagon' | 'trapezoid' | 'flow_chart_predefined_process'
| 'can' | 'cross' | 'cloud';
fillColor?: string;
x?: number;
y?: number;
width?: number;
height?: number;
}
interface CreateCardParams {
boardId: string;
title: string;
description?: string;
dueDate?: string; // ISO 8601 date
assigneeId?: string;
x?: number;
y?: number;
}
export class ItemFactory {
constructor(private token: string) {}
async createStickyNote(params: CreateStickyNoteParams): Promise<MiroBoardItem> {
return this.post(`/v2/boards/${params.boardId}/sticky_notes`, {
data: { content: params.content, shape: 'square' },
style: { fillColor: params.color ?? 'light_yellow', textAlign: 'center' },
position: { x: params.x ?? 0, y: params.y ?? 0 },
geometry: { width: params.width ?? 199 },
});
}
async createShape(params: CreateShapeParams): Promise<MiroBoardItem> {
return this.post(`/v2/boards/${params.boardId}/shapes`, {
data: { content: params.content, shape: params.shape ?? 'round_rectangle' },
style: { fillColor: params.fillColor ?? '#4262ff', textAlign: 'center' },
position: { x: params.x ?? 0, y: params.y ?? 0 },
geometry: { width: params.width ?? 200, height: params.height ?? 100 },
});
}
async createCard(params: CreateCardParams): Promise<MiroBoardItem> {
return this.post(`/v2/boards/${params.boardId}/cards`, {
data: {
title: params.title,
description: params.description,
dueDate: params.dueDate,
assigneeId: params.assigneeId,
},
position: { x: params.x ?? 0, y: params.y ?? 0 },
});
}
private async post(path: string, body: unknown): Promise<MiroBoardItem> {
const res = await fetch(`https://api.miro.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json();
throw new MiroApiError(res.status, err.message ?? 'API request failed', err.code);
}
return res.json();
}
}
Pattern 3: Error Handling Wrapper
// src/miro/errors.ts
export class MiroApiError extends Error {
constructor(
public readonly status: number,
message: string,
public readonly code?: string,
) {
super(message);
this.name = 'MiroApiError';
}
get isRetryable(): boolean {
return this.status === 429 || (this.status >= 500 && this.status < 600);
}
get isAuthError(): boolean {
return this.status === 401 || this.status === 403;
}
}
async function safeMiroCall<T>(
operation: () => Promise<T>,
context: string
): Promise<{ data: T | null; error: MiroApiError | null }> {
try {
const data = await operation();
return { data, error: null };
} catch (err) {
if (err instanceof MiroApiError) {
console.error(`[Miro:${context}] ${err.status}: ${err.message}`);
return { data: null, error: err };
}
throw err; // Re-throw unexpected errors
}
}
Pattern 4: Multi-Tenant Client Factory
// src/miro/multi-tenant.ts
import { Miro, MiroApi } from '@mirohq/miro-api';
// For SaaS apps serving multiple Miro users
const clients = new Map<string, MiroApi>();
export async function getClientForUser(userId: string): Promise<MiroApi> {
if (!clients.has(userId)) {
const miro = new Miro({
clientId: process.env.MIRO_CLIENT_ID!,
clientSecret: process.env.MIRO_CLIENT_SECRET!,
redirectUrl: process.env.MIRO_REDIRECT_URI!,
});
if (!await miro.isAuthorized(userId)) {
throw new Error(`User ${userId} has not authorized Miro`);
}
const api = await miro.as(userId);
clients.set(userId, api);
}
return clients.get(userId)!;
}
Pattern 5: Response Validation with Zod
import { z } from 'zod';
const MiroBoardSchema = z.object({
id: z.string(),
type: z.literal('board'),
name: z.string(),
description: z.string().optional(),
createdAt: z.string().datetime(),
modifiedAt: z.string().datetime(),
});
const MiroItemSchema = z.object({
id: z.string(),
type: z.enum(['sticky_note', 'shape', 'card', 'text', 'frame', 'image', 'document', 'embed', 'app_card']),
data: z.record(z.unknown()),
position: z.object({ x: z.number(), y: z.number() }),
});
function validateBoardResponse(data: unknown) {
return MiroBoardSchema.parse(data);
}
Error Handling
| Pattern | Use Case | Benefit | |---------|----------|---------| | Type-safe service | Board/item CRUD | Catches shape mismatches at compile time | | Item factory | Bulk item creation | Consistent defaults, validated params | | Error wrapper | All API calls | Classifies errors as retryable vs auth vs input | | Multi-tenant | SaaS applications | Isolates users, manages token lifecycle | | Zod validation | Response parsing | Runtime safety against API changes |
Resources
Next Steps
Apply these patterns in miro-core-workflow-a for board management operations.