MeetingRouter

Best Practices for v1 API

@meeting-baas/sdk - v1 API Reference / v1-best-practices

Best Practices for v1 API

This guide covers recommended patterns and best practices for using the Meeting BaaS v1 API effectively.

Client Configuration

Environment Variables

Always store sensitive credentials in environment variables:

import { createBaasClient } from "@meeting-baas/sdk";

const client = createBaasClient({
  api_key: process.env.MEETING_BAAS_API_KEY!,
  timeout: 60000 // 60 seconds for long operations
});

Singleton Pattern

Reuse a single client instance throughout your application:

// lib/meeting-baas.ts
import { createBaasClient } from "@meeting-baas/sdk";

let clientInstance: ReturnType<typeof createBaasClient> | null = null;

export function getMeetingBaasClient() {
  if (!clientInstance) {
    if (!process.env.MEETING_BAAS_API_KEY) {
      throw new Error("MEETING_BAAS_API_KEY is not set");
    }

    clientInstance = createBaasClient({
      api_key: process.env.MEETING_BAAS_API_KEY,
      timeout: 60000
    });
  }

  return clientInstance;
}
// Usage in other files
import { getMeetingBaasClient } from "./lib/meeting-baas";

const client = getMeetingBaasClient();
const result = await client.joinMeeting({ ... });

Error Handling

Type-Safe Error Checking

Always check the error type:

import { ZodError } from "zod";

const result = await client.joinMeeting({ ... });

if (!result.success) {
  if (result.error instanceof ZodError) {
    // Handle validation errors
    console.error("Validation failed:");
    result.error.errors.forEach(err => {
      console.error(`  ${err.path.join(".")}: ${err.message}`);
    });
  } else {
    // Handle API errors
    console.error("API error:", result.error.message);
  }
}

Graceful Degradation

async function joinMeetingWithFallback(
  meetingUrl: string,
  botName: string
) {
  const result = await client.joinMeeting({
    meeting_url: meetingUrl,
    bot_name: botName,
    reserved: true,
    recording_mode: "gallery_view",
    speech_to_text: { provider: "Gladia" }
  });

  if (result.success) {
    return result.data;
  }

  // Retry with basic configuration
  console.warn("Retrying with basic configuration");

  const fallbackResult = await client.joinMeeting({
    meeting_url: meetingUrl,
    bot_name: botName,
    reserved: true
  });

  if (fallbackResult.success) {
    return fallbackResult.data;
  }

  throw new Error(`Failed to join meeting: ${fallbackResult.error.message}`);
}

Bot Lifecycle Management

Complete Lifecycle Handler

class BotManager {
  private client: ReturnType<typeof createBaasClient>;
  private activeBots = new Map<string, { url: string; startedAt: Date }>();

  constructor(apiKey: string) {
    this.client = createBaasClient({ api_key: apiKey });
  }

  async joinMeeting(meetingUrl: string, botName: string) {
    const result = await this.client.joinMeeting({
      meeting_url: meetingUrl,
      bot_name: botName,
      reserved: true
    });

    if (!result.success) {
      throw new Error(`Failed to join: ${result.error.message}`);
    }

    const botId = result.data.bot_id;
    this.activeBots.set(botId, {
      url: meetingUrl,
      startedAt: new Date()
    });

    return botId;
  }

  async leaveMeeting(botId: string) {
    const result = await this.client.leaveMeeting({ uuid: botId });

    if (result.success) {
      this.activeBots.delete(botId);
    }

    return result;
  }

  async getMeetingData(botId: string) {
    return await this.client.getMeetingData({
      bot_id: botId,
      include_transcripts: true
    });
  }

  async cleanup() {
    const cleanupPromises = Array.from(this.activeBots.keys()).map(botId =>
      this.leaveMeeting(botId).catch(err =>
        console.error(`Failed to leave bot ${botId}:`, err)
      )
    );

    await Promise.allSettled(cleanupPromises);
  }

  getActiveBots() {
    return Array.from(this.activeBots.entries()).map(([id, info]) => ({
      botId: id,
      ...info
    }));
  }
}

