| 1 | --- |
| 2 | title: Layers |
| 3 | description: A Layer defines a service contract with swappable implementations. In Alchemy, a Layer can encapsulate Resources and Bindings, so shared services carry their own infrastructure and permissions. |
| 4 | --- |
| 5 | |
| 6 | As your application grows, you'll need to start sharing components of |
| 7 | your code using Layers. A |
| 8 | [**Layer**](https://effect.website/docs/requirements-management/layers/) |
| 9 | is Effect's unit of service construction: it defines how to build a |
| 10 | service, so you can swap different implementations of that service in |
| 11 | and out across your code — a different storage backend or a different |
| 12 | cloud. |
| 13 | |
| 14 | In Alchemy, [Resources](/infrastructure-as-code/resource) and |
| 15 | [Bindings](/infrastructure-as-effects/binding) can be encapsulated inside |
| 16 | a Layer, meaning you can create shareable services that carry their own |
| 17 | infrastructure and permissions. You can share these within your |
| 18 | codebase, across packages, or publish them to npm for others to consume. |
| 19 | |
| 20 | Let's look at how they work. |
| 21 | |
| 22 | ## The contract |
| 23 | |
| 24 | A service is a `Context.Service` — a typed tag that names a capability: |
| 25 | |
| 26 | ```typescript |
| 27 | import * as Alchemy from "alchemy"; |
| 28 | import * as Context from "effect/Context"; |
| 29 | import * as Effect from "effect/Effect"; |
| 30 | |
| 31 | export class JobService extends Context.Service< |
| 32 | JobService, |
| 33 | { |
| 34 | getJob(id: string): Effect.Effect<Job, JobError, Alchemy.RuntimeContext>; |
| 35 | } |
| 36 | >()("JobService") {} |
| 37 | ``` |
| 38 | |
| 39 | Consumers write `yield* JobService` and get this typed object. The |
| 40 | signature mentions `Alchemy.RuntimeContext` (covered below) and nothing |
| 41 | cloud-specific. |
| 42 | |
| 43 | ## The implementation |
| 44 | |
| 45 | `Layer.effect(JobService, …)` builds the service — "to construct |
| 46 | `JobService`, run this Effect": |
| 47 | |
| 48 | ```typescript |
| 49 | import * as Effect from "effect/Effect"; |
| 50 | import * as Layer from "effect/Layer"; |
| 51 | |
| 52 | export const JobServiceKV = Layer.effect( |
| 53 | JobService, |
| 54 | Effect.gen(function* () { |
| 55 | return { |
| 56 | getJob: Effect.fn(function* (id: string) { |
| 57 | // TODO: read the job from storage |
| 58 | }), |
| 59 | }; |
| 60 | }), |
| 61 | ); |
| 62 | ``` |
| 63 | |
| 64 | Declare the storage — a real KV namespace, owned by the Layer: |
| 65 | |
| 66 | ```diff lang="typescript" |
| 67 | +import * as Cloudflare from "alchemy/Cloudflare"; |
| 68 | import * as Effect from "effect/Effect"; |
| 69 | import * as Layer from "effect/Layer"; |
| 70 | |
| 71 | export const JobServiceKV = Layer.effect( |
| 72 | JobService, |
| 73 | Effect.gen(function* () { |
| 74 | + const MyKV = yield* Cloudflare.KV.Namespace("MyKV"); |
| 75 | + |
| 76 | return { |
| 77 | // ... |
| 78 | }; |
| 79 | }), |
| 80 | ); |
| 81 | ``` |
| 82 | |
| 83 | Bind it — `kv` is a typed client for reading and writing that namespace |
| 84 | at runtime: |
| 85 | |
| 86 | ```diff lang="typescript" |
| 87 | Effect.gen(function* () { |
| 88 | const MyKV = yield* Cloudflare.KV.Namespace("MyKV"); |
| 89 | + const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV); |
| 90 | |
| 91 | return { |
| 92 | // ... |
| 93 | }; |
| 94 | }), |
| 95 | ``` |
| 96 | |
| 97 | Implement the contract against the client: |
| 98 | |
| 99 | ```diff lang="typescript" |
| 100 | return { |
| 101 | getJob: Effect.fn(function* (id: string) { |
| 102 | - // TODO: read the job from storage |
| 103 | + return yield* kv.get<Job>(id, "json"); |
| 104 | }), |
| 105 | }; |
| 106 | ``` |
| 107 | |
| 108 | ## The consumer |
| 109 | |
| 110 | A Worker yields the service and provides the Layer: |
| 111 | |
| 112 | ```typescript |
| 113 | export default Cloudflare.Worker( |
| 114 | "Api", |
| 115 | { main: import.meta.url }, |
| 116 | Effect.gen(function* () { |
| 117 | const jobs = yield* JobService; |
| 118 | |
| 119 | return { |
| 120 | fetch: Effect.gen(function* () { |
| 121 | return yield* jobs.getJob("job-1"); |
| 122 | }), |
| 123 | }; |
| 124 | }).pipe( |
| 125 | Effect.provide( |
| 126 | JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)), |
| 127 | ), |
| 128 | ), |
| 129 | ); |
| 130 | ``` |
| 131 | |
| 132 | Providing the Layer is what brings the infrastructure in. On the next |
| 133 | deploy, the `MyKV` namespace is created in the |
| 134 | [Stack](/infrastructure-as-code/stack) and bound to this Worker, so |
| 135 | `kv.get` works inside its handlers. |
| 136 | |
| 137 | The inner `Layer.provide` satisfies `JobServiceKV`'s dependency on the KV |
| 138 | runtime binding privately. The handler sees only `JobService`: |
| 139 | |
| 140 | ```typescript |
| 141 | JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)) |
| 142 | ``` |
| 143 | |
| 144 | ## Reuse across Workers |
| 145 | |
| 146 | A Layer is a value. Provide `JobServiceKV` to a second Worker and both |
| 147 | share the same namespace, because `KV.Namespace("MyKV")` has a stable |
| 148 | logical id: |
| 149 | |
| 150 | ```typescript |
| 151 | // src/Admin.ts |
| 152 | export default Cloudflare.Worker( |
| 153 | "Admin", |
| 154 | { main: import.meta.url }, |
| 155 | Effect.gen(function* () { |
| 156 | const jobs = yield* JobService; |
| 157 | // ... |
| 158 | }).pipe( |
| 159 | Effect.provide( |
| 160 | JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)), |
| 161 | ), |
| 162 | ), |
| 163 | ); |
| 164 | ``` |
| 165 | |
| 166 | The Stack ends up with one `MyKV` namespace, bound to both `Api` and |
| 167 | `Admin`. |
| 168 | |
| 169 | ## Swapping implementations |
| 170 | |
| 171 | Changing the implementation is a one-line change: |
| 172 | |
| 173 | ```diff lang="typescript" |
| 174 | - JobServiceKV.pipe(Layer.provide(Cloudflare.KV.ReadWriteNamespaceBinding)), |
| 175 | + JobServiceDynamo.pipe(Layer.provide(AWS.DynamoDB.GetItemHttp)), |
| 176 | ``` |
| 177 | |
| 178 | This swap crossed clouds: the Worker still runs on Cloudflare, but its |
| 179 | jobs now come from a DynamoDB table on AWS, with the credentials and |
| 180 | permissions wired up automatically. |
| 181 | |
| 182 | Alchemy's own Bindings follow the same tag/Layer split |
| 183 | ([a contract and a Layer](/infrastructure-as-effects/binding#a-contract-and-a-layer)). |
| 184 | |
| 185 | ## The types hold the boundary |
| 186 | |
| 187 | A Layer that needs an environment the host can't supply is a compile |
| 188 | error: |
| 189 | |
| 190 | ```typescript |
| 191 | export default Cloudflare.Worker( |
| 192 | "Api", |
| 193 | { main: import.meta.url }, |
| 194 | handler.pipe(Effect.provide(JobServiceDynamo)), |
| 195 | // ✗ Type error: the Layer requires AWS `Credentials` and `Region`, |
| 196 | // which a Cloudflare Worker can't provide |
| 197 | ); |
| 198 | ``` |
| 199 | |
| 200 | And `Alchemy.RuntimeContext` marks methods that only work inside a |
| 201 | deployed handler ([Phases](/infrastructure-as-effects/phases)): |
| 202 | |
| 203 | ```typescript |
| 204 | Effect.gen(function* () { |
| 205 | const jobs = yield* JobService; |
| 206 | |
| 207 | yield* jobs.getJob("job-1"); |
| 208 | // ✗ Type error: `RuntimeContext` only exists inside handlers |
| 209 | |
| 210 | return { |
| 211 | fetch: Effect.gen(function* () { |
| 212 | return yield* jobs.getJob("job-1"); // ✓ |
| 213 | }), |
| 214 | }; |
| 215 | }); |
| 216 | ``` |
| 217 | |
| 218 | ## Composing Layers |
| 219 | |
| 220 | A typical app provides one Layer per capability: |
| 221 | |
| 222 | ```typescript |
| 223 | .pipe( |
| 224 | Effect.provide(Layer.mergeAll( |
| 225 | JobServiceKV, // provides JobService |
| 226 | BetterAuthD1, // provides BetterAuth |
| 227 | RateLimiterDurable, // provides RateLimiter |
| 228 | )), |
| 229 | ) |
| 230 | ``` |
| 231 | |
| 232 | Each Layer brings its own resources into the Stack and its own typed |
| 233 | service into scope. Use `Layer.mergeAll` for independent services and |
| 234 | `Layer.provide` to satisfy one Layer's dependencies privately from |
| 235 | another. |
| 236 | |
| 237 | ## Where next |
| 238 | |
| 239 | - [Circular Bindings](/infrastructure-as-effects/circular-bindings) — |
| 240 | the tag/Layer split applied to two services that reference each other. |
| 241 | - [Bindings](/infrastructure-as-effects/binding) — the deploy-time |
| 242 | mechanics Layers are built on. |