Cloudflare Worker's Bindings in tRPC 🧙‍♀️


When using Cloudflare Workers, the main selling point is you can integrate with a lot of other Cloudflare APIs, like D1 (database), R2 (storage) and Durable Objects (websockets), and more. However, there isn’t any documentation on how to do that.

The up-to-date way to implement Cloudflare Workers is by using the module workers syntax, as follows:

export default {
  async fetch(
    request: Request,
    env: Env, // 👈 not shown on the tRPC docs
    ctx: ExecutionContext, // 👈 not shown on the tRPC docs
  ): Promise<Response> {
    return fetchRequestHandler({
      endpoint: trpcApiPath,
      req: request,
      router: appRouter,
      createContext,
    });
  },
};

You get the env object here, allowing you to access cloudflare bindings. Often, examples won’t include the 2nd and 3rd parameters of fetch, showing only async fetch(request: Request): Promise<Response> {...}. For example, the tRPC docs.

Actually, we just need to use env in createContext. Pretty simple. So let’s try.

Use env in createContext 🐛

The easiest option is to “inline” the createContext argument. However, this won’t actually work because we need the createContext function to instantiate tRPC / appRouter: const t = initTRPC.context<Context>().create();, in another file. You might also get into a circular dependency hell.

import { FetchCreateContextFnOptions, fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './ts/trpc';
import { trpcApiPath } from './ts/trpcPath';
import { Env } from './ts/worker-configuration';
import { drizzle } from 'drizzle-orm/d1';

export default {
    async fetch(
        request: Request,
        env: Env,
        ctx: ExecutionContext
    ): Promise<Response> {
        return fetchRequestHandler({
            endpoint: trpcApiPath,
            req: request,
            router: appRouter,
            createContext: ({ req, resHeaders }: FetchCreateContextFnOptions) =>
          			// 🎸 You have access to `env` here, so
                // use a binding, for example: `env.SERVICE.fetch()`
                const user = { name: req.headers.get('username') ?? 'anonymous' };
                const db = drizzle(env.DB);
                return { req, resHeaders, user, db };
            },
        });
    },
};

Refactoring… it works. 😍

// src/index.ts
import {
  FetchCreateContextFnOptions,
  fetchRequestHandler,
} from "@trpc/server/adapters/fetch";
import { appRouter } from "./trpc/appRouter";
import { trpcApiPath } from "./trpc/trpcPath";
import { Env } from "./worker-configuration";
import { createContext } from "./trpc/context";

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    if (request.method === "OPTIONS") {
      const response = new Response(null, {
        status: 200,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Headers": "*",
        },
      });
      return response;
    }

    return fetchRequestHandler({
      endpoint: trpcApiPath,
      req: request,
      router: appRouter,
      createContext: (options: FetchCreateContextFnOptions) =>
        createContext({ ...options, env, ctx }),
    });
  },
};
// src/trpc/context.ts
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { Env } from "./ts/worker-configuration";
import { drizzle } from "drizzle-orm/d1";

const createContext = async ({
  req,
  env,
  resHeaders,
}: FetchCreateContextFnOptions & { env: Env; ctx: ExecutionContext }) => {
  console.log(`Gotttt itt: ${env.MY_ENV_VAR}`);
  // Now use a binding, for example: `env.SERVICE.fetch()`
  const user = { name: req.headers.get("username") ?? "anonymous" };
  const db = drizzle(env.DB);
  return { req, resHeaders, user, db };
};

export type Context = inferAsyncReturnType<typeof createContext>;

Benefits 📈

This approach:

  • avoids a third party dependency
  • explains how to do it, so you can change it or extend it
  • so when the Cloudflare Worker’s API changes in the future, your understanding can help you upgrade to the new API.
  • Avoids hijacking the Cloudflare “entry point”, so you can still use the official way of defining a Cloudflare worker. For example, to use static assets,
  • There is an alternative (a plugin, cloudflare-pages-plugin-trpc) which doesn’t always work, for example when you’re using Astro + tRPC + Cloudflare Workers. See it’s internals.

Conclusion

I’d be interested to know what you’re building, and how you’re using tRPC and Cloudflare!