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-api installed
- TypeScript 5+ project
- Understanding of Miro REST API v2 item model
Two Client Modes
import { Miro, MiroApi } from '@mirohq/miro-api';
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');
const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!);
Pattern 1: Type-Safe Board Service
import { MiroApi } from '@mirohq/miro-api';
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
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;
?: ;
?: ;
?: ;
}
{
: ;
: ;
?: ;
?: ;
?: ;
?: ;
?: ;
}
{
() {}
(: ): <> {
.(, {
: { : params., : },
: { : params. ?? , : },
: { : params. ?? , : params. ?? },
: { : params. ?? },
});
}
(: ): <> {
.(, {
: { : params., : params. ?? },
: { : params. ?? , : },
: { : params. ?? , : params. ?? },
: { : params. ?? , : params. ?? },
});
}
(: ): <> {
.(, {
: {
: params.,
: params.,
: params.,
: params.,
},
: { : params. ?? , : params. ?? },
});
}
(: , : ): <> {
res = (, {
: ,
: {
: ,
: ,
},
: .(body),
});
(!res.) {
err = res.();
(res., err. ?? , err.);
}
res.();
}
}
Pattern 3: Error Handling Wrapper
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<{ : T | ; : | }> {
{
data = ();
{ data, : };
} (err) {
(err ) {
.();
{ : , : err };
}
err;
}
}
Pattern 4: Multi-Tenant Client Factory
import { Miro, MiroApi } from '@mirohq/miro-api';
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) {
.(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.