iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💪

Deploying Next.js and Prisma to Cloudflare: Handling 3 Million Monthly DB Queries for Free

に公開

Introduction

When you host Next.js on Cloudflare, it inevitably runs in the Edge Runtime environment. However, unlike the Node.js Runtime, Prisma cannot be used as is in the Edge Runtime.

The first solution that comes to mind is Prisma Accelerate. Prisma Accelerate is an official service equipped with connection pooling and global caching, making Prisma usable in the Edge Runtime.
However, the free plan has a limit of 60,000 queries per month, which leaves some anxiety for production use.

Therefore, in this article, I will introduce how to build your own Prisma Accelerate on Cloudflare Workers and develop a production-ready service for free. With this method, you can handle up to 3 million queries per month even on the free plan.

For the implementation, I referred to this article and the sample code from the developer of the package mentioned below.

prisma-accelerate-local

To build your own Prisma Accelerate, you use a package called prisma-accelerate-local.
Originally intended for local development environments, you can self-host Prisma Accelerate by running this on Cloudflare Workers.

Preparing Cloudflare Workers

First, prepare the development environment for Cloudflare Workers. You can easily build it by cloning the following repository and following the instructions in README.md.

https://github.com/yu-3in/prisma-accelerate-pg-workers

Also, please refer to the sample repository by the developer of prisma-accelerate-local.

https://github.com/SoraKumo001/prisma-accelerate-workers

The following content provides the steps to build the above repository from scratch.

Creating a Cloudflare Workers project

First, create a project. Use Cloudflare Wrangler to create it from the CLI.

npx wrangler init prisma-accelerate-pg-workers

You can answer y or yes to all questions.
When the option to choose the type of application appears, select "Hello World" Worker (you can select it with the space bar).
Answering yes to the final question will automatically deploy it to Cloudflare Workers.

What type of application do you want to create?
  ● "Hello World" Worker
  ○ "Hello World" Worker (Python)
  ○ "Hello World" Durable Object
  ○ Website or web app
  ○ Example router & proxy Worker
  ○ Scheduled Worker (Cron Trigger)
  ○ Queue consumer & producer Worker
  ○ API starter (OpenAPI compliant)
  ○ Worker built from a template hosted in a git repository

Installing Packages

Install the prisma-accelerate-local package and other Prisma-related packages.
Note that this guide assumes the use of PostgreSQL.

npm install @prisma/client prisma-accelerate-local @prisma/adapter-pg @prisma/adapter-pg-worker @prisma/pg-worker

Setting environment variables in .dev.vars

Create a .dev.vars file in the root directory and set the environment variables as follows.
Replace xxx with an arbitrary random string. This value will later be used as a seed to generate the API_KEY.

.dev.vars
PRISMA_ACCELERATE_SECRET=xxx

Run the following command to reflect the changes in the deployment destination.
You will be prompted for input, so enter the same value as above.

npx wrangler secret put PRISMA_ACCELERATE_SECRET

Creating Cloudflare KV

Go to the Cloudflare dashboard.
Select Workers & Pages from the left menu, then select KV under it.

You will be redirected to a screen like this, so click Create Namespace on the right.
Cloudflare KV dashboard screen

Set an easily identifiable name for Namespace Name. Here, we'll use prisma-accelerate-pg-workers. After entering it, click Add.
Take note of the ID of the created KV.

Configuring wrangler.toml

Next, edit the wrangler.toml file.
In wrangler.toml, you configure settings related to the Cloudflare Workers deployment.
Uncomment the necessary lines from the commented-out settings and edit them.

Set the ID of the KV you just created in the id field of kv_namespaces.

wrangler.toml
#:schema node_modules/wrangler/config-schema.json
name = "prisma-accelerate-pg-workers"
main = "src/index.ts"
compatibility_date = "2024-05-29"
compatibility_flags = ["nodejs_compat"]
+ minify = true

# Automatically place your workloads in an optimal location to minimize latency.
# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure
# rather than the end user may result in better performance.
# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
+ [placement]
+ mode = "smart"

...

# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces
+ [[kv_namespaces]]
+ binding = "KV"
+ id = "xxxxxx"

...

Type Definitions

First, use Wrangler to automatically generate types for reading environment variables.

npm run cf-typegen

Types will be generated in worker-configuration.d.ts.

worker-configuration.d.ts
interface Env {
  KV: KVNamespace;
  PRISMA_ACCELERATE_SECRET: string;
}

Next, create a types directory and define types for Prisma and Wasm.

types/prisma-edge.d.ts
declare module '@prisma/client/runtime/wasm.js' {
  export * from '@prisma/client/runtime/library';
}
types/wasm.d.ts
declare module '*.wasm' {
  const content: any;
  export default content;
}

Adding polyfills

polyfills/util.ts
export * from 'node:util';

Implementation

Finally, implement the logic equivalent to Prisma Accelerate in src/index.ts.

src/index.ts
import { Pool } from '@prisma/pg-worker';
import { PrismaPg } from '@prisma/adapter-pg-worker';
import WASM from '@prisma/client/runtime/query_engine_bg.postgresql.wasm';
import { PrismaAccelerate, PrismaAccelerateConfig, ResultError } from 'prisma-accelerate-local/lib';
import { getPrismaClient } from '@prisma/client/runtime/wasm.js';

const getAdapter = (datasourceUrl: string) => {
  const url = new URL(datasourceUrl);
  const schema = url.searchParams.get('schema') ?? undefined;
  const pool = new Pool({
    connectionString: url.toString() ?? undefined,
  });
  return new PrismaPg(pool, { schema });
};

