Skip to content
Moku
@moku-labs/worker pages

d1

Typed wrappers over D1's prepare().bind(). Raw SQL in, the D1 result out. Not an ORM.

On this page

What it does

d1 runs SQL on Cloudflare D1. You write SQL with ? placeholders. You get back the object D1 returned. There is no query builder and no schema mapping.

How it works

query, first and run are one line each: prepare(sql).bind(...params), then all(), first() or run(). batch needs real prepared statements, so prepare(env) hands you the database to build them. It issues no query.

The config is a map of instances with a default, as in kv.

Config

pluginConfigs.d1, keyed by your logical name. The default is {}.

OptionTypeDefaultDescription
[key].namestringrequiredThe Cloudflare database. A stage other than production suffixes it at deploy.
[key].bindingstringrequiredThe key on env.
[key].migrationsstringnoneThe directory of .sql migrations. Deploy metadata: never read at request time.
[key].defaultbooleannoneMarks the default when there are several.

API

Mounted at app.d1, reachable with require(d1Plugin) in a handler.

MethodSignatureDescription
query<T>(env, sql: string, ...params: unknown[]) => Promise<D1Result<T>>All rows, in .results.
first<T>(env, sql: string, ...params: unknown[]) => Promise<T | null>The first row, or null.
run(env, sql: string, ...params: unknown[]) => Promise<D1Result>A write or DDL. Read .meta.
batch(env, statements: D1PreparedStatement[]) => Promise<D1Result[]>Statements in one atomic round trip, results in order.
prepare(env) => D1DatabaseThe database, to build statements for batch.
use(key: string) => D1DatabaseApiThe same five methods on another database.
deployManifest() => { kind: "d1"; name: string; binding: string; migrations?: string }[]Build-time metadata. Takes no env.

Events

Emits

None.

Listens to

None.

Depends on

  • bindings: resolves the database on every call.

Example

src/app.ts
import { createApp, d1Plugin, endpoint } from "@moku-labs/worker";

type Task = { id: number; title: string };

export const app = createApp({
  plugins: [d1Plugin],
  pluginConfigs: {
    d1: { main: { name: "tasks-db", binding: "DB", migrations: "db/migrations" } },
    server: {
      endpoints: [
        endpoint("/tasks").get(async ({ env, require }) => {
          const { results } = await require(d1Plugin).query<Task>(env, "SELECT id, title FROM tasks");
          return Response.json(results);
        }),
        endpoint("/tasks/{id}").get(async ({ params, env, require }) => {
          const task = await require(d1Plugin).first<Task>(env, "SELECT id, title FROM tasks WHERE id = ?", params.id);
          return task === null ? new Response("Not Found", { status: 404 }) : Response.json(task);
        }),
        endpoint("/tasks").post(async ({ request, env, require }) => {
          const { meta } = await require(d1Plugin).run(env, "INSERT INTO tasks (title) VALUES (?)", await request.text());
          return Response.json({ id: meta.last_row_id }, { status: 201 });
        }),
        endpoint("/tasks/{from}/move/{to}").post(async ({ params, env, require }) => {
          const d1 = require(d1Plugin);
          const db = d1.prepare(env);
          await d1.batch(env, [
            db.prepare("UPDATE tasks SET list = ? WHERE list = ?").bind(params.to, params.from),
            db.prepare("UPDATE lists SET moved = moved + 1 WHERE id = ?").bind(params.from)
          ]);
          return new Response(null, { status: 204 });
        })
      ]
    }
  }
});
  1. Both statements commit, or neither does.

Migrations and seeds

d1 only stores the migrations path. deploy acts on it:

WhereWhen
LocalEach dev start, while migrateLocal is on. Also when seed runs locally.
RemoteAfter a live deploy, only with deploy({ migration: true }).
  • A plain deploy does not migrate production. Pass migration: true, or the new code runs on the old schema.
  • Row types are your word. <T> is forwarded to D1. Nothing checks the rows against it.
  • batch takes statements, not SQL. Build them from prepare(env) on the same database.