Skip to content

Cloudflare Wrangler Usage Tips

A few tips for using the Cloudflare Wrangler CLI.

Published View as Markdown

wrangler is Cloudflare’s official tool for managing Cloudflare Workers.

Explicitly Creating a Local D1 Database

After you’ve configured d1_databases in wrangler.toml or wrangler.jsonc, you may want to create a SQLite database locally to simulate D1.

d1_databases = [
  { binding = "DB", database_name = "my_db", database_id = "1234567890" }
]

By default, wrangler does not create this local database immediately. To create it immediately locally, you need to:

  • Run npx wrangler dev
  • Run a SQL query, for example npx wrangler d1 execute my_db --local --command='SELECT * FROM my_table'

After that, you can see the SQLite database created under the .wrangler/state/v3/d1/ directory:

.wrangler
├── state
│   └── v3
│       ├── cache
│       │   └── miniflare-CacheObject
│       ├── d1
│       │   └── miniflare-D1DatabaseObject
│       │       ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite
│       │       ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-shm
│       │       └── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-wal
│       └── workflows
└── tmp

8 directories, 3 files

Then you can set up a drizzle-dev.config.ts file, point the data source at this local SQLite database, and do CRUD locally.

Configuring D1 and Drizzle-ORM for the Development Environment (the wrong turn)

drizzle-orm can provide a config file for each environment. Usually you’ll set up two environments, development and production.

For the local development environment, here is an example config, drizzle-dev.config.ts:

import { defineConfig } from 'drizzle-kit';
import fs from "fs";
import path from "path";

function resolveDbPath() {
    const dir = path.resolve(".wrangler/state/v3/d1/miniflare-D1DatabaseObject");
    try {
        const files = fs.readdirSync(dir);
        const sqliteFile = files.find((file) => file.endsWith('.sqlite'));
        if (!sqliteFile) {
            throw new Error(`No .sqlite file found in ${dir}`);
        }
        return path.join(dir, sqliteFile);
    } catch (err) {
        console.error('Error resolving database path:', err);
        // Return a default or throw an error if the directory/file can't be found
        // For now, let's throw to make it obvious something is wrong.
        throw new Error('Could not resolve the local D1 database path. Please ensure you have mannual created the local db correctly.');
    }
}

export default defineConfig({
    schema: './src/db/schemas/*',
    out: 'drizzle/migrations',
    dialect: 'sqlite',
    dbCredentials: {
        url: resolveDbPath(),
    },
});

That way you can generate migration files for schema changes:

npx drizzle-kit generate --config drizzle-dev.config.ts --name='update_tables'

For convenience, add a script to package.json:

"scripts": {
  "db:generate": "drizzle-kit generate --config drizzle-dev.config.ts",
}

Example of using the script:

npm run db:generate -- --name='init_tables'

Configuring D1 and Drizzle-ORM for the Development Environment (the correct way)

Actually, the drizzle.config.ts file is not mandatory, especially at runtime, where it is not needed.

For a Cloudflare Workers project, the main purpose of providing a local drizzle.config.ts file is to generate migrations files locally. Applying the migrations to the D1 database, however, has to be done with wrangler.

After some experimentation, drizzle can be configured with the following steps: Step 1: Create a drizzle.config.ts file in the project with the following content:

import { defineConfig } from "drizzle-kit";

export default defineConfig({
    dialect: "sqlite",
    schema: "./src/db/schemas",
    driver: "d1-http",
    out: "./drizzle/migrations",
    dbCredentials: {
        accountId: process.env.CF_ACCOUNT_ID!,
        databaseId: process.env.CF_DATABASE_ID!,
        token: process.env.CF_ACCESS_TOKEN!,
    },
});

You can leave all the dbCredentials values empty in .env.local; drizzle never really uses them, because the runtime code does not read drizzle.config.ts.

Step 2: Generate the migration files using the drizzle.config.ts file above:

npx drizzle-kit generate --name=your_message

Step 3: Use wrangler to apply them to the local development D1 database:

npx wrangler d1 apply $your_database_name --local

This step applies the migrations to the SQLite database file under the .wrangler/state/v3/d1/ directory; that is D1’s local development database.

Step 4: Connect to the D1 database with drizzle-orm at runtime:


import { drizzle } from "drizzle-orm/d1"
import { Database } from "../types"
import * as schemas from "./schemas"

export function getDatabase(db: D1Database): Database {
    return drizzle(db, { schema: schemas })
}

