Shipping Effect v4

Notes from cutting over the template to effect-smol — what changed, what surprised us, and what we'd do again.

TT
The Team Engineering
1 min read

The template just landed on Effect v4 (effect-smol). Three things felt different from day one: services lost the .Default layer, models split Generated into two explicit variants, and HttpApi got a stable v4 surface.

Services use Context.Service

There is no Effect.Service in v4. The published package exports Context.Service only. Every service module pairs its tag with a Layer.effect(Tag, make) — no static .Default, no static .Test.

A concrete example

import { Context, Effect, Layer } from "effect"

export class Greeter extends Context.Service<Greeter, {
  readonly hello: (name: string) => Effect.Effect<string>
}>()("app/Greeter") {}

export const make = Effect.gen(function*() {
  return Greeter.of({
    hello: (name) =>
      Effect.succeed(`hello, ${name}`).pipe(
        Effect.withSpan("Greeter.hello", { attributes: { name } }),
      ),
  })
})

export const GreeterLive = Layer.effect(Greeter, make)

Models pick an explicit generator

Model.GeneratedByDb is for DB-assigned values (serial IDs, default timestamps). Model.GeneratedByApp is for application-generated values (crypto.randomUUID()). The old Model.Generated is gone.

HttpApi stabilised

HttpApi.make + HttpApiBuilder.group + HttpApiEndpoint.{get,post,delete} are the v4 surface. Note HttpApiEndpoint.delete — not .del. Endpoints declare success + failure schemas; the client is derived end-to-end.

The Effect v4 template hero illustration
The template's marketing surface — Effect v4 plus React plus Alchemy v2 on Cloudflare.

What we’d do again

Cut over in one pass. Trying to live in both v3 and v4 simultaneously costs more than the rewrite. The template is small enough that a single focused effort lands cleanly.