Effect RPC on Cloud Run
Effect RPC gives you procedures instead of URLs, and a typed client with no codegen. Every request is checked against a schema, so use it where a web app or another service calls into yours. The concept lives at Effect RPC. This page wires it onto Cloud Run.
Define the schemas
Section titled “Define the schemas”import * as Schema from "effect/Schema";
export const JobId = Schema.String;
export class Job extends Schema.Class<Job>("Job")({ id: JobId, content: Schema.String,}) {}
export class JobNotFound extends Schema.TaggedClass<JobNotFound>()( "JobNotFound", { jobId: JobId },) {}
export class PutJobFailed extends Schema.TaggedClass<PutJobFailed>()( "PutJobFailed", { reason: Schema.String },) {}The client receives these errors as typed values it can catchTag.
Declare the procedures
Section titled “Declare the procedures”import * as Schema from "effect/Schema";import { Rpc, RpcGroup } from "effect/rpc";import { Job, JobId, JobNotFound, PutJobFailed } from "./Job.ts";
const getJob = Rpc.make("getJob", { success: Job, error: JobNotFound, payload: { jobId: JobId },});
const createJob = Rpc.make("createJob", { success: JobId, error: PutJobFailed, payload: { content: Schema.String },});
export class JobRpcs extends RpcGroup.make(getJob, createJob) {}Implement it on Cloud Run
Section titled “Implement it on Cloud Run”RpcServer.toHttpEffect turns the group into a fetch handler:
import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { RpcSerialization, RpcServer } from "effect/rpc";import { Job, JobNotFound, PutJobFailed } from "./Job.ts";import { JobRpcs } from "./JobRpcs.ts";
export default class JobService extends GCP.Function<JobService>()( "JobService", { main: import.meta.url, invokerIamDisabled: true }, Effect.gen(function* () { const jobs = yield* GCP.Firestore.Database("Jobs", { type: "FIRESTORE_NATIVE", }); const db = yield* GCP.Firestore.ReadWriteDatabase(jobs);
const handlersLayer = JobRpcs.toLayer({ getJob: ({ jobId }) => db.get(`jobs/${jobId}`).pipe( Effect.orDie, Effect.flatMap((doc) => doc ? Effect.succeed( new Job({ id: jobId, content: String(doc.fields.content ?? "") }), ) : Effect.fail(new JobNotFound({ jobId })), ), ), createJob: ({ content }) => Effect.gen(function* () { const jobId = crypto.randomUUID(); yield* db.set(`jobs/${jobId}`, { content }); return jobId; }).pipe( Effect.mapError((error) => new PutJobFailed({ reason: error._tag })), ), });
return { fetch: yield* RpcServer.toHttpEffect(JobRpcs).pipe( Effect.provide(Layer.mergeAll(handlersLayer, RpcSerialization.layerJson)), ), }; }).pipe(Effect.provide(GCP.Firestore.ReadWriteDatabaseHttp)),) {}getJob turns Firestore failures into 500s and keeps JobNotFound
for the client. createJob maps them to the declared PutJobFailed.
Add it to the Stack
Section titled “Add it to the Stack”import * as Alchemy from "alchemy";import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import JobService from "./src/JobService.ts";
export default Alchemy.Stack( "JobRpc", { providers: GCP.providers(), state: GCP.state() }, Effect.gen(function* () { const service = yield* JobService; return { url: service.uri }; }),);bun alchemy deployCall it from a typed client
Section titled “Call it from a typed client”import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as FetchHttpClient from "effect/http/FetchHttpClient";import { RpcClient, RpcSerialization } from "effect/rpc";import { JobRpcs } from "../src/JobRpcs.ts";
const program = Effect.gen(function* () { const client = yield* RpcClient.make(JobRpcs);
const jobId = yield* client.createJob({ content: "Ship the docs" }); const job = yield* client.getJob({ jobId }); yield* Effect.log(job.content);});
Effect.runPromise( program.pipe( Effect.scoped, Effect.provide( RpcClient.layerProtocolHttp({ url: process.env.JOB_RPC_URL! }).pipe( Layer.provide(FetchHttpClient.layer), Layer.provide(RpcSerialization.layerJson), ), ), ),);JOB_RPC_URL=https://<service>.run.app bun scripts/client.tsChoosing an API style
Section titled “Choosing an API style”Use Effect RPC when every caller is Effect. Use Effect HTTP when callers want plain REST. The RPC overview has the full decision.
Where next
Section titled “Where next”- Effect RPC — procedures, streaming, and clients in depth.
- Private services — keep the RPC service private and call it from another service.