On April 5, 2024, Japan time, Cloudflare announced communication between Cloudflare Workers using RPC.
https://blog.cloudflare.com/javascript-native-rpc
I think it's no exaggeration to say that this has solved several issues and at the same time raised the convenience of building applications on Cloudflare not by one level, but by more than two levels.
This RPC support has made Service Bindings even more user-friendly, so I would like to introduce it.
The completed code is here, so if you don't have time, please refer to it.
https://github.com/chimame/connect-remix-and-prisma-d1-using-rpc-on-cloudflare-pages
Prerequisites
Bundling Remix and Prisma will exceed 1MB in size. Therefore, deployment will not complete successfully with a free Cloudflare account. I will explain this on the premise of solving the problem.
Of course, the main topic is the content of Service Bindings using RPC announced by Cloudflare.
https://github.com/chimame/remix-prisma-d1-on-cloudflare-pages
Service Bindings
Service Bindings allow one Cloudflare Worker to call another Cloudflare Worker.
The point is that you don't have all the workers in one place, you divide them up and place them so that you can call workers from one worker to another. There are also some benefits to partitioning.
- Cloudflare Workers file size can be kept small
- Cache each Cloudflare Workers process
And so on. Of course, there are benefits to keeping it small, but if you divide it too much, there are also disadvantages similar to microservices, so be careful about that.
However, Cloudflare Workers also has size limitations (depending on your subscription plan). If it were free, there would be a limit of 1MB in post-build (compressed) size. Serivce Bindings are also a very useful feature to avoid size constraints.
However, until now, there was a problem of what to do with the interface between the two workers if they were simply split using Serivce Bindings. This problem has been solved by this RPC, so I will explain how to use it and divide it into parts.
Create a monorepo with 2 Workers
Currently, the size of Wasm in Prisma is a little large, so if you bundle Remix and Prisma, it will exceed 1MB. So, unfortunately, those who wanted to run the free version had to use other methods.
However, it has become possible to smartly avoid this using the RPC that was announced this time, so I will write about it.
This time, I will write an outline of the composition of the completed form below.
Split into two Workers as above.
- Workers specialized in DB access using Prisma
- Workers who render with received data using Remix
In the case of Remix, it uses Cloudflare Pages, so it is called Cloudflare Pages Functions, but in reality it is Cloudflare Workers, so there is little difference. So, to avoid the size limit, build the application by dividing it into two workers as shown above.
Since we will be creating two Workers, we will create the project. yarn
I will describe it with a monorepo configuration using, but pnpm
you can create it with whatever you like.
We will arrange it like this.
├── README.md
├── package.json
├── yarn.lock
└── pakcages/
├── prisma(workers)
└── remix(workers)
Initialize Remix
Place the Remix as shown below without thinking about anything in particular.
mkdir -p packages/remix
cd packages/remix
npx create-remix@latest --template remix-run/remix/templates/cloudflare
Initial setup of Prisma
Last time I put it in the same package.json as Remix, but this time I'll separate it. There is no problem if you configure it from the project root using the following command.
mkdir -p packages/prisma
# Create packages/prisma/package.json with only `name` written
yarn workspace <prisma package name> add -D prisma
yarn workspace <prisma package name> add @prisma/client @prisma/adapter-d1
yarn workspace <prisma package name> run prisma init --datasource-provider sqlite
schema.prisma
Add the following description to the last created file to complete it.
// scheme.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"] // <-- ADD
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
The initial settings for Workers on the Prisma side are now complete.
Database migration
Prisma's D1 support is still in Preview stage. Therefore, migration to D1 is not supported. The Prisma introductory article introduces the procedure for migrating from standard output to D1, but it is not suitable for actual operation. Therefore, if you want to use it, it would be a good idea to move Prisma's migration DDL to the D1 migration file as supported in the commit below.
#!/usr/bin/env zx
await $`mkdir -p ./migrations`
const packages = await glob(['prisma/migrations/*/migration.sql'])
for (let i = 0; i < packages.length; i++) {
const migrationName = packages[i]
.replace('prisma/migrations/', '')
.split('/')[0]
if (!fs.existsSync(`migrations/${migrationName}.sql`)) {
await $`cp ${packages[i]} migrations/${migrationName}.sql`
}
}
await $`yarn run wrangler d1 migrations apply prisma-rpc-db --local`
Process creation for Prisma Workers
First, let's write the process to access the Datebase.
Initial settings for Workers
The Prisma side package only includes Prisma, so first we will add the settings for Workers to run.
yarn workspace <prisma package name> add -D wrangler @cloudflare/workers-types
yarn workspace <prisma package name> run wrangler create D1 <D1 database name>
wrangler.toml
I will describe the settings of the D1 I created . Below is an example.
# packages/prisma/wrangler.toml
name = "prisma-d1-rpc-sample"
main = "src/index.ts"
compatibility_date = "2022-04-05"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "prisma-rpc-db"
database_id = "__YOUR_D1_DATABASE_ID__"
Creating Workers using Prisma
Next is the actual code for the Workers on the Prisma side, and I will write the code like this.
// pakcages/prisma/database/client.ts
import { PrismaClient } from "@prisma/client";
import { PrismaD1 } from "@prisma/adapter-d1";
export const connection = (db: D1Database) => {
const adapter = new PrismaD1(db);
return new PrismaClient({ adapter });
}
// packages/prisma/src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";
import { connection } from "./database/client";
export interface Env {
DB: D1Database,
}
export class UserService extends WorkerEntrypoint<Env> {
fetchUsers() {
const db = connection(this.env.DB)
return db.user.findMany();
}
async createUser(data: { name: string, email: string }) {
const db = connection(this.env.DB)
const user = await db.user.create({
data,
});
return user;
}
}
export default {
// An error occurs when deploying this worker without register event handlers as default export.
async fetch() {
return new Response("Healthy!");
},
};
client.ts
I don't think there is any need to explain anything, but it simply defines a function to connect to D1 using Prisma. src/index.ts
The side is the code of the Workers main body. The target of Service Bindings is exporting UserService. Although it is defined using named export, there is no problem with default export. UserServicehas two functions and has defined two functions: one that returns user data fetchUsersand one that creates a user . createUserThis function uses Prisma to access D1 and perform SELECT
and INSERT
processing.
I don't think it's particularly difficult if you just look at it. The important thing here is WorkerEntrypoint<Env>
. In particular Env, it makes D1 settings this.env.DBaccessible
by importing them, and serves as the basis for defining the RPC-compatible Service Bindings. This allows processing of D1 and Prisma to be confined to the Workers here.
Creating a Remix (Pages Functions)
Configuring Service Bindings
Remix has most of the settings in the initial setup, but this time we will be using Service Bindings using RPC, so we will need the following wrangler.toml
settings Env
.
# packages/remix/wrangler.toml
name = "remix-rpc-sample"
[[services]]
binding = "USER_SERVICE"
service = "prisma-d1-rpc-sample"
entrypoint = "UserService"
// packages/remix/load-context.ts
import { type PlatformProxy } from "wrangler";
import type { UserService } from "@rpc-sample/prisma/src";
interface Env {
USER_SERVICE: Service<UserService>; // <---ADD
}
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
declare module "@remix-run/cloudflare" {
interface AppLoadContext {
cloudflare: Cloudflare;
}
}
First, settings are written in wrangler.toml
so that Prisma's Workers can be referenced from the Remix side. By writing this, you will be able to use Service Bindigs settings locally. entrypointThis setting is specified here , but this setting is required when writing Workers to be bound by named export. Conversely, this is not necessary for default export. The content of
load-context.ts
what you write Env
is wrangler.toml
defined as a type.
By having these two things in place, the true power of Service Bindings using RPC, which will be described later, will be demonstrated.
There is a point to note, although wrangler types
a Remix npm script is defined, it does not output the type shown wrangler types
above even if you use it Service<UserService>
. I don't think it supports RPC yet, so please correct it manually.
Load DB data via Service Bindings
In Remix, loader
data is loaded using , but the program that loads data using the configured Service Bindings is as follows.
// packages/remix/app/routes/users/handlers/loader.ts
import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
export const loader = async ({ context }: LoaderFunctionArgs) => {
using users = await context.cloudflare.env.USER_SERVICE.fetchUsers();
return { users }
}
The actual data loading process is on the Workers side of Prisma, so Remix just calls it from the connection settings. But this is TypeScript. usersWhat happens to this variable?
The type is defined like this. This is the greatness of Service Bindings that support RPC. Since type information is linked in this way, you can greatly benefit from TypeScript. This tRPC
means that you can implement pretty much the same thing within Cloudflare as you did a while ago .
useLoaderData
Since we have a type , all we have to do is get the data from this as usual and render it.
// packages/remix/app/routes/users/route.tsx
import { Form, useLoaderData } from "@remix-run/react";
import { loader, action } from "./handlers";
export default function UserPage() {
const { users } = useLoaderData<typeof loader>()
return (
<div>
<Form method="post">
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
<br />
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
<button type="submit">Add</button>
</Form>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
export { loader, action };
Launching the application
Service Bindings using this RPC could not be started using Vite ( getPlatformProxy
). It seems that it is not supported yet. Therefore, we need to run it with wrangler pages dev
, which is the actual start
script, which is another startup command.
Prisma's Workers will also start, so concurrentlystart them at the same time.
{
...
"scripts": {
"dev": "concurrently \"yarn run dev:prisma\" \"yarn run dev:remix:build && yarn run dev:remix:start\"",
"dev:prisma": "yarn workspace @rpc-sample/prisma dev",
"dev:remix:build": "yarn workspace @rpc-sample/remix build",
"dev:remix:start": "yarn workspace @rpc-sample/remix start"
},
...
}
Now, /users
when you access the Remix screen and access the path, you should see a screen displaying user registration and the registered user.
The RPC announced this time makes it possible for Remix and Prisma to work together so that they can share type information while separating each worker. I tried it with a free version account and have confirmed that it can be successfully deployed and works.
using
What?
loader
During the Remix process using
, you will notice that it is smooth. This appeared in TypeScript 5.2, but this is a specification in Stage 3 of ECMAScript.
https://github.com/tc39/proposal-explicit-resource-management
If you write very roughly, it will free up resources in a good way. (Seriously, I'm writing this in a rough way, so please try to understand it yourself) So why are you using it this time? That's explained in detail in this document.
https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
It's too long to read it all, so I'll summarize it:
-
using
is a Stage 3 specification, but it will also be incorporated into V8 soon. - The return value of Cloudflare Workers' RPC is held in memory on the server side (Prisma side Workers in this article).
- To explicitly release memory on the server side,
Symbol.dispose
call the so-called disposer from the client side (Remix side in this article) and instruct the server side to release it. - If you want to keep the variable on the client side,
dup
copy it.
That's it. That's right. The value returned from this RPC is not held in the caller's memory space, but on the server side. (The person who implemented it is amazing) is done using
when freeing memory. Therefore, this specification is also used Symbol.dispose
to release memory on the server side.
In other words, Service Bindings are not exchanging data using a protocol like http, but rather the Workers connected using Service Bindings are running on some other host. It may be close to something like that.
Finally
Announcing RPC greatly improves the usability of Cloudflare Workers' Serivce Bindings. It is far beyond imagination that so much can be implemented using just the smart writing style of the class syntax. It should now be much easier to implement the front end and back end with just Cloudflare Workers.
Please try using it when you come up with a design that requires collaboration between Cloudflare Workers.
Top comments (0)