So all you need to provide is the D1Database object, which is the D1 Binding declared in the Cloudflare Workers env. The configuration looks like this:

{
    "d1_databases": [
		{
			"binding": "DB",
			"database_name": "xxxxx",
			"database_id": "xxxxx",
			"migrations_dir": "drizzle/migrations"
		}
	],
}

After running npx wrangler typegen, you can find the Env definition in the generated worker-configuration.d.ts or a similar file, for example:

declare namespace Cloudflare {
	interface Env {
		CF_ACCOUNT_ID: string;
		CF_DATABASE_ID: string;
		CF_ACCESS_TOKEN: string;
		DB: D1Database;
	}
}
interface CloudflareBindings extends Cloudflare.Env {}

Disposing Service Bindings in Practice

wrangler 4.x supports implicit disposal with the using keyword. But while writing the code you may run into an error like Object cannot be disposed.

Looking at the tsconfig.ts file, I found:

{
    "compilerOptions": {
        "target": "ES2017"
    }
}

In this case, if you want to dispose in the client Worker, one workable approach is to call the dispose method explicitly. Example code (nextjs):

import { getCloudflareContext } from "@opennextjs/cloudflare";

export async function runRpcUserService<T>(
  callback: (userService: IUserService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const userService = (await env.PICKNAMES_CORE.getUserService()) as IUserService;
  try {
    return await callback(userService);
  } finally {
    userService[Symbol.dispose];
  }
}

export async function runRpcChannelService<T>(
  callback: (channelService: IChannelService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const channelService =(await env.PICKNAMES_CORE.getChannelService()) as IChannelService;
  try {
    return await callback(channelService);
  } finally {
    channelService[Symbol.dispose];
  }
}

But this leads to the following problem: at build time you get Error: Expected an assignment or function call and instead saw an expression.

So I changed it to:

  • userService[Symbol.dispose] -> userService[Symbol.dispose]()
  • channelService[Symbol.dispose] -> channelService[Symbol.dispose]()

In the build stage the error above no longer appears. But at runtime — that is, when running npm run build — the following error appears:

  • TypeError: r[Symbol.dispose] is not a function

After some analysis, I concluded that the error was caused by the as IUserService and as IChannelService type casts. Since those two interface types are not a Disposeable, dispose-related errors show up at compile time or runtime.

See the Cloudflare official docs: Use RpcTarget class to handle Durable Object metadata There is a piece of code:

export default {
  async fetch(request, env, ctx): Promise<Response> {
    let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(
      new URL(request.url).pathname,
    );
    let stub = env.MY_DURABLE_OBJECT.get(id);

    //  * Set the Durable Object metadata using the RpcTarget
    //  * Notice that no await is needed here

    const rpcTarget = stub.setMetaData(id.name ?? "default");

    // Call the Durable Object method using the RpcTarget.
    // The DO identifier is stored in the Durable Object's storage
    const greeting = await rpcTarget.computeMessage("world");

    // Call the Durable Object method that does not use the Durable Object identifier
    const simpleGreeting = await rpcTarget.simpleGreeting("world");

    // Clean up the RpcTarget.
    try {
      (await rpcTarget)[Symbol.dispose]?.();
      console.log("RpcTarget cleaned up.");
    } catch (e) {
      console.error({
        message: "RpcTarget could not be cleaned up.",
        error: String(e),
        errorProperties: e,
      });
    }

    return new Response(greeting, { status: 200 });
  },
} satisfies ExportedHandler<Env>;

So I changed the code above to:

export async function runRpcUserService<T>(
  callback: (userService: IUserService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const userService = await env.PICKNAMES_CORE.getUserService();
  try {
    return await callback(userService);
  } finally {
    // if (typeof userService[Symbol.dispose] === 'function') {
    //   userService[Symbol.dispose]();
    // }
    (await userService)[Symbol.dispose]?.();
  }
}

export async function runRpcChannelService<T>(
  callback: (channelService: IChannelService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const channelService = await env.PICKNAMES_CORE.getChannelService();
  try {
    return await callback(channelService);
  } catch (e) {
    console.error(e);
  } finally {
    // if (typeof channelService[Symbol.dispose] === 'function') {
    //   channelService[Symbol.dispose]();
    // }
    (await channelService)[Symbol.dispose]?.();
  }
}

Compared with before, this approach:

  • removes the problems caused by the type casts
  • although the RPC call returns type any, the callback parameter gives it a concrete type, so callers still get type hints.

Problem solved.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close