Skip to content
/ fastrpc Public

A high-performance RPC framework for Deno with TCP transport, MessagePack serialization, and built-in observability

Notifications You must be signed in to change notification settings

Akin01/fastrpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

22 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

fastRPC

A high-performance RPC framework for Deno with TCP transport, MessagePack serialization, and built-in observability.

Features

  • πŸš€ High Performance - Built on TCP with MessagePack binary serialization
  • 🎯 Type-Safe - Full TypeScript support with decorators
  • πŸ”Œ Request/Response & Events - Support for both synchronous RPC calls and fire-and-forget events
  • πŸ›‘οΈ TLS Support - Optional secure communication
  • πŸ“Š Built-in Tracing - OpenTelemetry integration for distributed tracing
  • πŸ”§ Middleware Support - Extensible middleware pipeline
  • ⚑ Framed Messages - Length-prefixed message framing for reliable communication
  • πŸ₯ Health Checks - Built-in health check endpoint

Performance

  • Binary Protocol - MessagePack is more compact than JSON
  • Framed Messages - Length-prefixed frames prevent partial reads
  • Connection Pooling - Reuse connections for multiple requests
  • Zero-copy - Efficient buffer handling with Uint8Array
  • Async/Await - Non-blocking I/O throughout

Installation

deno add @akin01/fastrpc

Or import directly:

import { Controller, MessagePattern, TcpTransport } from "jsr:@akin01/fastrpc";

Quick Start

Server

import {
  Controller,
  EventPattern,
  getControllerHandler,
  MessagePattern,
  RpcHandler,
  TcpTransport,
} from "@akin01/fastrpc";

@Controller()
class MathController {
  @MessagePattern("math.add")
  add({ a, b }: { a: number; b: number }) {
    return a + b;
  }

  @MessagePattern("math.multiply")
  multiply({ a, b }: { a: number; b: number }) {
    return a * b;
  }
}

@Controller()
class UserController {
  @MessagePattern("user.get")
  async getUser({ id }: { id: number }) {
    // Simulate database query
    await new Promise((r) => setTimeout(r, 10));
    return { id, name: "Deno User" };
  }

  @EventPattern("user.created")
  onUserCreated(data: unknown) {
    console.log("πŸ”” New user created:", data);
  }
}

// Setup RPC handler
const rpcHandler = new RpcHandler();
rpcHandler.merge(getControllerHandler(MathController));
rpcHandler.merge(getControllerHandler(UserController));

// Start TCP transport
const transport = new TcpTransport(rpcHandler);
await transport.listen(3000);
console.log("πŸš€ RPC Server listening on port 3000");

Client

import {
  MESSAGE_PATTERN_EVENT,
  MESSAGE_PATTERN_REQUEST,
  MessagePackSerializer,
  type RpcMessage,
} from "@akin01/fastrpc";

const serializer = new MessagePackSerializer();

async function sendRequest(
  conn: Deno.Conn,
  message: RpcMessage,
): Promise<RpcMessage> {
  const payload = serializer.serialize(message);
  await conn.write(payload);

  const lenBuf = new Uint8Array(4);
  await conn.read(lenBuf);
  const len = new DataView(lenBuf.buffer).getUint32(0);

  const resBuf = new Uint8Array(len);
  await conn.read(resBuf);

  return serializer.deserialize(resBuf);
}

async function main() {
  const conn = await Deno.connect({ port: 3000 });

  try {
    // Math request
    const result = await sendRequest(conn, {
      pattern: "math.add",
      data: { a: 5, b: 3 },
      patternType: MESSAGE_PATTERN_REQUEST,
    });
    console.log("Result:", result.data); // 8

    // User request
    const user = await sendRequest(conn, {
      pattern: "user.get",
      data: { id: 1 },
      patternType: MESSAGE_PATTERN_REQUEST,
    });
    console.log("User:", user.data); // { id: 1, name: "Deno User" }
  } finally {
    conn.close();
  }
}

await main();

API Reference

Decorators

@Controller()

Marks a class as an RPC controller. Automatically manages an RpcHandler instance for the class.

@Controller()
class MyController {
  // ... handler methods
}

@MessagePattern(pattern: string)

Registers a request/response handler for the specified pattern.

@MessagePattern("user.get")
async getUser(data: { id: number }) {
  return { id: data.id, name: "John" };
}

@EventPattern(pattern: string)

Registers an event handler (fire-and-forget) for the specified pattern.

@EventPattern("user.created")
onUserCreated(data: unknown) {
  console.log("User created:", data);
}

@UseFilters(...middleware: MiddlewareFunc[])

Applies middleware to a specific handler.

@UseFilters(LoggingMiddleware, ValidationMiddleware)
@MessagePattern("user.create")
createUser(data: unknown) {
  // ... handler logic
}

Classes

RpcHandler

Manages message patterns and their handlers.

const handler = new RpcHandler();
handler.use(LoggingMiddleware); // Global middleware
handler.merge(getControllerHandler(MyController)); // Merge controller

TcpTransport

Handles TCP connections and message routing.

const transport = new TcpTransport(rpcHandler, {
  certFile: "./cert.pem", // Optional TLS
  keyFile: "./key.pem", // Optional TLS
});
await transport.listen(3000);

MessagePackSerializer

Serializes/deserializes messages using MessagePack. Notes that serialize also frame the message with 4-byte length prefix.

