Files
gh-jezweb-claude-skills-ski…/references/rpc-patterns.md
2025-11-30 08:24:13 +08:00

6.5 KiB

RPC vs Fetch Patterns - Decision Guide

When to use RPC methods vs HTTP fetch handler.


Quick Decision Matrix

Requirement Use Why
New project (compat_date >= 2024-04-03) RPC Simpler, type-safe
Type safety important RPC TypeScript knows method signatures
Simple method calls RPC Less boilerplate
WebSocket upgrade needed Fetch Requires HTTP upgrade
Complex HTTP routing Fetch Full request/response control
Need headers, cookies, status codes Fetch HTTP-specific features
Legacy compatibility Fetch Pre-2024-04-03 projects
Auto-serialization wanted RPC Handles structured data automatically

Enable RPC

Set compatibility date >= 2024-04-03:

{
  "compatibility_date": "2025-10-22"
}

Define RPC Methods

export class MyDO extends DurableObject {
  // Public methods are automatically exposed as RPC
  async increment(): Promise<number> {
    // ...
  }

  async get(): Promise<number> {
    // ...
  }

  // Private methods are NOT exposed
  private async internalHelper(): Promise<void> {
    // ...
  }
}

Call from Worker

const stub = env.MY_DO.getByName('my-instance');

// Direct method calls
const count = await stub.increment();
const value = await stub.get();

Advantages

Type-safe - TypeScript knows method signatures Less boilerplate - No HTTP ceremony Auto-serialization - Structured data works seamlessly Exception propagation - Errors thrown in DO received in Worker

Limitations

Cannot use HTTP-specific features (headers, status codes) Cannot handle WebSocket upgrades Requires compat_date >= 2024-04-03


HTTP Fetch Pattern

Define fetch() Handler

export class MyDO extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/increment' && request.method === 'POST') {
      // ...
      return new Response(JSON.stringify({ count }), {
        headers: { 'content-type': 'application/json' },
      });
    }

    return new Response('Not found', { status: 404 });
  }
}

Call from Worker

const stub = env.MY_DO.getByName('my-instance');

const response = await stub.fetch('https://fake-host/increment', {
  method: 'POST',
});

const data = await response.json();

Advantages

Full HTTP control - Headers, cookies, status codes WebSocket upgrades - Required for WebSocket server Complex routing - Use path, method, headers for routing Legacy compatible - Works with pre-2024-04-03

Limitations

More boilerplate - Manual JSON parsing, response creation No type safety - Worker doesn't know what methods exist Manual error handling - Must parse HTTP status codes


Hybrid Pattern (Both)

Use both RPC and fetch() in same DO:

export class MyDO extends DurableObject {
  // RPC method for simple calls
  async getStatus(): Promise<{ active: boolean }> {
    return { active: true };
  }

  // Fetch for WebSocket upgrade
  async fetch(request: Request): Promise<Response> {
    const upgradeHeader = request.headers.get('Upgrade');

    if (upgradeHeader === 'websocket') {
      // Handle WebSocket upgrade
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);

      this.ctx.acceptWebSocket(server);

      return new Response(null, {
        status: 101,
        webSocket: client,
      });
    }

    return new Response('Not found', { status: 404 });
  }
}

Call from Worker:

const stub = env.MY_DO.getByName('my-instance');

// Use RPC for status
const status = await stub.getStatus();

// Use fetch for WebSocket upgrade
const response = await stub.fetch(request);

RPC Serialization

What works:

  • Primitives (string, number, boolean, null)
  • Objects (plain objects)
  • Arrays
  • Nested structures
  • Date objects
  • ArrayBuffer, Uint8Array, etc.

What doesn't work:

  • Functions
  • Symbols
  • Circular references
  • Class instances (except basic types)

Example:

// ✅ WORKS
async getData(): Promise<{ users: string[]; count: number }> {
  return {
    users: ['alice', 'bob'],
    count: 2,
  };
}

// ❌ DOESN'T WORK
async getFunction(): Promise<() => void> {
  return () => console.log('hello');  // Functions not serializable
}

Error Handling

RPC Error Handling

// In DO
async doWork(): Promise<void> {
  if (somethingWrong) {
    throw new Error('Something went wrong');
  }
}

// In Worker
try {
  await stub.doWork();
} catch (error) {
  console.error('RPC error:', error.message);
  // Error propagated from DO
}

Fetch Error Handling

// In DO
async fetch(request: Request): Promise<Response> {
  if (somethingWrong) {
    return new Response(JSON.stringify({ error: 'Something went wrong' }), {
      status: 500,
    });
  }

  return new Response('OK');
}

// In Worker
const response = await stub.fetch(request);

if (!response.ok) {
  const error = await response.json();
  console.error('Fetch error:', error);
}

Migration from Fetch to RPC

Before (Fetch):

export class Counter extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/increment') {
      let count = await this.ctx.storage.get<number>('count') || 0;
      count += 1;
      await this.ctx.storage.put('count', count);

      return new Response(JSON.stringify({ count }), {
        headers: { 'content-type': 'application/json' },
      });
    }

    return new Response('Not found', { status: 404 });
  }
}

After (RPC):

export class Counter extends DurableObject {
  async increment(): Promise<number> {
    let count = await this.ctx.storage.get<number>('count') || 0;
    count += 1;
    await this.ctx.storage.put('count', count);
    return count;
  }
}

// Worker before:
const response = await stub.fetch('https://fake-host/increment');
const { count } = await response.json();

// Worker after:
const count = await stub.increment();

Benefits:

  • ~60% less code
  • Type-safe
  • Cleaner, more maintainable

Official Docs: https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/