Skip to content

Repository files navigation

Effect Worker Monorepo

A production-ready monorepo for building Cloudflare Workers with Effect-TS, featuring shared domain models, type-safe API contracts, and database integration.

Quick Start

pnpm install # Install dependencies
pnpm build # Build all packages
pnpm check # Type check
pnpm test# Run tests# Local developmentcd apps/effect-worker-api
pnpm dev # Start dev server

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│ Applications │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│
│ │effect-worker-api │ │ react-app │ │ tanstack-start ││
│ │ (HTTP REST) │ │(SPA + RPC Worker)│ │ (Full-Stack UI) ││
│ └──────────────────┘ └──────────────────┘ └──────────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ Shared Packages │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ domain │ │ contracts │ │cloudflare │ │ db │ │
│ │ (types) │ │ (API) │ │ (infra) │ │ (schema) │ │
│ └───────────┘ └───────────┘ └───────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Packages

All packages use the @repo/* namespace for internal monorepo imports.

@repo/domain

Core domain schemas and errors (Effect Schema).

import{CreateUserSchema,UserNotFoundError}from"@repo/domain"// Schemas provide both compile-time types and runtime validation.typeCreateUser=typeofCreateUserSchema.Type

@repo/contracts

API definitions for HTTP and RPC endpoints. Defines the contract between client and server.

import{WorkerApi,UsersGroup,UsersRpc}from"@repo/contracts"

HTTP Groups:

  • HealthGroup - Health check endpoints
  • UsersGroup - User CRUD operations

RPC Procedures:

  • UsersRpc - User operations via RPC

@repo/cloudflare

Effectful, type-safe Cloudflare bindings. Keeps the wrangler types workflow (the typed env from cloudflare:workers) and adds an Effect SDK on top: bindings become yieldable, error-typed, and composable.

import{makeCloudflare}from"@repo/cloudflare"// Build cast-free accessors over your typed Env.const{ hyperdrive, r2, kv, queue }=makeCloudflare<Env>(()=>env)// In a handler:const{ connectionString }=yield*hyperdrive((e)=>e.HYPERDRIVE)

@repo/db

Drizzle ORM schema, the Database service tag, the request-scoped connect factory, and reusable Effect query programs.

import{users,Database,connect}from"@repo/db"// `Database` is provided per request via middleware; handlers just yield it.constdb=yield*Databaseconstrows=yield*db.select().from(users)

Applications

effect-worker-api

REST HTTP API built with @effect/platform.

cd apps/effect-worker-api
pnpm dev # Local dev server
pnpm deploy # Deploy to Cloudflare

Endpoints:

  • GET /health - Health check
  • GET /users - List users
  • GET /users/:id - Get user by ID
  • POST /users - Create user

react-app

React SPA (Vite + TanStack Router) with an Effect RPC server co-located in worker/. The Cloudflare Vite plugin runs the worker alongside the SPA, so the client calls the relative path /rpc — one app, one origin, no separate RPC service. The worker uses Effect RpcServer over the shared UsersRpc contract and the SPA queries it through @effect/atom-react (see src/atoms/).

cd apps/react-app
pnpm dev # SPA + RPC worker on one dev server (port 3001)
pnpm deploy # Build + deploy to Cloudflare

Endpoints:

  • POST /rpc - Effect RPC endpoint (UsersRpc: listUsers / getUser / createUser)

tanstack-start

Full-stack React application with TanStack Start, featuring Effect-TS integration via middleware.

cd apps/tanstack-start
pnpm dev # Local dev server (port 3000)
pnpm deploy # Deploy to Cloudflare

Features:

  • TanStack Router (file-based routing)
  • TanStack Query (server state management)
  • Effect runtime middleware for server functions
  • Tailwind CSS v4 + Shadcn/UI components

Effect Integration Pattern:

// Middleware creates scoped Effect runtime per-requestexportconsteffectRuntimeMiddleware=createMiddleware().server(async({ next })=>{constservicesLayer=Layer.mergeAll(/* your services */)returnEffect.runPromise(Effect.scoped(Effect.gen(function*(){construntime=yield*Layer.toRuntime(servicesLayer)construnEffect=<A,E>(effect: Effect.Effect<A,E,Services>)=>Runtime.runPromise(runtime)(effect)returnyield*Effect.tryPromise({try: ()=>next({context: { env, runEffect }}),catch: (e)=>{throwe}})})))})// Server functions use runEffect to execute Effect programsexportconstmyFunction=createServerFn().middleware([effectRuntimeMiddleware]).handler(async({ context })=>{returncontext.runEffect(Effect.gen(function*(){constdb=yield*Databasereturnyield*db.select().from(users)}))})

