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.
Binding type
Section titled “Binding type”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.
Raw D1 API
Section titled “Raw D1 API”The D1 API provides three execution methods on prepared statements:
prepare(sql).bind(...params)
Section titled “prepare(sql).bind(...params)”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);});.first(column?)
Section titled “.first(column?)”Returns the first row, or a single column value if column is specified:
// First row as an objectconst 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 valueconst count = await c.env.DB.prepare('SELECT COUNT(*) as total FROM users') .first('total');// 150.all()
Section titled “.all()”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, ... }.run()
Section titled “.run()”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();.batch(statements)
Section titled “.batch(statements)”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'),]);Using with Drizzle ORM
Section titled “Using with Drizzle ORM”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' });});Insert and return
Section titled “Insert and return”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);});Update
Section titled “Update”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);});Delete
Section titled “Delete”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 });});Relational queries
Section titled “Relational queries”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.
Type safety
Section titled “Type safety”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;Specifications
Section titled “Specifications”| Property | Limit |
|---|---|
| Max query execution time | 30 seconds |
| Max database size | 10 GB |
| Max bound parameters per query | 100 |
| Consistency | Strong (single-region SQLite) |
D1 provides strong read-after-write consistency — unlike KV, a read immediately after a write always returns the latest value.