Skip to content

Database

Projects on a Pro or Team plan get a managed D1 (SQLite) database. The database is available in API routes as the DB binding — a standard Cloudflare D1Database instance. See the Managed Database guide for setup instructions.

The DB binding is a D1Database object from the Cloudflare Workers runtime. Declare it in your Hono env type:

type Env = {
Bindings: {
DB: D1Database;
KV: KVNamespace;
};
};
const app = new Hono<Env>();

With a plain fetch handler, access it from the env parameter:

export default {
async fetch(request: Request, env: { DB: D1Database }) {
const result = await env.DB.prepare('SELECT 1').first();
return Response.json(result);
},
};

The DB binding is only present when the project has a provisioned database (i.e., you have deployed with db/migrations/ files at least once). If no database exists, env.DB is undefined.

The D1 API provides three execution methods on prepared statements:

Create a prepared statement with parameter binding. Always use .bind() for user-provided values to prevent SQL injection:

app.get('/api/users/:id', async (c) => {
const id = c.req.param('id');
const user = await c.env.DB.prepare(
'SELECT * FROM users WHERE telegram_id = ?'
)
.bind(id)
.first();
if (!user) {
return c.json({ error: 'Not found' }, 404);
}
return c.json(user);
});

Returns the first row, or a single column value if column is specified:

// First row as an object
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(1)
.first();
// { id: 1, telegram_id: 12345, username: 'alice', score: 42 }
// Single column value
const count = await c.env.DB.prepare('SELECT COUNT(*) as total FROM users')
.first('total');
// 150

Returns all matching rows:

const result = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY score DESC LIMIT 50'
).all();
// result.results = [{ id: 1, ... }, { id: 2, ... }, ...]
// result.success = true
// result.meta = { duration: 0.5, ... }

Execute a statement that does not return rows (INSERT, UPDATE, DELETE):

await c.env.DB.prepare('INSERT INTO users (telegram_id, username) VALUES (?, ?)')
.bind(12345, 'alice')
.run();

Execute multiple statements in a single round-trip. All statements in a batch run inside an implicit transaction:

const results = await c.env.DB.batch([
c.env.DB.prepare('INSERT INTO users (telegram_id, username) VALUES (?, ?)')
.bind(12345, 'alice'),
c.env.DB.prepare('INSERT INTO items (user_id, name, rarity) VALUES (?, ?, ?)')
.bind(1, 'Sword', 'rare'),
]);

For type-safe queries, use drizzle-orm with the D1 driver:

import { drizzle } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm';
import * as schema from '../../db/schema';
app.get('/api/users/:telegramId', async (c) => {
const db = drizzle(c.env.DB, { schema });
const telegramId = Number(c.req.param('telegramId'));
const user = await db
.select()
.from(schema.users)
.where(eq(schema.users.telegramId, telegramId))
.get();
return c.json(user ?? { error: 'Not found' });
});
app.post('/api/users', async (c) => {
const db = drizzle(c.env.DB, { schema });
const body = await c.req.json();
const user = await db
.insert(schema.users)
.values({
telegramId: body.telegramId,
username: body.username,
})
.returning()
.get();
return c.json(user, 201);
});
app.put('/api/users/:telegramId/score', async (c) => {
const db = drizzle(c.env.DB, { schema });
const telegramId = Number(c.req.param('telegramId'));
const { score } = await c.req.json();
const updated = await db
.update(schema.users)
.set({ score })
.where(eq(schema.users.telegramId, telegramId))
.returning()
.get();
return c.json(updated);
});
app.delete('/api/items/:id', async (c) => {
const db = drizzle(c.env.DB, { schema });
const id = Number(c.req.param('id'));
await db.delete(schema.items).where(eq(schema.items.id, id)).run();
return c.json({ ok: true });
});
app.get('/api/users/:telegramId/items', async (c) => {
const db = drizzle(c.env.DB, { schema });
const telegramId = Number(c.req.param('telegramId'));
const user = await db.query.users.findFirst({
where: eq(schema.users.telegramId, telegramId),
with: {
items: true,
},
});
return c.json(user);
});

Relational queries require that you pass { schema } when creating the Drizzle instance and that your schema defines relations(). See the Drizzle ORM docs for details.

Define your binding types once and reuse them across your API routes:

import { Hono } from 'hono';
type Env = {
Bindings: {
DB: D1Database;
KV: KVNamespace;
BOT_TOKEN: string;
};
};
const app = new Hono<Env>();

When using Drizzle ORM, your schema definitions serve as the source of truth for table types. Use typeof schema.users.$inferSelect and typeof schema.users.$inferInsert for inferred row types:

import * as schema from '../../db/schema';
type User = typeof schema.users.$inferSelect;
type NewUser = typeof schema.users.$inferInsert;
PropertyLimit
Max query execution time30 seconds
Max database size10 GB
Max bound parameters per query100
ConsistencyStrong (single-region SQLite)

D1 provides strong read-after-write consistency — unlike KV, a read immediately after a write always returns the latest value.