Skip to content

Instantly share code, notes, and snippets.

@colelawrence
Last active July 31, 2025 12:36
Show Gist options
  • Save colelawrence/36923c3d95a120da85df25ead0de83e5 to your computer and use it in GitHub Desktop.
Save colelawrence/36923c3d95a120da85df25ead0de83e5 to your computer and use it in GitHub Desktop.
Effect RPC (RpcServer.layerHttpRouter), HttpApi, HttpLayerRouter example with toWebHandler (with Bun.serve)
// ============================================================================
// PACKAGE VERSIONS (2025-07-30)
// ============================================================================
//
// Dependencies:
// - effect: 3.17.3
// - @effect/platform: 0.90.0
// - @effect/rpc: 0.68.0
//
// ============================================================================
// KEY EFFECT APIs DEMONSTRATED
// ============================================================================
//
// * Effect.Service - Service definition with scoped lifecycle
// * Effect.gen - Generator-based effect composition
// * Ref - Mutable references for state management
// * Effect.addFinalizer - Resource cleanup and finalization
// * Schema.Class - Type-safe data serialization
// * RpcGroup.make & Rpc.make - RPC service definitions
// * HttpApi.make & HttpApiGroup.make - HTTP API definitions
// * HttpApiBuilder.group - HTTP API implementation
// * RpcServer.layerHttpRouter - RPC routing setup
// * HttpLayerRouter - HTTP routing and middleware
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpLayerRouter, HttpServer } from "@effect/platform";
import { Rpc, RpcGroup, RpcSerialization, RpcServer } from "@effect/rpc";
import { Effect, Layer, Ref, Schema } from "effect";
const colors = {
cyan: "\x1b[36m",
red: "\x1b[31m",
yellow: "\x1b[33m",
reset: "\x1b[0m",
};
class TestService extends Effect.Service<TestService>()("TestService", {
scoped: Effect.gen(function* () {
const isActiveRef = yield* Ref.make<unknown>("active");
// let isActive: unknown = true;
yield* Effect.addFinalizer((exit) => {
console.log(colors.cyan + "TestService finalized" + colors.reset, exit);
return isActiveRef.modify(() => [void 0, exit]);
});
return {
active: isActiveRef,
};
}),
}) {}
// ============================================================================
// SHARED SCHEMA
// ============================================================================
export class User extends Schema.Class<User>("User")({
id: Schema.String,
name: Schema.String,
}) {}
// ============================================================================
// RPC DEFINITION AND IMPLEMENTATION
// ============================================================================
// Define a group of RPCs
export class UserRpcs extends RpcGroup.make(
Rpc.make("UserById", {
success: User,
error: Schema.String, // Indicates that errors, if any, will be returned as strings
payload: { id: Schema.String },
}),
) {}
const UsersLive = UserRpcs.toLayer({
UserById: ({ id }) =>
Effect.gen(function* () {
yield* Effect.sleep("0.1 seconds");
yield* Effect.allowInterrupt;
return new User({ id, name: "John Doe" });
}),
});
// ============================================================================
// HTTPAPI DEFINITION
// ============================================================================
const MyApi = HttpApi.make("MyApi")
.add(
HttpApiGroup.make("Users")
.add(HttpApiEndpoint.get("getUsers", "/users").addSuccess(Schema.Array(User)))
.add(
HttpApiEndpoint.get("getUser", "/users/:id")
.setPath(Schema.Struct({ id: Schema.String }))
.addSuccess(User),
)
.add(
HttpApiEndpoint.post("createUser", "/users")
.setPayload(Schema.Struct({ name: Schema.String }))
.addSuccess(User),
),
)
.add(HttpApiGroup.make("Tests").add(HttpApiEndpoint.post("testService", "/test-service").addSuccess(Schema.String)));
// Implement the Users API
const UsersApiLive = HttpApiBuilder.group(MyApi, "Users", (handlers) =>
handlers
.handle("getUsers", () =>
Effect.succeed([new User({ id: "1", name: "Alice" }), new User({ id: "2", name: "Bob" })]),
)
.handle("getUser", ({ path }) => Effect.succeed(new User({ id: path.id, name: "Found User" })))
.handle("createUser", ({ payload }) => Effect.succeed(new User({ id: "new-id", name: payload.name }))),
);
// Implement the Users API
const TestApiLive = HttpApiBuilder.group(MyApi, "Tests", (handlers) =>
handlers.handle("testService", () =>
Effect.gen(function* () {
const service = yield* TestService;
const current = yield* service.active.get;
yield* Effect.log(colors.cyan + "Service.active = " + colors.reset + current);
return JSON.stringify(current);
}),
),
).pipe(Layer.provide(TestService.Default));
// ============================================================================
// COMBINED ROUTES SETUP
// ============================================================================
// RPC routes
const MyRpcRoutes = RpcServer.layerHttpRouter({
group: UserRpcs,
path: "/rpc",
protocol: "http",
}).pipe(Layer.provide(Layer.mergeAll(UsersLive, RpcSerialization.layerJson)));
// HttpApi routes
const MyHttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, {
openapiPath: "/docs/openapi.json",
}).pipe(
Layer.provide(UsersApiLive),
Layer.provide(TestApiLive),
// Important: provide platform dependencies
Layer.provide(HttpServer.layerContext),
);
// Combine all routes into one layer
const MyAllRoutes = Layer.mergeAll(MyRpcRoutes, MyHttpApiRoutes).pipe(Layer.provide(HttpLayerRouter.cors()));
// Create web handler for the combined routes
const { handler: myHandler, dispose: myDispose } = HttpLayerRouter.toWebHandler(MyAllRoutes);
const myServer = Bun.serve({
port: 3010,
fetch: (request) => myHandler(request),
});
console.log(`Combined server running at ${myServer.url}`);
console.log("Available endpoints:");
console.log("- RPC: POST /rpc");
console.log("- HttpApi: GET /users, GET /users/:id, POST /users");
console.log("- Docs: GET /docs/openapi.json");
// ============================================================================
// CLIENT TESTS
// ============================================================================
setTimeout(async () => {
try {
console.log(colors.yellow + "-- 1 --" + colors.reset);
// Test RPC endpoint
await fetch("http://localhost:3010/rpc", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
_tag: "Request",
id: "123",
tag: "UserById",
payload: { id: "123" },
traceId: "traceId",
spanId: "spanId",
sampled: true,
headers: {},
}),
})
.then(async (response) => {
if (!response.ok) throw `HTTP error! status: ${response.status}`;
const data = await response.json();
console.log("RPC Response:", data);
})
.catch((error) => console.error("RPC Error:", error));
/*
timestamp=2025-07-30T02:34:01.931Z level=INFO fiber=#16 message="Sent HTTP response" http.span.1=109ms http.status=200 http.method=POST http.url=/rpc
RPC Response: [
{
_tag: "Exit",
requestId: "123",
exit: {
_tag: "Success",
value: {
id: "123",
name: "John Doe",
},
},
}
]
*/
console.log(colors.yellow + "-- 2 --" + colors.reset);
// Test GET /users endpoint
await fetch("http://localhost:3010/users", {
method: "GET",
headers: { Accept: "application/json" },
})
.then(async (response) => {
if (!response.ok) throw `HTTP error! status: ${response.status}`;
const data = await response.json();
console.log("HttpApi GET /users Response:", data);
})
.catch((error) => console.error("HttpApi GET /users Error:", error));
/*
timestamp=2025-07-30T02:34:01.935Z level=INFO fiber=#20 message="Sent HTTP response" http.span.2=2ms http.status=200 http.method=GET http.url=/users
HttpApi GET /users Response: [
{
id: "1",
name: "Alice",
}, {
id: "2",
name: "Bob",
}
]
*/
console.log(colors.yellow + "-- 3 --" + colors.reset);
// Test POST /users endpoint
await fetch("http://localhost:3010/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ name: "Charlie" }),
})
.then(async (response) => {
if (!response.ok) throw `HTTP error! status: ${response.status}`;
const data = await response.json();
console.log("HttpApi POST /users Response:", data);
})
.catch((error) => console.error("HttpApi POST /users Error:", error));
/*
timestamp=2025-07-30T02:34:01.936Z level=INFO fiber=#21 message="Sent HTTP response" http.span.3=1ms http.status=200 http.method=POST http.url=/users
HttpApi POST /users Response: {
id: "new-id",
name: "Charlie",
}
*/
console.log(colors.yellow + "-- 4 --" + colors.reset);
// Test service endpoint
const testService = () =>
fetch("http://localhost:3010/test-service", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
})
.then(async (response) => {
if (!response.ok) throw `HTTP error! status: ${response.status}`;
const data = await response.json();
console.log("HttpApi POST /test-service Response:", data);
})
.catch((error) => console.error("HttpApi POST /test-service Error:", error));
await testService();
/*
timestamp=2025-07-30T02:34:01.937Z level=INFO fiber=#23 message="Service.active = active" http.span.4=0ms
timestamp=2025-07-30T02:34:01.938Z level=INFO fiber=#23 message="Sent HTTP response" http.span.4=1ms http.status=200 http.method=POST http.url=/test-service
HttpApi POST /test-service Response: "active"
*/
console.log(colors.yellow + "-- 5 --" + colors.reset);
await myDispose();
/*
TestService finalized {
_id: "Exit",
_tag: "Success",
value: undefined,
}
*/
console.log(colors.yellow + "-- 6 --" + colors.reset);
await testService();
/*
timestamp=2025-07-30T02:34:01.942Z level=INFO fiber=#25 message="Service.active = {
\"_id\": \"Exit\",
\"_tag\": \"Success\"
}" http.span.5=0ms
timestamp=2025-07-30T02:34:01.943Z level=INFO fiber=#25 message="Sent HTTP response" http.span.5=1ms http.status=200 http.method=POST http.url=/test-service
HttpApi POST /test-service Response: {"_id":"Exit","_tag":"Success"}
*/
} catch (error) {
console.error("Error!", error);
}
}, 1000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment