tech

Effect-ification end to end

Effect is taking over the TypeScript ecosystem, now faster than ever before. I have been running effect everywhere for a while: here is what you will get on the other side once all becomes effect.


Sandro Maglione

Sandro Maglione

Software

Effect is ramping-up. Like a lot, with AI agents helping 🚀

I am already on the other side, where (nearly) everything in my codebases is based on effect ⚡️

Here is how your future looks like if you go effect 👇


Effect end to end

The major win is when effect runs your client and your server, and those are connected with effect.

I always go with a monorepo, with usually a client and server app, and a shared api package 🤝

api defines shared Schema (as strict and refined as possible) and export an HttpApi.

api has a single dependency: effect (beta) 💡

Both client and server share the same single source of truth, and effect derives all the http clients for you:

import { AuthMiddleware, ServerApi } from "@app/api"; // 👈 Shared API

import { Config, Context, Effect, Layer } from "effect";
import { FetchHttpClient, HttpClientRequest } from "effect/unstable/http";
import { HttpApiClient, HttpApiMiddleware } from "effect/unstable/httpapi";

class CurrentAccessToken extends Context.Service<CurrentAccessToken, string>()(
  "CurrentAccessToken"
) {}

const AuthMiddlewareClient = HttpApiMiddleware.layerClient(
  AuthMiddleware,
  Effect.fn(function* ({ next, request }) {
    const token = yield* CurrentAccessToken;
    return yield* next(HttpClientRequest.bearerToken(request, token));
  })
);

export class ApiClient extends Context.Service<ApiClient>()("ApiClient", {
  make: Effect.gen(function* () {
    const baseUrl = yield* Config.string("API_BASE_URL");

    return {
      withAccessToken: <A, E, R>(
        f: (
          api: HttpApiClient.ForApi<typeof ServerApi>
        ) => Effect.Effect<A, E, R>,
        accessToken: string
      ): Effect.Effect<A, E, Exclude<R, CurrentAccessToken>> =>
        Effect.gen(function* () {
          const api = yield* HttpApiClient.make(ServerApi, { baseUrl });
          return yield* f(api);
        }).pipe(
          Effect.provide(
            Layer.mergeAll(AuthMiddlewareClient, FetchHttpClient.layer),
          ),
          Effect.provideService(CurrentAccessToken, accessToken),
        ),
    };
  }),
}) {
  static readonly layer = Layer.effect(this, this.make);
}

Keep your api definition clean, and watch everything else in the app just work as a consequence, guided by types.

Backend: all you need is effect

server implements the HttpApi from api. And all it needs, again, it's mostly effect 💁🏼‍♂️

Effect has wrappers for all major databases, AI APIs, and more, no custom implementation needed 🙌

Example: I am using Cloudflare, D1 as database, and my app calls AI. I use @effect/sql-d1 and @effect/ai-openai-compat.

But you are not required to go effect packages only. For example, I use Clerk for authentication, I can just bring @clerk/backend and wrap it with effect to make it work:

import { createClerkClient } from "@clerk/backend";
import { Config, Context, Effect, Layer, Redacted, Schema } from "effect";

class ClerkError extends Schema.TaggedErrorClass<ClerkError>()("ClerkError", {
  cause: Schema.Unknown,
}) {}

export class Clerk extends Context.Service<Clerk>()("Clerk", {
  make: Effect.gen(function* () {
    const { publishableKey, secretKey } = yield* Config.all({
      publishableKey: Config.redacted("CLERK_PUBLISHABLE_KEY"),
      secretKey: Config.redacted("CLERK_SECRET_KEY"),
    });

    const clerkClient = createClerkClient({
      publishableKey: Redacted.value(publishableKey),
      secretKey: Redacted.value(secretKey),
    });

    return {
      authenticateRequest: (request: globalThis.Request) =>
        Effect.tryPromise({
          try: () => clerkClient.authenticateRequest(request),
          catch: (error) => new ClerkError({ cause: error }),
        }).pipe(
          Effect.timeout("5 seconds"),
          Effect.catchTag("TimeoutError", (cause) =>
            new ClerkError({ cause }).asEffect()
          )
        ),
    };
  }),
}) {
  static readonly layer = Layer.effect(this, this.make);
}

That's what it means to "Effectify" your codebase: effect provides most of it, but it also allows to bring your own 🤝

And now watch as major providers start bringing effect-native integrations (starting with Drizzle) 🤝

Effect also on the client, no matter what client

With effect v4 the effect-client story becomes even more appealing, with reduced bundle size and increased speed.

Effect on the client is the same as the backend, same logic, different runtime 💁🏼‍♂️

Same as server, also on the client I have a services folder with all the logic wrapped into effects. And the modules are abundant also on the client:

  • HttpClient (no need fetch)
  • Reactivity (syncing between components, in effect)
  • IndexedDb (the right way)

A possible new addition with state machines is also on its way here 👀

Effect's Atom is there as well to solve most of your state management hurdles.

Result: all you need for business logic on the client is 1 dependency (effect) 🪄


Effect end to end, monorepo, AI agents work great with it, all type checked. Add a few more guardrails with the LSP and some custom linting rules, and just watch everything "just work" ☑️

See you next 👋

Start here.

Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights

Not convinced? Well, let me tell you more about it