Last active
July 31, 2025 12:36
-
-
Save colelawrence/36923c3d95a120da85df25ead0de83e5 to your computer and use it in GitHub Desktop.
Effect RPC (RpcServer.layerHttpRouter), HttpApi, HttpLayerRouter example with toWebHandler (with Bun.serve)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ============================================================================ | |
// 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