// Usage
const manager = new BotManager(process.env.MEETING_BAAS_API_KEY!);

// Cleanup on process exit
process.on("SIGINT", async () => {
  console.log("Cleaning up bots...");
  await manager.cleanup();
  process.exit(0);
});

Polling for Completion

Since v1 doesn't have webhooks, poll for completion:

async function waitForBotCompletion(
  botId: string,
  maxWaitMs = 3600000, // 1 hour
  pollIntervalMs = 10000 // 10 seconds
) {
  const startTime = Date.now();

  while (Date.now() - startTime < maxWaitMs) {
    const result = await client.getMeetingData({ bot_id: botId });

    if (!result.success) {
      throw new Error(`Failed to get bot status: ${result.error.message}`);
    }

    const statusChanges = result.data.status_changes;
    const lastStatus = statusChanges[statusChanges.length - 1];

    if (lastStatus?.code === "done" || lastStatus?.code === "fatal") {
      return result.data;
    }

    await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
  }

  throw new Error("Bot did not complete within timeout period");
}

// Usage
const botId = await joinMeeting("...", "...");
const meetingData = await waitForBotCompletion(botId);
console.log("Meeting completed:", meetingData);

Parameter Validation

Pre-Validation

Validate parameters before API calls:

function isValidMeetingUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    const validHosts = [
      "meet.google.com",
      "zoom.us",
      "teams.microsoft.com"
    ];

    return validHosts.some(host => parsed.hostname.includes(host));
  } catch {
    return false;
  }
}

async function joinMeetingSafely(meetingUrl: string, botName: string) {
  // Pre-validate
  if (!isValidMeetingUrl(meetingUrl)) {
    throw new Error("Invalid meeting URL format");
  }

  if (!botName || botName.length < 2) {
    throw new Error("Bot name must be at least 2 characters");
  }

  // Make API call
  return await client.joinMeeting({
    meeting_url: meetingUrl,
    bot_name: botName,
    reserved: true
  });
}

Calendar Integration

Automatic Meeting Recording

async function setupCalendarRecording(calendarId: string) {
  // Get upcoming events
  const now = new Date();
  const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);

  const eventsResult = await client.listCalendarEvents({
    calendar_id: calendarId,
    start_date_gte: now.toISOString(),
    start_date_lte: nextWeek.toISOString()
  });

  if (!eventsResult.success) {
    throw new Error(`Failed to list events: ${eventsResult.error.message}`);
  }

  // Join meetings with bots
  const joinPromises = eventsResult.data.events.map(async event => {
    if (!event.meeting_url) return null;

    try {
      const result = await client.joinMeeting({
        meeting_url: event.meeting_url,
        bot_name: `Recorder for ${event.title}`,
        reserved: true,
        extra: {
          event_id: event.id,
          calendar_id: calendarId
        }
      });

      return result.success ? result.data : null;
    } catch (error) {
      console.error(`Failed to join event ${event.id}:`, error);
      return null;
    }
  });

  const results = await Promise.all(joinPromises);
  return results.filter(r => r !== null);
}

Retry Logic

Exponential Backoff

async function withExponentialBackoff\<T\>(
  operation: () => Promise\<T\>,
  maxRetries = 3,
  baseDelay = 1000
): Promise\<T\> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      const isLastAttempt = attempt === maxRetries - 1;

      // Don't retry validation errors
      if (error instanceof ZodError || isLastAttempt) {
        throw error;
      }

      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error("Max retries exceeded");
}

// Usage
const result = await withExponentialBackoff(() =>
  client.joinMeeting({
    meeting_url: "https://meet.google.com/abc",
    bot_name: "Test Bot",
    reserved: true
  })
);

Logging and Monitoring

Structured Logging

interface LogEntry {
  timestamp: string;
  level: "info" | "warn" | "error";
  operation: string;
  botId?: string;
  duration?: number;
  error?: string;
  metadata?: Record<string, any>;
}

class Logger {
  log(entry: LogEntry) {
    console.log(JSON.stringify(entry));
  }

