diff --git a/src/content/changelog/workers/2025-03-17-importable-env.mdx b/src/content/changelog/workers/2025-03-17-importable-env.mdx new file mode 100644 index 000000000000000..7282d14bdc3b70c --- /dev/null +++ b/src/content/changelog/workers/2025-03-17-importable-env.mdx @@ -0,0 +1,67 @@ +--- +title: Import `env` to access bindings in your Worker's global scope +description: More easily configure your Worker and call bindings from anywhere with an importable `env` +products: + - workers +date: 2025-03-17T15:00:00Z +--- + +import { Render, TypeScriptExample } from "~/components"; + +You can now access [bindings](/workers/runtime-apis/bindings/) +from anywhere in your Worker by importing the `env` object from `cloudflare:workers`. + +Previously, `env` could only be accessed during a request. This meant that +bindings could not be used in the top-level context of a Worker. + +Now, you can import `env` and access bindings such as [secrets](/workers/configuration/secrets/) +or [environment variables](/workers/configuration/environment-variables/) in the +initial setup for your Worker: + +```js +import { env } from "cloudflare:workers"; +import ApiClient from "example-api-client"; + +// API_KEY and LOG_LEVEL now usable in top-level scope +const apiClient = ApiClient.new({ apiKey: env.API_KEY }); +const LOG_LEVEL = env.LOG_LEVEL || "info"; + +export default { + fetch(req) { + // you can use apiClient or LOG_LEVEL, configured before any request is handled + }, +}; +``` + +:::note +Workers do not allow I/O from outside a request context. This means that even +though `env` is accessible from the top-level scope, you will not be able to access +every binding's methods. + +For instance, environment variables and secrets are accessible, and you are able to +call `env.NAMESPACE.get` to get a [Durable Object stub](/durable-objects/api/stub/) in the +top-level context. However, calling methods on the Durable Object stub, making [calls to a KV store](/kv/api/), +and [calling to other Workers](/workers/runtime-apis/bindings/service-bindings) will not work. +::: + +Additionally, `env` was normally accessed as a argument to a Worker's entrypoint handler, +such as [`fetch`](/workers/runtime-apis/fetch/). +This meant that if you needed to access a binding from a deeply nested function, +you had to pass `env` as an argument through many functions to get it to the +right spot. This could be cumbersome in complex codebases. + +Now, you can access the bindings from anywhere in your codebase +without passing `env` as an argument: + +```js +// helpers.js +import { env } from "cloudflare:workers"; + +// env is *not* an argument to this function +export async function getValue(key) { + let prefix = env.KV_PREFIX; + return await env.KV.get(`${prefix}-${key}`); +} +``` + +For more information, see [documentation on accessing `env`](/workers/runtime-apis/bindings#how-to-access-env). diff --git a/src/content/docs/workers/runtime-apis/bindings/index.mdx b/src/content/docs/workers/runtime-apis/bindings/index.mdx index e33f1928f396bed..7e27c17b05268cf 100644 --- a/src/content/docs/workers/runtime-apis/bindings/index.mdx +++ b/src/content/docs/workers/runtime-apis/bindings/index.mdx @@ -3,10 +3,9 @@ pcx_content_type: concept title: Bindings (env) head: [] description: Worker Bindings that allow for interaction with other Cloudflare Resources. - --- -import { DirectoryListing, WranglerConfig } from "~/components" +import { DirectoryListing, WranglerConfig } from "~/components"; Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform. Bindings provide better performance and less restrictions when accessing resources from Workers than the [REST APIs](/api/) which are intended for non-Workers applications. @@ -31,12 +30,12 @@ r2_buckets = [ ```js export default { - async fetch(request, env) { - const key = url.pathname.slice(1); - await env.MY_BUCKET.put(key, request.body); - return new Response(`Put ${key} successfully!`); - } -} + async fetch(request, env) { + const key = url.pathname.slice(1); + await env.MY_BUCKET.put(key, request.body); + return new Response(`Put ${key} successfully!`); + }, +}; ``` You can think of a binding as a permission and an API in one piece. With bindings, you never have to add secret keys or tokens to your Worker in order to access resources on your Cloudflare account — the permission is embedded within the API itself. The underlying secret is never exposed to your Worker's code, and therefore can't be accidentally leaked. @@ -51,12 +50,12 @@ The following is a good approach: ```ts export default { - fetch(request, env) { - let client = new Client(env.MY_SECRET); // `client` is guaranteed to be up-to-date with the latest value of `env.MY_SECRET` since a new instance is constructed with every incoming request + fetch(request, env) { + let client = new Client(env.MY_SECRET); // `client` is guaranteed to be up-to-date with the latest value of `env.MY_SECRET` since a new instance is constructed with every incoming request - // ... do things with `client` - } -} + // ... do things with `client` + }, +}; ``` Compared to this alternative, which might have surprising and unwanted behavior: @@ -65,12 +64,153 @@ Compared to this alternative, which might have surprising and unwanted behavior: let client = undefined; export default { - fetch(request, env) { - client ??= new Client(env.MY_SECRET); // `client` here might not be updated when `env.MY_SECRET` changes, since it may already exist in global scope + fetch(request, env) { + client ??= new Client(env.MY_SECRET); // `client` here might not be updated when `env.MY_SECRET` changes, since it may already exist in global scope + + // ... do things with `client` + }, +}; +``` + +If you have more advanced needs, explore the [AsyncLocalStorage API](/workers/runtime-apis/nodejs/asynclocalstorage/), which provides a mechanism for exposing values down to child execution handlers. + +## How to access `env` + +Bindings are located on the `env` object, which can be accessed in several ways: + +- It is an argument to entrypoint handlers such as [`fetch`](/workers/runtime-apis/fetch/): + + ```js + export default { + async fetch(request, env) { + return new Response(`Hi, ${env.NAME}`); + }, + }; + ``` - // ... do things with `client` +* It is as class property on [WorkerEntrypoint](/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env), + [DurableObject](/durable-objects/), and [Workflow](/workflows/): + + ```js + export class MyDurableObject extends DurableObject { + async sayHello() { + return `Hi, ${this.env.NAME}!`; + } } + ``` + +* It can be imported from `cloudflare:workers`: + + ```js + import { env } from "cloudflare:workers"; + console.log(`Hi, ${this.env.Name}`); + ``` + +### Importing `env` as a global + +Importing `env` from `cloudflare:workers` is useful when you need to access a binding +such as [secrets](/workers/configuration/secrets/) or [environment variables](/workers/configuration/environment-variables/) +in top-level global scope. For example, to initialize an API client: + +```js +import { env } from "cloudflare:workers"; +import ApiClient from "example-api-client"; + +// API_KEY and LOG_LEVEL now usable in top-level scope +let apiClient = ApiClient.new({ apiKey: env.API_KEY }); +const LOG_LEVEL = env.LOG_LEVEL || "info"; + +export default { + fetch(req) { + // you can use apiClient or LOG_LEVEL, configured before any request is handled + }, +}; +``` + +Workers do not allow I/O from outside a request context. This means that even +though `env` is accessible from the top-level scope, you will not be able to access +every binding's methods. + +For instance, environment variables and secrets are accessible, and you are able to +call `env.NAMESPACE.get` to get a [Durable Object stub](/durable-objects/api/stub/) in the +top-level context. However, calling methods on the Durable Object stub, making [calls to a KV store](/kv/api/), +and [calling to other Workers](/workers/runtime-apis/bindings/service-bindings) will not work. + +```js +import { env } from "cloudflare:workers"; + +// This would error! +// env.KV.get('my-key') + +export default { + async fetch(req) { + // This works + let myVal = await env.KV.get("my-key"); + Response.new(myVal); + }, +}; +``` + +Additionally, importing `env` from `cloudflare:workers` lets you avoid passing `env` +as an argument through many function calls if you need to access a binding from a deeply-nested +function. This can be helpful in a complex codebase. + +```js +import { env } from "cloudflare:workers"; + +export default { + fetch(req) { + Response.new(sayHello()); + }, +}; + +// env is not an argument to sayHello... +function sayHello() { + let myName = getName(); + return `Hello, ${myName}`; +} + +// ...nor is it an argument to getName +function getName() { + return env.MY_NAME; } ``` -If you have more advanced needs, explore the [AsyncLocalStorage API](/workers/runtime-apis/nodejs/asynclocalstorage/), which provides a mechanism for exposing values down to child execution handlers. +:::note +While using `env` from `cloudflare:workers` may be simpler to write than passing it +through a series of function calls, passing `env` as an argument is a helpful pattern +for dependency injection and testing. +::: + +### Overriding `env` values + +The `withEnv` function provides a mechanism for overriding values of `env`. + +Imagine a user has defined the [environment variable](/workers/configuration/environment-variables/) +"NAME" to be "Alice" in their Wrangler configuration file and deployed a Worker. By default, logging +`env.NAME` would print "Alice". Using the `withEnv` function, you can override the value of +"NAME". + +```js +import { env, withEnv } from "cloudflare:workers"; + +function logName() { + console.log(env.NAME); +} + +export default { + fetch(req) { + // this will log "Alice" + logName(); + + withEnv({ NAME: "Bob" }, () => { + // this will log "Bob" + logName(); + }); + + // ...etc... + }, +}; +``` + +This can be useful when testing code that relies on an imported `env` object.