🎯

d1-database

🎯Skill

from null-shot/cloudflare-skills

VibeIndex|
What it does

Enables serverless SQLite database operations at the edge, supporting structured data, SQL queries, migrations, and complex relational data management in Cloudflare Workers.

πŸ“¦

Part of

null-shot/cloudflare-skills(11 items)

d1-database

Installation

npxRun with npx
npx skills-ref validate ./agents-sdk
npxRun with npx
npx skills-ref validate ./*/
πŸ“– Extracted from docs: null-shot/cloudflare-skills
1Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Serverless SQLite database for structured data at the edge. Load when building relational schemas, running SQL queries, managing migrations, performing CRUD operations, using JOINs/aggregations, handling JSON columns, or enforcing foreign keys with D1.

Overview

# D1 Database

D1 is Cloudflare's native serverless SQL database built on SQLite. Run queries at the edge with automatic replication and low latency.

FIRST: Create Database

```bash

# Create D1 database

wrangler d1 create my-database

# Output includes database_id - add to wrangler.jsonc

```

Add binding to wrangler.jsonc:

```jsonc

{

"d1_databases": [

{

"binding": "DB",

"database_name": "my-database",

"database_id": "",

"migrations_dir": "./migrations"

}

]

}

```

Generate TypeScript types:

```bash

wrangler types

```

When to Use

| Use Case | Why D1 |

|----------|--------|

| Relational data | Structured tables with relationships |

| Complex queries | JOINs, aggregations, full SQL support |

| User data | Accounts, profiles, settings |

| Content management | Posts, comments, metadata |

| E-commerce | Products, orders, inventory |

| Analytics | Event tracking with SQL queries |

When NOT to use D1:

  • Simple key-value data β†’ Use Workers KV
  • Large files/objects β†’ Use R2
  • Real-time coordination β†’ Use Durable Objects
  • High-frequency writes β†’ Use Queues + D1

Quick Reference

| Operation | Method | Returns |

|-----------|--------|---------|

| Execute query | db.prepare(sql).bind(...).run() | { success: boolean, meta: {...} } |

| Get all rows | db.prepare(sql).bind(...).all() | { results: T[], success: boolean } |

| Get first row | db.prepare(sql).bind(...).first() | T \| null |

| Get single value | db.prepare(sql).bind(...).first('column') | any \| null |

| Batch queries | db.batch([stmt1, stmt2, ...]) | Array |

| Raw query | db.prepare(sql).bind(...).raw() | Array> |

Minimal Example

```typescript

// src/index.ts

interface Env {

DB: D1Database;

}

export default {

async fetch(request: Request, env: Env) {

// Get all users

const { results } = await env.DB

.prepare("SELECT * FROM users WHERE active = ?")

.bind(true)

.all<{ id: number; name: string; email: string }>();

return Response.json(results);

}

} satisfies ExportedHandler;

```

CRUD Operations

```typescript

interface Env {

DB: D1Database;

}

type User = {

id: number;

name: string;

email: string;

created_at: string;

};

type NewUser = Omit;

export default {

async fetch(request: Request, env: Env) {

const url = new URL(request.url);

const { pathname } = url;

// CREATE - Insert new user

if (pathname === "/users" && request.method === "POST") {

const user: NewUser = await request.json();

const result = await env.DB

.prepare("INSERT INTO users (name, email) VALUES (?, ?)")

.bind(user.name, user.email)

.run();

if (!result.success) {

return Response.json({ error: "Failed to create user" }, { status: 500 });

}

return Response.json({

id: result.meta.last_row_id,

message: "User created"

}, { status: 201 });

}

// READ - Get user by ID

if (pathname.startsWith("/users/") && request.method === "GET") {

const id = pathname.split("/")[2];

const user = await env.DB

.prepare("SELECT * FROM users WHERE id = ?")

.bind(id)

.first();

if (!user) {

return Response.json({ error: "User not found" }, { status: 404 });

}

return Response.json(user);

}

// READ - Get all users (with pagination)

if (pathname === "/users" && request.method === "GET") {

const page = parseInt(url.searchParams.get("page") || "1");

const limit = parseInt(url.searchParams.get("limit") || "10");

const offset = (page - 1) * limit;

const { results } = await env.DB

.prepare("SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?")

.bind(limit, offset)

.all();

return Response.json({

users: results,

page,

limit

});

}

// UPDATE - Update user

if (pathname.startsWith("/users/") && request.method === "PUT") {

const id = pathname.split("/")[2];

const updates: Partial = await request.json();

const result = await env.DB

.prepare("UPDATE users SET name = ?, email = ? WHERE id = ?")

.bind(updates.name, updates.email, id)

.run();

if (result.meta.changes === 0) {

return Response.json({ error: "User not found" }, { status: 404 });

}

return Response.json({ message: "User updated" });

}

// DELETE - Delete user

if (pathname.startsWith("/users/") && request.method === "DELETE") {

const id = pathname.split("/")[2];

const result = await env.DB

.prepare("DELETE FROM users WHERE id = ?")

.bind(id)

.run();

if (result.meta.changes === 0) {

return Response.json({ error: "User not found" }, { status: 404 });

}

return Response.json({ message: "User deleted" });

}

return Response.json({ error: "Not found" }, { status: 404 });

}

} satisfies ExportedHandler;

```

Migrations Workflow

D1 uses SQL migration files to manage schema changes over time.

Create Migration

```bash

# Create new migration file

wrangler d1 migrations create my-database create_users_table

# Creates: migrations/0001_create_users_table.sql

```

Edit the generated migration file:

```sql

-- migrations/0001_create_users_table.sql

CREATE TABLE users (

id INTEGER PRIMARY KEY AUTOINCREMENT,

name TEXT NOT NULL,

email TEXT UNIQUE NOT NULL,

created_at DATETIME DEFAULT CURRENT_TIMESTAMP

);

CREATE INDEX idx_users_email ON users(email);

```

Apply Migrations

```bash

# Apply locally for testing

wrangler d1 migrations apply my-database --local

# Apply to remote database

wrangler d1 migrations apply my-database --remote

# List pending migrations

wrangler d1 migrations list my-database --local

```

Migration Best Practices

  1. One migration per change: Create separate migrations for each schema change
  2. Test locally first: Always run --local before --remote
  3. Never edit applied migrations: Create new migrations to fix issues
  4. Use transactions: Migrations run in transactions by default
  5. Add indexes: Include indexes in migrations for query performance

Example migration with multiple tables:

```sql

-- migrations/0002_add_posts.sql

CREATE TABLE posts (

id INTEGER PRIMARY KEY AUTOINCREMENT,

user_id INTEGER NOT NULL,

title TEXT NOT NULL,

content TEXT NOT NULL,

published BOOLEAN DEFAULT 0,

created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE

);

CREATE INDEX idx_posts_user_id ON posts(user_id);

CREATE INDEX idx_posts_published ON posts(published);

```

Batch Operations

Execute multiple statements in a single transaction for better performance and atomicity.

```typescript

interface Env {

DB: D1Database;

}

type Post = {

title: string;

content: string;

userId: number;

};

export default {

async fetch(request: Request, env: Env) {

const posts: Post[] = await request.json();

// Prepare all statements

const statements = posts.map(post =>

env.DB

.prepare("INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)")

.bind(post.title, post.content, post.userId)

);

// Execute as a batch (all or nothing)

try {

const results = await env.DB.batch(statements);

const insertedIds = results.map(r => r.meta.last_row_id);

return Response.json({

message: "Posts created",

ids: insertedIds,

count: results.length

});

} catch (error) {

return Response.json(

{ error: "Batch insert failed", details: error.message },

{ status: 500 }

);

}

}

} satisfies ExportedHandler;

```

Batch benefits:

  • All statements succeed or all fail (atomic)
  • Single round-trip to database
  • Better performance for bulk operations
  • Automatic transaction handling

Batch limitations:

  • Maximum 100 statements per batch
  • All statements must be from same database
  • Cannot read from one statement and use in another within batch

SQLite Features

D1 supports comprehensive SQLite features including:

  • JSON Functions - Query and manipulate JSON data in columns. See [references/json-functions.md](references/json-functions.md)
  • Foreign Keys - Enforce referential integrity across tables. See [references/foreign-keys.md](references/foreign-keys.md)
  • Full-Text Search (FTS5) - Fast text search with stemming and ranking. See [references/sql-statements.md](references/sql-statements.md)
  • Math Functions - sqrt(), pow(), sin(), cos(), and more
  • PRAGMA Statements - Schema introspection, optimization, constraint checking

Query Patterns

Parameterized Queries (Required)

ALWAYS use parameterized queries to prevent SQL injection:

```typescript

// βœ… CORRECT - Parameterized query

const user = await env.DB

.prepare("SELECT * FROM users WHERE email = ?")

.bind(userEmail)

.first();

// ❌ DANGEROUS - String concatenation (SQL injection risk)

const user = await env.DB

.prepare(SELECT * FROM users WHERE email = '${userEmail}')

.first();

```

Type-Safe Queries

Use TypeScript generics for type-safe results:

```typescript

type UserWithPosts = {

id: number;

name: string;

email: string;

post_count: number;

};

const { results } = await env.DB

.prepare(`

SELECT u.*, COUNT(p.id) as post_count

FROM users u

LEFT JOIN posts p ON u.id = p.user_id

GROUP BY u.id

`)

.all();

// results is typed as UserWithPosts[]

results.forEach(user => {

console.log(${user.name} has ${user.post_count} posts);

});

```

Complex Queries with JOINs

```typescript

type PostDetail = {

post_id: number;

post_title: string;

post_content: string;

author_name: string;

author_email: string;

created_at: string;

};

const { results } = await env.DB

.prepare(`

SELECT

p.id as post_id,

p.title as post_title,

p.content as post_content,

u.name as author_name,

u.email as author_email,

p.created_at

FROM posts p

INNER JOIN users u ON p.user_id = u.id

WHERE p.published = ?

ORDER BY p.created_at DESC

LIMIT ?

`)

.bind(true, 10)

.all();

return Response.json(results);

```

Pagination and Search

```typescript

// Pagination

const offset = (page - 1) * limit;

const { results } = await env.DB

.prepare("SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?")

.bind(limit, offset)

.all();

// Search with LIKE

const { results } = await env.DB

.prepare("SELECT * FROM users WHERE name LIKE ? LIMIT 20")

.bind(%${searchTerm}%)

.all();

```

See [references/queries.md](references/queries.md) for complete pagination patterns with total counts and hasMore flags

Error Handling

```typescript

try {

const result = await env.DB

.prepare("INSERT INTO users (name, email) VALUES (?, ?)")

.bind(name, email)

.run();

if (!result.success) {

throw new Error("Insert failed");

}

return Response.json({ id: result.meta.last_row_id });

} catch (error) {

// Handle specific SQLite errors

if (error.message.includes("UNIQUE constraint failed")) {

return Response.json(

{ error: "Email already exists" },

{ status: 409 }

);

}

console.error("Database error:", error);

return Response.json(

{ error: "Database operation failed" },

{ status: 500 }

);

}

```

Result Metadata

Query results include metadata about the operation:

```typescript

const result = await env.DB

.prepare("UPDATE users SET name = ? WHERE id = ?")

.bind("New Name", 123)

.run();

// Check metadata

console.log({

success: result.success, // boolean - operation succeeded

changes: result.meta.changes, // number - rows affected

lastRowId: result.meta.last_row_id, // number - last inserted ID

duration: result.meta.duration // number - query time in ms

});

// Common patterns

if (result.meta.changes === 0) {

return Response.json({ error: "Not found" }, { status: 404 });

}

```

Detailed References

Core SQL Features

  • [references/queries.md](references/queries.md) - Advanced query patterns, aggregations, window functions, CTEs, subqueries
  • [references/json-functions.md](references/json-functions.md) - Complete JSON API: extract, modify, arrays, objects, generated columns
  • [references/foreign-keys.md](references/foreign-keys.md) - Foreign key constraints, CASCADE, RESTRICT, SET NULL, deferring constraints
  • [references/sql-statements.md](references/sql-statements.md) - SQLite extensions (FTS5, Math), PRAGMA statements, schema introspection

D1-Specific

  • [references/migrations.md](references/migrations.md) - Schema design, migration strategies, rollback patterns
  • [references/testing.md](references/testing.md) - Vitest integration, applying migrations in tests, test isolation

Best Practices

  1. Always use parameterized queries: Use .bind() to prevent SQL injection
  2. Use TypeScript generics: Type your query results with .first() and .all()
  3. Batch for bulk operations: Use db.batch() for multiple related operations
  4. Index your queries: Add indexes on columns used in WHERE, JOIN, and ORDER BY
  5. Test migrations locally: Run wrangler d1 migrations apply --local before --remote
  6. Handle unique constraint errors: Check for specific error messages like "UNIQUE constraint failed"
  7. Use transactions via batch: Batch operations are automatically atomic
  8. Paginate large result sets: Use LIMIT and OFFSET to avoid memory issues
  9. Check result.success: Always verify operation success before proceeding
  10. Design for eventual consistency: D1 replicates across regions, reads may lag slightly

Performance Tips

  • Limit results: Always use LIMIT for queries that could return many rows
  • Use indexes: Create indexes on frequently queried columns
  • Batch writes: Use db.batch() instead of multiple individual run() calls
  • Avoid SELECT \*: Only select columns you need
  • Use first() for single rows: More efficient than .all() when you need one result
  • Cache read-heavy data: Consider Workers KV for frequently accessed data