  async logOperation\<T\>(
    operation: string,
    fn: () => Promise\<T\>,
    metadata?: Record<string, any>
  ): Promise\<T\> {
    const startTime = Date.now();

    try {
      const result = await fn();

      this.log({
        timestamp: new Date().toISOString(),
        level: "info",
        operation,
        duration: Date.now() - startTime,
        metadata
      });

      return result;
    } catch (error: any) {
      this.log({
        timestamp: new Date().toISOString(),
        level: "error",
        operation,
        duration: Date.now() - startTime,
        error: error.message,
        metadata
      });

      throw error;
    }
  }
}

// Usage
const logger = new Logger();

const result = await logger.logOperation(
  "joinMeeting",
  () => client.joinMeeting({
    meeting_url: meetingUrl,
    bot_name: botName,
    reserved: true
  }),
  { meetingUrl, botName }
);

Security

Sanitize Inputs

function sanitizeBotName(name: string): string {
  // Remove potentially harmful characters
  return name
    .replace(/[<>\"\']/g, "")
    .substring(0, 100);
}

function sanitizeMetadata(metadata: Record<string, any>): Record<string, any> {
  const sanitized: Record<string, any> = {};

  for (const [key, value] of Object.entries(metadata)) {
    if (typeof value === "string") {
      sanitized[key] = value.substring(0, 1000);
    } else if (typeof value === "number" || typeof value === "boolean") {
      sanitized[key] = value;
    }
  }

  return sanitized;
}

// Usage
const result = await client.joinMeeting({
  meeting_url: meetingUrl,
  bot_name: sanitizeBotName(userProvidedName),
  reserved: true,
  extra: sanitizeMetadata(userProvidedMetadata)
});

Never Log Sensitive Data

function sanitizeForLogging(data: any): any {
  const sanitized = { ...data };

  const sensitiveKeys = [
    "api_key",
    "oauth_client_secret",
    "oauth_refresh_token",
    "password",
    "token"
  ];

  for (const key of sensitiveKeys) {
    if (key in sanitized) {
      sanitized[key] = "[REDACTED]";
    }
  }

  return sanitized;
}

console.log("Request:", sanitizeForLogging(requestData));

Testing

Mock Client for Testing

// test/utils/mock-client.ts
export function createMockClient() {
  return {
    joinMeeting: jest.fn().mockResolvedValue({
      success: true,
      data: {
        bot_id: "mock-bot-id",
        status_changes: []
      }
    }),

    leaveMeeting: jest.fn().mockResolvedValue({
      success: true,
      data: { bot_id: "mock-bot-id" }
    }),

    getMeetingData: jest.fn().mockResolvedValue({
      success: true,
      data: {
        bot_id: "mock-bot-id",
        status_changes: [{ code: "done", message: "Complete" }],
        mp4: "https://example.com/video.mp4"
      }
    })
  };
}

// test/bot-manager.test.ts
import { createMockClient } from "./utils/mock-client";

test("should join meeting", async () => {
  const mockClient = createMockClient();
  const manager = new BotManager(mockClient as any);

  const botId = await manager.joinMeeting(
    "https://meet.google.com/abc",
    "Test Bot"
  );

  expect(botId).toBe("mock-bot-id");
  expect(mockClient.joinMeeting).toHaveBeenCalledWith({
    meeting_url: "https://meet.google.com/abc",
    bot_name: "Test Bot",
    reserved: true
  });
});

Performance

Parallel Operations

async function joinMultipleMeetings(
  meetings: Array<{ url: string; name: string }>
) {
  // Execute joins in parallel
  const results = await Promise.allSettled(
    meetings.map(m =>
      client.joinMeeting({
        meeting_url: m.url,
        bot_name: m.name,
        reserved: true
      })
    )
  );

  const successful = results
    .filter((r): r is PromiseFulfilledResult<any> =>
      r.status === "fulfilled" && r.value.success
    )
    .map(r => r.value.data);

  const failed = results
    .filter(r =>
      r.status === "rejected" ||
      (r.status === "fulfilled" && !r.value.success)
    );

  return { successful, failed };
}

On this page