Core Patterns

Effectful bindings

Cloudflare bindings are accessed through @repo/cloudflare. makeCloudflare returns cast-free accessors backed by CloudflareEnv, a Context.Reference that defaults to the cloudflare:workers env — so reading a binding adds nothing to a handler's requirement channel:

const{ hyperdrive, r2, kv }=makeCloudflare<Env>(()=>env)Effect.gen(function*(){const{ connectionString }=yield*hyperdrive((e)=>e.HYPERDRIVE)// ...})

Request-scoped database via middleware

On Cloudflare the Postgres socket must be opened per request. Contracts define an abstract middleware tag that providesDatabase; apps implement it by opening a connection inside the request Scope and providing it downstream. The type system makes it impossible to ship a DB-using route without wiring it.

// In contracts (abstract): the tag declares what it provides.exportclassDatabaseMiddlewareextendsHttpApiMiddleware.Service<DatabaseMiddleware,{provides: Database}>()("@repo/api/DatabaseMiddleware",{error: DatabaseConnectionError}){}// In app (implementation): a function that wraps the downstream effect,// opens the request-scoped connection, and provides Database.exportconstDatabaseMiddlewareLive=Layer.succeed(DatabaseMiddleware,(httpEffect)=>Effect.gen(function*(){const{ connectionString }=yield*hyperdrive((e)=>e.HYPERDRIVE)constdb=yield*connect(connectionString).pipe(Effect.catch(()=>Effect.fail(newDatabaseConnectionError({message: "Database connection failed"}))))returnyield*httpEffect.pipe(Effect.provideService(Database,db))}))

Handler Implementation

Handlers yield Database directly (provided by the middleware above):

.handle("list",()=>Effect.gen(function*(){constdb=yield*Databasereturnyield*db.select().from(users)})).handle("get",({path: { id }})=>Effect.gen(function*(){constdb=yield*Databaseconst[user]=yield*db.select().from(users).where(eq(users.id,id))if(!user)returnyield*Effect.fail(newUserNotFoundError({ id,message: "Not found"}))returnuser}))

Error Handling

Typed errors with automatic HTTP status mapping via httpApiStatus:

exportclassUserNotFoundErrorextendsS.TaggedErrorClass<UserNotFoundError>()("UserNotFoundError",{id: S.Number,message: S.String},{httpApiStatus: 404}){}

Project Structure

effect-worker-mono/
├── apps/
│ ├── effect-worker-api/ # HTTP REST API
│ │ ├── src/
│ │ │ ├── index.ts # Worker entry point
│ │ │ ├── runtime.ts # Effect runtime
│ │ │ ├── handlers/ # Handler implementations
│ │ │ └── services/ # Middleware implementations
│ │ └── wrangler.jsonc # Cloudflare config
│ ├── react-app/ # React SPA + co-located Effect RPC worker
│ │ ├── src/ # SPA (atoms call /rpc via @effect/atom-react)
│ │ └── worker/ # Effect RpcServer (UsersRpc) served at /rpc
│ └── tanstack-start/ # Full-stack React app
│ ├── src/
│ │ ├── routes/ # File-based routes
│ │ ├── components/ # React components
│ │ └── server/ # Server-side code
│ │ ├── middleware/ # Effect runtime middleware
│ │ ├── functions/ # Server functions
│ │ └── types.ts # Effect service types
│ └── wrangler.jsonc # Cloudflare config
├── packages/
│ ├── domain/ # Domain types & schemas
│ │ └── src/
│ │ ├── schemas/ # Branded types
│ │ └── errors/ # Domain errors
│ ├── contracts/ # API definitions
│ │ └── src/
│ │ ├── http/ # HTTP endpoints
│ │ └── rpc/ # RPC procedures
│ ├── cloudflare/ # Effectful Cloudflare bindings
│ │ └── src/
│ │ ├── make.ts # makeCloudflare accessors
│ │ └── {r2,kv,queue}.ts # Per-binding effect wrappers
│ └── db/ # Database
│ └── src/
│ ├── schema.ts # Drizzle tables
│ ├── database.ts # Database service tag
│ ├── connect.ts # Request-scoped connection factory
│ └── queries/ # Reusable Effect query programs
└── designs/ # Design docs & architecture decisions

Configuration

TypeScript

Strict mode enabled with path aliases for all packages:

{
"compilerOptions": {
"paths": {
"@repo/domain": ["./packages/domain/src"],
"@repo/contracts": ["./packages/contracts/src"],
"@repo/cloudflare": ["./packages/cloudflare/src"],
"@repo/db": ["./packages/db/src"]
}
}
}

Cloudflare Bindings

Configure in wrangler.jsonc:

{
"kv_namespaces": [{ "binding": "MY_KV", "id": "xxx" }],
"r2_buckets": [{ "binding": "MY_R2", "bucket_name": "xxx" }],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "xxx" }]
}