let prismaAccelerate: PrismaAccelerate;

const getPrismaAccelerate = async ({
  secret,
  onRequestSchema,
  onChangeSchema,
}: {
  secret: string;
  onRequestSchema: PrismaAccelerateConfig['onRequestSchema'];
  onChangeSchema: PrismaAccelerateConfig['onChangeSchema'];
}) => {
  if (prismaAccelerate) {
    return prismaAccelerate;
  }
  prismaAccelerate = new PrismaAccelerate({
    singleInstance: true,
    secret,
    adapter: getAdapter,
    getRuntime: () => require('@prisma/client/runtime/query_engine_bg.postgresql.js'),
    getQueryEngineWasmModule: async () => WASM,
    getPrismaClient,
    onRequestSchema,
    onChangeSchema,
  });
  return prismaAccelerate;
};

const createResponse = async (result: Promise<unknown>) => {
  try {
    const response = await result;
    return new Response(JSON.stringify(response), {
      headers: { 'content-type': 'application/json' },
    });
  } catch (e) {
    if (e instanceof ResultError) {
      console.error(e.value);
      return new Response(JSON.stringify(e.value), {
        status: e.code,
        headers: { 'content-type': 'application/json' },
      });
    }
    return new Response(JSON.stringify(e), {
      status: 500,
      headers: { 'content-type': 'application/json' },
    });
  }
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const prismaAccelerate = await getPrismaAccelerate({
      secret: env.PRISMA_ACCELERATE_SECRET,
      onRequestSchema: ({ engineVersion, hash, datasourceUrl }) => env.KV.get(`schema-${engineVersion}:${hash}:${datasourceUrl}`),
      onChangeSchema: ({ inlineSchema, engineVersion, hash, datasourceUrl }) =>
        env.KV.put(`schema-${engineVersion}:${hash}:${datasourceUrl}`, inlineSchema, { expirationTtl: 60 * 60 * 24 * 7 }),
    });

    const url = new URL(request.url);
    const paths = url.pathname.split('/');
    const command = paths[3];
    const headers = Object.fromEntries(request.headers.entries());

    if (request.method === 'POST') {
      const body = await request.text();
      if (command === 'graphql') {
        return createResponse(prismaAccelerate.query({ body, hash: paths[2], headers }));
      }
      if (command === 'transaction') {
        return createResponse(prismaAccelerate.startTransaction({ body, hash: paths[2], headers, version: paths[1] }));
      }
      if (command === 'itx') {
        const id = paths[4];
        const subCommand = paths[5];
        if (subCommand === 'commit') {
          return createResponse(prismaAccelerate.commitTransaction({ id, hash: paths[2], headers }));
        }
        if (subCommand === 'rollback') {
          return createResponse(prismaAccelerate.rollbackTransaction({ id, hash: paths[2], headers }));
        }
      }
    } else if (request.method === 'PUT' && command === 'schema') {
      const body = await request.text();
      return createResponse(prismaAccelerate.updateSchema({ body, hash: paths[2], headers }));
    }

    return new Response('Not Found', { status: 404 });
  },
};

Deployment

Once you have implemented up to this point, deploy it to Cloudflare Workers.

npm run deploy

Modifications on the Next.js Side

In the Next.js project, you need to configure the following two points:

  1. Change the Prisma client for edge
  2. Modify DATABASE_URL for Prisma Accelerate

Let's explain each step in order.

1. Changing Prisma Client for Edge

First, change the Prisma client to the version for edge.
Specifically, change the import source of PrismaClient from @prisma/client to @prisma/client/edge.
Also, ensure that withAccelerate is called when creating the PrismaClient.

import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";

const prismaClientSingleton = () => {
  return new PrismaClient().$extends(withAccelerate());
};

declare const globalThis: {
  prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;

2. Modifying DATABASE_URL for Prisma Accelerate

Next, set the DATABASE_URL environment variable to the one for Cloudflare Workers.

Generate API_KEY

First, run the following command:

For --secret, specify the same value as the PRISMA_ACCELERATE_SECRET specified in .dev.vars.
For --make, specify a DATABASE_URL such as one from Supabase.

npx prisma-accelerate-local --secret enter_your_secret --make postgres://xxx

When executed, an API_KEY starting with ey will be output, so copy it.

Setting environment variables

Set it in .env.local or a similar file as follows:

DATABASE_URL=prisma://xxxx.workers.dev?api_key=your_api_key

As a point of caution, make sure that the URL scheme is prisma, the host is xxxx.workers.dev, and the query parameter includes api_key.

Conclusion

I introduced a method to build your own Prisma Accelerate on Cloudflare Workers and run Next.js and Prisma in production for free.
By overcoming the limitations of the Edge Runtime environment, it is possible to maintain high performance while keeping costs low. The ability to build a robust service that can handle 3 million accesses per month is a major advantage.
I hope you try this method and find it useful for your projects.

Error Handling

PrismaClientValidationError: Invalid client engine type, please use library or binary

Please run prisma generate again with the --no-engine option.

https://www.prisma.io/docs/orm/reference/prisma-cli-reference#options-1

Unauthorized, check your connection string: {"type":"UnknownJsonError","body":{"Unauthorized":{"reason":"InvalidKey"}}}

The API_KEY is not set correctly. Please check the steps in Generate API_KEY.

References

https://www.prisma.io/docs/orm/prisma-client/deployment/edge/deploy-to-cloudflare

https://next-blog.croud.jp/contents/5f33241c-d6b2-4e8b-8b0a-f8d369729ce0

GitHubで編集を提案

Discussion