const serializer = new MessagePackSerializer();
const bytes = serializer.serialize(message);
const message = serializer.deserialize(bytes);

Utilities

getControllerHandler(controller: Class | Instance)

Retrieves the RpcHandler from a decorated controller class or instance. Supports dependency injection.

// Without dependencies - pass the class
const handler = getControllerHandler(MyController);
rpcHandler.merge(handler);

// With dependencies - pass an instance
const db = new DatabaseService();
const logger = new LoggerService();
const controller = new MyController(db, logger);
const handler = getControllerHandler(controller);
rpcHandler.merge(handler);

Dependency Injection

Controllers can accept dependencies through their constructors, enabling better testability and separation of concerns.

Simple Controllers (No Dependencies)

@Controller()
class MathController {
  @MessagePattern("math.add")
  add({ a, b }: { a: number; b: number }) {
    return a + b;
  }
}

// Auto-instantiation
rpcHandler.merge(getControllerHandler(MathController));

Constructor Injection (Recommended)

// Define services
class DatabaseService {
  async findUser(id: number) {
    // ... database logic
  }
}

class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

// Controller with dependencies
@Controller()
class UserController {
  constructor(
    private db: DatabaseService,
    private logger: LoggerService,
  ) {}

  @MessagePattern("user.get")
  async getUser({ id }: { id: number }) {
    this.logger.log(`Fetching user ${id}`);
    return await this.db.findUser(id);
  }
}

// Manually instantiate with dependencies
const db = new DatabaseService();
const logger = new LoggerService();
const userController = new UserController(db, logger);

// Register the instance
rpcHandler.merge(getControllerHandler(userController));

Benefits

  • Testability: Easy to mock dependencies in unit tests
  • Flexibility: Swap implementations without changing controller code
  • Type Safety: Full TypeScript support for dependencies
  • Separation of Concerns: Controllers focus on RPC logic, not service creation

For a complete example, see examples/dependency-injection.

Middleware

Create custom middleware by implementing the MiddlewareFunc type:

import type { MiddlewareFunc } from "@akin01/fastrpc";

export const LoggingMiddleware: MiddlewareFunc = async (message, next) => {
  console.log(`πŸ“₯ [${message.pattern}]`, message.data);
  const start = Date.now();

  try {
    const result = await next();
    console.log(`βœ… Reply (${Date.now() - start}ms):`, result);
    return result;
  } catch (error) {
    console.log(`❌ Error (${Date.now() - start}ms):`, error);
    throw error;
  }
};

Apply middleware globally or per-handler:

// Global middleware
rpcHandler.use(LoggingMiddleware);

// Per-handler middleware
@UseFilters(LoggingMiddleware)
@MessagePattern("important.operation")
handleOperation(data: unknown) {
  // ...
}

Message Format

RpcMessage

type RpcMessage = {
  id?: string; // Optional message ID for request/response correlation
  pattern: string; // Handler pattern (e.g., "user.get")
  data: ValueType; // Message payload (serializable by MessagePack)
  patternType: MessagePattern; // 0 = REQUEST, 1 = EVENT
  timeoutMs?: number; // Optional timeout override
};

Message Patterns

  • MESSAGE_PATTERN_REQUEST (0) - Request/response pattern
  • MESSAGE_PATTERN_EVENT (1) - Fire-and-forget event pattern

TLS Support

Enable TLS by providing certificate and key files:

const transport = new TcpTransport(rpcHandler, {
  certFile: "./certs/server.crt",
  keyFile: "./certs/server.key",
});

Observability

fastRPC has built-in OpenTelemetry tracing support. Set the OTEL_DENO=true environment variable to enable automatic tracing:

OTEL_DENO=true deno run -A server.ts

Each RPC call creates a span with:

  • Pattern name
  • Request/Event type
  • Parent trace context propagation
  • Success/error status

Health Checks

A built-in health check endpoint is automatically registered at __health__:

const health = await sendRequest(conn, {
  pattern: "__health__",
  data: {},
  patternType: MESSAGE_PATTERN_REQUEST,
});
// Response: { status: "ok", timestamp: 1234567890, uptime: 12345 }

Examples

See the examples directory for complete working examples:

  • hello-rpc - Basic request/response and event examples with dependency injection
  • dependency-injection - Comprehensive guide to dependency injection patterns

Run Examples

# Start the hello-rpc server
deno task example-server

# In another terminal, run the client
deno task example-client

# Or run the dependency injection example
deno run --allow-net examples/dependency-injection/server.ts
deno run --allow-net examples/dependency-injection/client.ts

Error Handling

Handlers can throw errors which will be caught and sent back to the client:

@MessagePattern("user.get")
getUser({ id }: { id: number }) {
  if (id < 0) {
    throw new Error("Invalid user ID");
  }
  return { id, name: "User" };
}

Client receives:

{
  error: "Invalid user ID";
}

Timeouts

Set custom timeouts per request:

const response = await sendRequest(conn, {
  pattern: "slow.operation",
  data: {},
  patternType: MESSAGE_PATTERN_REQUEST,
  timeoutMs: 10000, // 10 seconds
});

Default timeout: 5000ms

Graceful Shutdown

The transport handles graceful shutdown on SIGINT/SIGTERM:

const transport = new TcpTransport(rpcHandler);
await transport.listen(3000);

// Server will wait for active connections to complete (up to 5s)
// before shutting down

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Links

About

A high-performance RPC framework for Deno with TCP transport, MessagePack serialization, and built-in observability

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published