D1 — A SQL Database Your Worker Can Call
Hoplink's owners now ask real questions: "Which of my links got the most clicks last week?" "Show every link I created, newest first." KV can only fetch a value by its key; it cannot sort, filter or join. For questions like these you want SQL, and on Cloudflare that means D1, a serverless database built on SQLite.
The rule
Prepare the SQL, bind the values, pick how many rows you want.
You create a database with wrangler d1 create hop-db, add the printed d1_databases entry to your config with "binding": "DB", and every query follows the same three moves:
// 1. prepare 2. bind 3. run it and choose the shape of the result
const link = await env.DB
.prepare("SELECT code, url, owner FROM links WHERE code = ?")
.bind("cat")
.first(); // one row as an object, or null
const { results } = await env.DB
.prepare("SELECT code, url FROM links WHERE owner = ? ORDER BY created_at DESC")
.bind("priya")
.all(); // every matching row, in results
await env.DB
.prepare("INSERT INTO links (code, url, owner) VALUES (?, ?, ?)")
.bind("dog", "https://example.com/dogs", "priya")
.run(); // for writes: returns success and meta, no rowspreparetakes SQL with?placeholders — SQLite's dialect, so the SQLite functions and types apply.bindfills the placeholders in order. The values travel separately from the SQL text, so a malicious code likex'; DROP TABLE links; --is treated as plain data. This is your protection against SQL injection.first/all/runchoose the result shape: one row, many rows, or just "did it work".
Every result also carries meta, including how many rows were read and written — useful for understanding cost, since D1 bills by rows read and written.
D1 is strongly consistent: a row you insert is there for the very next query, unlike a KV write that spreads slowly. Each database has a primary copy in one location, so D1 fits data your app owns and queries, while KV remains the faster choice for simple global lookups.
The shortcut
Before writing a D1 call, say the question out loud and listen for the word that picks the method: "the" link → first(), "all" or "every" link → all(), "save/change/remove" → run().
Say the question
"Get every link Priya owns, newest first, top five." The word "every" means
all().
Trap: building SQL with template strings such as `WHERE code = '${code}'`. It works in testing and opens the door to SQL injection in production.
Drills
Answer fast — trust the rule.
Which method returns a single row as an object, or null if nothing matches?
Why use .bind(code) instead of inserting code into the SQL string?
Which SQL dialect does D1 speak?
You insert a new link and immediately select it. What happens?
Remember
- Prepare the SQL, bind the values, pick how many rows you want.
- Shortcut: "the" →
first(), "every" →all(), change →run(). - Trap: pasting values into SQL strings instead of binding them.
