Skip to content
rdlabo.devdocs

Run your first query

Read an actual MySQL result through the package without creating tables or an application schema. Then see exactly what changes when moving from a local Node.js connection to a Workers Hyperdrive binding.

1. Prepare the local exercise

You need Node.js 24, npm, Docker, and an unused local port 3307. These commands create a disposable local database. The password below is only for this localhost demonstration.

mkdir workers-mysql-demo
cd workers-mysql-demo
npm init -y
npm pkg set type=module
npm install @rdlabo/workers-mysql@0.12.2 mysql2@3 drizzle-orm@0.45
npm install --save-dev tsx@4 @types/node@24

The package includes mysql2 internally. This example also imports mysql2 and Drizzle directly to create an application-owned pool, so it declares them as direct dependencies.

docker run --name workers-mysql-docs-demo --rm -d \
  -p 127.0.0.1:3307:3306 \
  -e MYSQL_ROOT_PASSWORD=local-demo \
  -e MYSQL_DATABASE=demo \
  mysql:8.4

Wait for startup. Run this until it reports mysqld is alive:

docker exec workers-mysql-docs-demo mysqladmin ping -h 127.0.0.1 -uroot -plocal-demo

2. Query and close the connection

Save this as demo.ts. Both roles use the same local pool in this exercise; it does not demonstrate replica routing.

import { createPool } from 'mysql2/promise';
import { drizzle } from 'drizzle-orm/mysql2';
import { createMysqlDatabase } from '@rdlabo/workers-mysql';

const pool = createPool({
  host: '127.0.0.1',
  port: 3307,
  user: 'root',
  password: 'local-demo',
  database: 'demo',
});
const db = createMysqlDatabase({ orm: drizzle(pool), replica: pool });

try {
  const rows = await db.read<{ value: number }>('SELECT ? AS value', [42]);
  console.log(rows[0]?.value);
} finally {
  await pool.end();
}
npx tsx demo.ts

Expected output:

42

The value came from a parameterized SELECT through db.read(). No tables were created or changed. If you get a connection error, confirm the container is ready and port 3307 is available.

Stop the disposable database when finished. Because it was started with --rm, stopping removes the container and its demonstration data:

docker stop workers-mysql-docs-demo

3. Move to Workers and Hyperdrive

In Node.js, the application owns the pool and closes it. In Workers, enable nodejs_compat, configure a Hyperdrive binding named DB that connects to your database, and create the database inside each invocation.

Once the binding exists, the complete Worker below returns [{"value":42}]. It uses one binding for both roles; it needs no schema because the example uses raw SQL:

import { createHyperdriveDatabase, type HyperdriveLike } from '@rdlabo/workers-mysql';
import { DRIZZLE_ORM_OPTIONS } from '@rdlabo/workers-mysql/drizzle';
import { drizzle } from 'drizzle-orm/mysql2';

interface Env {
  DB: HyperdriveLike;
}

export default {
  async fetch(_request: Request, env: Env): Promise<Response> {
    const db = createHyperdriveDatabase({
      primaryHyperdrive: env.DB,
      replicaHyperdrive: env.DB,
      createOrm: (connection) => drizzle(connection, DRIZZLE_ORM_OPTIONS),
    });
    const rows = await db.query<Array<{ value: number }>>('SELECT ? AS value', [42]);
    return Response.json(rows);
  },
};

Use query() for primary SELECTs and read() for replica reads. Add an ORM schema only when you need typed table queries. Runtime explains invocation lifetime, snapshot reads, and retries; Drizzle and dates covers column and storage behavior.

For Hono request containers, add the kit /mysql adapter. Do not wrap already-retrying database methods in another retry loop. A transaction callback may run again, so keep email, payments, and other external side effects outside it.