@elto/erpc
v0.1.1
Published
Effect RPC helpers for client, server, and React Query
Maintainers
Readme
@elto/erpc
Opinionated Effect RPC helpers for building type-safe client-server communication with optional React Query integration.
Features
- 🚀 Type-safe RPC - Built on
@effect/rpcwith full TypeScript support - 🔄 Streaming support - NDJSON serialization by default for efficient streaming
- 🌐 Framework agnostic - Works with Hono, Express, Fastify, Bun, Deno, and more
- ⚛️ React Query integration - Optional hooks for React applications
- 🔌 Middleware support - Easy header injection and request transformation
- 📦 Tree-shakeable - Subpath exports for client, server, and React
Installation
npm install @elto/erpc effect @effect/rpc @effect/platform
# or
pnpm add @elto/erpc effect @effect/rpc @effect/platformFor React Query support:
npm install react @tanstack/react-queryQuick Start
Define your RPC API (shared package)
import { Rpc, RpcGroup } from "@elto/erpc";
import { Schema } from "effect";
// Define schemas
export class SystemStatus extends Schema.Class<SystemStatus>("SystemStatus")({
version: Schema.String,
uptime: Schema.Number,
}) {}
export class Note extends Schema.Class<Note>("Note")({
id: Schema.String,
title: Schema.String,
body: Schema.String,
}) {}
// Define RPC group
export class ApiRpcs extends RpcGroup.make(
Rpc.make("Status", { success: SystemStatus }),
Rpc.make("NoteList", { success: Note, stream: true }),
Rpc.make("NoteCreate", {
success: Note,
payload: { title: Schema.String, body: Schema.String },
}),
) {}Server Setup
import { createRpcWebHandler, defaultSerializationLayer } from "@elto/erpc/server";
import { Effect, Layer, Stream } from "effect";
import { ApiRpcs, SystemStatus, Note } from "@myapp/api";
// Implement handlers
const ApiHandlersLive = ApiRpcs.toLayer(Effect.gen(function* () {
return {
Status: () => Effect.succeed(SystemStatus.make({ version: "1.0.0", uptime: 123 })),
NoteList: () => Stream.fromIterable(notes),
NoteCreate: ({ title, body }) => Effect.succeed(Note.make({ id: "1", title, body })),
};
}));
// Create handler
const AppLayer = ApiHandlersLive.pipe(
Layer.provideMerge(defaultSerializationLayer),
);
const { handler, managedRuntime } = createRpcWebHandler({
group: ApiRpcs,
layer: AppLayer,
});
// Use with Hono
app.post("/rpc/*", (c) => handler(c.req.raw));
// Cleanup on shutdown
process.on("SIGTERM", () => managedRuntime.dispose());Client Setup
import {
createRpcHttpClientLayer,
createRpcClientRunner,
collectStream
} from "@elto/erpc/client";
import { ApiRpcs } from "@myapp/api";
// Create client layer
const RpcClientLive = createRpcHttpClientLayer({
url: "http://localhost:3000/rpc",
});
// Create runner
const runWithApi = createRpcClientRunner(ApiRpcs, RpcClientLive);
// Use it
const status = await runWithApi(client => client.Status());
const notes = await runWithApi(client => collectStream(client.NoteList()));Serialization Options
The package defaults to NDJSON (Newline-Delimited JSON) for optimal streaming support. You can change this:
// Client
const RpcClientLive = createRpcHttpClientLayer({
url: "http://localhost:3000/rpc",
serialization: "json", // Standard JSON
// serialization: "ndjson", // Default - best for streaming
// serialization: "msgpack", // Binary format
});
// Server with explicit serialization
import { createRpcWebHandlerWithSerialization } from "@elto/erpc/server";
const { handler } = createRpcWebHandlerWithSerialization({
group: ApiRpcs,
handlers: ApiHandlersLive,
serialization: "json",
});Client Middleware
Inject headers or transform requests:
import { makeClientMiddleware, makeHeaderMiddleware } from "@elto/erpc/client";
import { RpcMiddleware } from "@elto/erpc";
import { Headers } from "@effect/platform";
import { Context, Effect, Layer } from "effect";
// Define context and middleware tag
class AuthContext extends Context.Tag("@app/AuthContext")<
AuthContext,
{ readonly token: string }
>() {}
class AuthMiddleware extends RpcMiddleware.Tag<AuthMiddleware>()(
"AuthMiddleware",
{ provides: AuthContext, requiredForClient: true }
) {}
// Full control middleware
const AuthMiddlewareLive = makeClientMiddleware(
AuthMiddleware,
({ request }) => Effect.gen(function* () {
const token = yield* getAuthToken();
return {
...request,
headers: Headers.set(request.headers, "Authorization", `Bearer ${token}`),
};
})
);
// Simple header injection
const RequestIdMiddlewareLive = makeHeaderMiddleware(
RequestIdMiddleware,
() => ({ "X-Request-ID": crypto.randomUUID() })
);
// Compose with client layer
const RpcClientLive = createRpcHttpClientLayer({ url: "http://localhost:3000/rpc" }).pipe(
Layer.provideMerge(AuthMiddlewareLive),
Layer.provideMerge(RequestIdMiddlewareLive),
);React Query Integration
The /react export provides React Query helpers:
import { RpcQueryProvider, createRpcQueryHooks, formatRpcError } from "@elto/erpc/react";
import { useQueryClient } from "@tanstack/react-query";
// Create hooks from your client
const { useRpcQuery, useRpcMutation, queryKeys } = createRpcQueryHooks({
client: rpcClient,
});
// Provider setup
function App() {
return (
<RpcQueryProvider>
<NotesPage />
</RpcQueryProvider>
);
}
// Use in components
function NotesPage() {
const queryClient = useQueryClient();
const { data: notes, error, isLoading } = useRpcQuery("notes", []);
const createNote = useRpcMutation("noteCreate", {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.key("notes") });
},
});
if (error) {
return <div>Error: {formatRpcError(error)}</div>;
}
return (
<div>
{notes?.map(note => <NoteCard key={note.id} note={note} />)}
<button onClick={() => createNote.mutate({ title: "New", body: "Content" })}>
Add Note
</button>
</div>
);
}Framework Examples
Hono
import { Hono } from "hono";
import { createRpcWebHandler } from "@elto/erpc/server";
const app = new Hono();
const { handler } = createRpcWebHandler({ group: ApiRpcs, layer: AppLayer });
app.post("/rpc/*", (c) => handler(c.req.raw));Express
import express from "express";
import { createRpcWebHandler } from "@elto/erpc/server";
const app = express();
const { handler } = createRpcWebHandler({ group: ApiRpcs, layer: AppLayer });
// Using express-to-web-api adapter
app.post("/rpc/*", async (req, res) => {
const webRequest = toWebRequest(req);
const webResponse = await handler(webRequest);
await sendWebResponse(webResponse, res);
});Fastify
import Fastify from "fastify";
import { createRpcWebHandler } from "@elto/erpc/server";
const fastify = Fastify();
const { handler } = createRpcWebHandler({ group: ApiRpcs, layer: AppLayer });
fastify.post("/rpc/*", async (request, reply) => {
const webRequest = toWebRequest(request);
const response = await handler(webRequest);
return reply.send(response);
});Bun / Deno
import { createRpcWebHandler } from "@elto/erpc/server";
const { handler } = createRpcWebHandler({ group: ApiRpcs, layer: AppLayer });
Bun.serve({
port: 3000,
fetch: (req) => {
if (req.url.includes("/rpc")) {
return handler(req);
}
return new Response("Not found", { status: 404 });
},
});API Reference
Client (@elto/erpc/client)
| Export | Description |
| -------------------------- | ----------------------------------------------------------- |
| createRpcHttpClientLayer | Creates an RPC client layer with configurable serialization |
| createRpcClientRunner | Creates a reusable runner function for RPC calls |
| withRpcClient | Executes an effect with an RPC client in scope |
| runWithRpcClient | Executes and returns a Promise |
| collectStream | Collects a stream into an array |
| makeClientMiddleware | Creates client-side middleware for request transformation |
| makeHeaderMiddleware | Simple header injection middleware |
| getSerializationLayer | Gets serialization layer for a format |
Server (@elto/erpc/server)
| Export | Description |
| -------------------------------------- | ------------------------------------------------- |
| createRpcWebHandler | Creates a Web API-compatible request handler |
| createRpcWebHandlerWithSerialization | Handler with explicit serialization config |
| createRpcHttpApp | Creates an HTTP application from an RPC group |
| makeHandlerRuntime | Creates a managed runtime for custom integrations |
| getRuntimeFromManaged | Extracts runtime from managed runtime |
| getSerializationLayer | Gets serialization layer for a format |
React (@elto/erpc/react)
| Export | Description |
| ----------------------- | ------------------------------------------ |
| RpcQueryProvider | React Query provider component |
| createRpcQueryHooks | Factory for type-safe query/mutation hooks |
| createQueryKeyFactory | Creates organized query key factories |
| formatRpcError | Formats RPC errors for display |
| rpcQueryClient | Default QueryClient instance |
| createRpcQueryClient | Creates new QueryClient instance |
License
Apache-2.0