Hyperdrive (Database Connection Pooling)

Cloudflare Hyperdrive provides connection pooling for PostgreSQL. In production, configure via wrangler.jsonc:

{
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "your-hyperdrive-id" }]
}

For local development, Wrangler simulates Hyperdrive using an environment variable. Create a .env file or export:

# .dev.vars (or export in shell)
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@localhost:5432/effect_worker"

The HYPERDRIVE suffix must match your binding name. Wrangler will automatically provide env.HYPERDRIVE.connectionString in your worker.

Usage in middleware:

constdb=yield*connect(env.HYPERDRIVE.connectionString)

Scripts

CommandDescription
pnpm buildBuild all packages
pnpm checkType check all packages
pnpm testRun all tests
pnpm coverageGenerate coverage report
pnpm cleanRemove dist folders

Tech Stack

CategoryTechnology
RuntimeCloudflare Workers
FrameworkEffect-TS v4 (effect@4.0.0-beta.70)
HTTPeffect/unstable/httpapi
RPCeffect/unstable/rpc
Full-Stack UITanStack Start + TanStack Router + TanStack Query
DatabaseDrizzle ORM (drizzle-orm/effect-postgres) + PostgreSQL
Buildpnpm workspaces + TypeScript
TestingVitest + @effect/vitest
DeploymentWrangler

Key Dependencies

  • effect (4.0.0-beta.70) - Core runtime; HTTP/RPC live under effect/unstable/*
  • @effect/sql-pg - PostgreSQL client (underlies the Drizzle connection)
  • @effect/atom-react - Reactive state for the React SPA
  • drizzle-orm (1.0.0-rc.3) - Type-safe ORM; drizzle-orm/effect-postgres client
  • @tanstack/react-start - Full-stack React framework
  • @tanstack/react-router - Type-safe file-based routing
  • @tanstack/react-query - Server state management
  • wrangler - Cloudflare Workers CLI

Development Workflow

  1. Make changes to packages or apps
  2. Build packages if contract/domain/infra changed: pnpm build
  3. Type check: pnpm check
  4. Run tests: pnpm test
  5. Dev server: cd apps/effect-worker-api && pnpm dev
  6. Deploy: pnpm deploy

Database Operations

cd packages/db
# Push schema changes
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:push
# Open Drizzle Studio
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:studio
# Generate migrations
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:generate
# Run migrations
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:migrate

License

See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

129 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages