技能 编程开发 Miro API开发最佳实践与模式

Miro API开发最佳实践与模式

v20260423
miro-sdk-patterns
本指南提供了生产级的Miro API使用模式和最佳实践。内容涵盖了从高层状态管理客户端到低层REST API v2的完整用法。提供了用于实现健壮的画板服务(数据获取与分页)和元素工厂(创建便利贴、形状或卡片)的结构化模式,是构建SaaS应用和复杂自动化工作流的必备参考。
获取技能
413 次下载
概览

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';

// 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.

信息
Category 编程开发
Name miro-sdk-patterns
版本 v20260423
大小 9.33KB
更新时间 2026-04-28
语言