A high-performance RPC framework for Deno with TCP transport, MessagePack serialization, and built-in observability.
- π 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
- 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
deno add @akin01/fastrpcOr import directly:
import { Controller, MessagePattern, TcpTransport } from "jsr:@akin01/fastrpc";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");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();Marks a class as an RPC controller. Automatically manages an RpcHandler instance for the class.
@Controller()
class MyController {
// ... handler methods
}Registers a request/response handler for the specified pattern.
@MessagePattern("user.get")
async getUser(data: { id: number }) {
return { id: data.id, name: "John" };
}Registers an event handler (fire-and-forget) for the specified pattern.
@EventPattern("user.created")
onUserCreated(data: unknown) {
console.log("User created:", data);
}Applies middleware to a specific handler.
@UseFilters(LoggingMiddleware, ValidationMiddleware)
@MessagePattern("user.create")
createUser(data: unknown) {
// ... handler logic
}Manages message patterns and their handlers.
const handler = new RpcHandler();
handler.use(LoggingMiddleware); // Global middleware
handler.merge(getControllerHandler(MyController)); // Merge controllerHandles TCP connections and message routing.
const transport = new TcpTransport(rpcHandler, {
certFile: "./cert.pem", // Optional TLS
keyFile: "./key.pem", // Optional TLS
});
await transport.listen(3000);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);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);Controllers can accept dependencies through their constructors, enabling better testability and separation of concerns.
@Controller()
class MathController {
@MessagePattern("math.add")
add({ a, b }: { a: number; b: number }) {
return a + b;
}
}
// Auto-instantiation
rpcHandler.merge(getControllerHandler(MathController));// 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));- 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.
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) {
// ...
}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_PATTERN_REQUEST(0) - Request/response patternMESSAGE_PATTERN_EVENT(1) - Fire-and-forget event pattern
Enable TLS by providing certificate and key files:
const transport = new TcpTransport(rpcHandler, {
certFile: "./certs/server.crt",
keyFile: "./certs/server.key",
});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.tsEach RPC call creates a span with:
- Pattern name
- Request/Event type
- Parent trace context propagation
- Success/error status
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 }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
# 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.tsHandlers 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";
}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
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 downMIT
Contributions are welcome! Please feel free to submit a Pull Request.