Back to list
cameronapak

bknd-crud-delete

by cameronapak

A no-build, un-bloated stack built upon Web Standards that feels freeing to use and can be deployed anywhere.

23🍴 2📅 Jan 21, 2026

SKILL.md


name: bknd-crud-delete description: Use when deleting records from a Bknd entity via the SDK or REST API. Covers deleteOne, deleteMany, soft delete patterns, cascade considerations, response handling, and common patterns.

CRUD Delete

Delete records from your Bknd database using the SDK or REST API.

Prerequisites

  • Bknd project running (local or deployed)
  • Entity exists with records to delete
  • SDK configured or API endpoint known
  • Record ID or filter criteria known
  • Understanding of any relationships/dependencies

When to Use UI Mode

  • Quick one-off deletions
  • Manual data cleanup
  • Visual verification of what's being deleted

UI steps: Admin Panel > Data > Select Entity > Click record > Delete button > Confirm

When to Use Code Mode

  • Application logic for user-initiated deletes
  • Automated data cleanup/maintenance
  • Bulk deletions
  • Soft delete implementations

Code Approach

Step 1: Set Up SDK Client

import { Api } from "bknd";

const api = new Api({
  host: "http://localhost:7654",
});

// If auth required:
api.updateToken("your-jwt-token");

Step 2: Delete Single Record

Use deleteOne(entity, id):

const { ok, data, error } = await api.data.deleteOne("posts", 1);

if (ok) {
  console.log("Deleted post:", data.id);
} else {
  console.error("Failed:", error.message);
}

Step 3: Handle Response

The response object:

type DeleteResponse = {
  ok: boolean;       // Success/failure
  data?: {           // Deleted record (if ok)
    id: number;
    // ...all fields of deleted record
  };
  error?: {          // Error info (if !ok)
    message: string;
    code: string;
  };
};

Step 4: Delete Multiple Records (Bulk)

Use deleteMany(entity, where):

// Delete all archived posts
const { ok, data } = await api.data.deleteMany("posts", {
  status: { $eq: "archived" },
});

// data contains deleted records
console.log("Deleted", data.length, "posts");

Important: where clause is required to prevent accidental delete-all.

// Delete old sessions
await api.data.deleteMany("sessions", {
  last_active: { $lt: "2024-01-01" },
});

// Delete by multiple conditions
await api.data.deleteMany("logs", {
  level: { $eq: "debug" },
  created_at: { $lt: "2024-06-01" },
});

Step 5: Verify Before Delete

Always verify record exists or check count before deleting:

// Check record exists
const { data: existing } = await api.data.readOne("posts", id);
if (!existing) {
  throw new Error("Post not found");
}
await api.data.deleteOne("posts", id);

// Check count before bulk delete
const { data: countResult } = await api.data.count("logs", {
  level: { $eq: "debug" },
});
console.log(`About to delete ${countResult.count} records`);

REST API Approach

Delete One

curl -X DELETE http://localhost:7654/api/data/posts/1

Delete with Auth

curl -X DELETE http://localhost:7654/api/data/posts/1 \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Delete Many

# Delete all archived posts
curl -X DELETE "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22archived%22%7D"

# URL-decoded where: {"status":"archived"}

React Integration

Delete Button

import { useApp } from "bknd/react";
import { useState } from "react";

function DeleteButton({ postId, onDeleted }: { postId: number; onDeleted?: () => void }) {
  const { api } = useApp();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleDelete() {
    if (!confirm("Are you sure you want to delete this post?")) {
      return;
    }

    setLoading(true);
    setError(null);

    const { ok, error: apiError } = await api.data.deleteOne("posts", postId);

    setLoading(false);

    if (ok) {
      onDeleted?.();
    } else {
      setError(apiError.message);
    }
  }

  return (
    <>
      <button onClick={handleDelete} disabled={loading}>
        {loading ? "Deleting..." : "Delete"}
      </button>
      {error && <p className="error">{error}</p>}
    </>
  );
}

With SWR Revalidation

import { mutate } from "swr";

function useDeletePost() {
  const { api } = useApp();

  async function deletePost(id: number) {
    const { ok, error } = await api.data.deleteOne("posts", id);

    if (ok) {
      // Revalidate list
      mutate("posts");
    }

    return { ok, error };
  }

  return { deletePost };
}

Optimistic Delete

function useOptimisticDelete() {
  const { api } = useApp();
  const [posts, setPosts] = useState<Post[]>([]);

  async function deletePost(id: number) {
    // Optimistic: remove immediately
    const originalPosts = [...posts];
    setPosts((prev) => prev.filter((p) => p.id !== id));

    // Actual delete
    const { ok } = await api.data.deleteOne("posts", id);

    if (!ok) {
      // Rollback on failure
      setPosts(originalPosts);
    }

    return { ok };
  }

  return { posts, deletePost };
}

Full Example

import { Api } from "bknd";

const api = new Api({ host: "http://localhost:7654" });

// Authenticate
await api.auth.login({ email: "admin@example.com", password: "password" });

// Simple delete
const { ok, data } = await api.data.deleteOne("posts", 1);
if (ok) {
  console.log("Deleted:", data.title);
}

// Delete with verification
const postId = 5;
const { data: post } = await api.data.readOne("posts", postId);
if (post) {
  await api.data.deleteOne("posts", postId);
}

// Bulk delete: remove old archived posts
const { data: deleted } = await api.data.deleteMany("posts", {
  status: { $eq: "archived" },
  created_at: { $lt: "2023-01-01" },
});
console.log("Deleted", deleted.length, "old archived posts");

// Cleanup expired sessions
await api.data.deleteMany("sessions", {
  expires_at: { $lt: new Date().toISOString() },
});

Common Patterns

Instead of permanent deletion, mark as deleted:

// Soft delete: set timestamp
async function softDelete(api: Api, entity: string, id: number) {
  return api.data.updateOne(entity, id, {
    deleted_at: new Date().toISOString(),
  });
}

// Restore soft-deleted record
async function restore(api: Api, entity: string, id: number) {
  return api.data.updateOne(entity, id, {
    deleted_at: null,
  });
}

// Query non-deleted records
async function findActive(api: Api, entity: string, query = {}) {
  return api.data.readMany(entity, {
    ...query,
    where: {
      ...query.where,
      deleted_at: { $isnull: true },
    },
  });
}

// Permanently delete soft-deleted records older than 30 days
async function purgeDeleted(api: Api, entity: string) {
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  return api.data.deleteMany(entity, {
    deleted_at: { $lt: thirtyDaysAgo.toISOString() },
  });
}

Delete with Confirmation

async function deleteWithConfirmation(
  api: Api,
  entity: string,
  id: number,
  confirm: () => Promise<boolean>
) {
  const { data } = await api.data.readOne(entity, id);
  if (!data) {
    return { ok: false, error: { message: "Record not found" } };
  }

  const confirmed = await confirm();
  if (!confirmed) {
    return { ok: false, error: { message: "Cancelled by user" } };
  }

  return api.data.deleteOne(entity, id);
}

Cascade Delete (Manual)

When Bknd doesn't auto-cascade, delete children first:

async function cascadeDelete(api: Api, userId: number) {
  // Delete children first
  await api.data.deleteMany("posts", { author_id: { $eq: userId } });
  await api.data.deleteMany("comments", { user_id: { $eq: userId } });
  await api.data.deleteMany("likes", { user_id: { $eq: userId } });

  // Then delete parent
  return api.data.deleteOne("users", userId);
}

Batch Delete with Progress

async function batchDelete(
  api: Api,
  entity: string,
  ids: number[],
  onProgress?: (done: number, total: number) => void
) {
  const results = [];

  for (let i = 0; i < ids.length; i++) {
    const result = await api.data.deleteOne(entity, ids[i]);
    results.push(result);
    onProgress?.(i + 1, ids.length);
  }

  return results;
}

// Usage
const idsToDelete = [1, 5, 12, 23];
await batchDelete(api, "posts", idsToDelete, (done, total) => {
  console.log(`Deleted ${done}/${total}`);
});

Archive Before Delete

async function archiveAndDelete(
  api: Api,
  sourceEntity: string,
  archiveEntity: string,
  id: number
) {
  // Read current record
  const { data: record } = await api.data.readOne(sourceEntity, id);
  if (!record) {
    return { ok: false, error: { message: "Record not found" } };
  }

  // Create archive copy
  await api.data.createOne(archiveEntity, {
    ...record,
    original_id: record.id,
    archived_at: new Date().toISOString(),
  });

  // Delete original
  return api.data.deleteOne(sourceEntity, id);
}

Conditional Delete

async function deleteIf(
  api: Api,
  entity: string,
  id: number,
  condition: (record: any) => boolean
) {
  const { data } = await api.data.readOne(entity, id);
  if (!data) {
    return { ok: false, error: { message: "Record not found" } };
  }

  if (!condition(data)) {
    return { ok: false, error: { message: "Condition not met" } };
  }

  return api.data.deleteOne(entity, id);
}

// Only delete if draft
await deleteIf(api, "posts", 1, (post) => post.status === "draft");

Common Pitfalls

Record Not Found

Problem: Delete returns no data or error.

Fix: Check if record exists first:

const { data: existing } = await api.data.readOne("posts", id);
if (!existing) {
  console.error("Post not found");
  return;
}
await api.data.deleteOne("posts", id);

Foreign Key Constraint

Problem: FOREIGN KEY constraint failed when deleting parent.

Fix: Delete or unlink children first:

// Option 1: Delete children
await api.data.deleteMany("comments", { post_id: { $eq: postId } });
await api.data.deleteOne("posts", postId);

// Option 2: Unlink children (if nullable FK)
await api.data.updateMany(
  "comments",
  { post_id: { $eq: postId } },
  { post_id: null }
);
await api.data.deleteOne("posts", postId);

Not Checking Response

Problem: Assuming success without verification.

Fix: Always check ok:

// Wrong
await api.data.deleteOne("posts", id);
console.log("Deleted!");  // Might have failed!

// Correct
const { ok, error } = await api.data.deleteOne("posts", id);
if (!ok) {
  console.error("Delete failed:", error.message);
  return;
}
console.log("Deleted!");

Accidental Mass Delete

Problem: Deleting more records than intended.

Fix: Always use specific where clause and verify count:

// Dangerous - might delete more than expected
await api.data.deleteMany("posts", { status: { $eq: "draft" } });

// Safer - check count first
const { data: count } = await api.data.count("posts", {
  status: { $eq: "draft" }
});
console.log(`About to delete ${count.count} posts`);
if (count.count > 100) {
  throw new Error("Too many records - aborting");
}

Missing Auth

Problem: Unauthorized error.

Fix: Authenticate before deleting:

await api.auth.login({ email, password });
// or
api.updateToken(savedToken);

await api.data.deleteOne("posts", id);

No Undo for Hard Delete

Problem: Accidentally deleted important data.

Fix: Use soft delete for recoverable data:

// Instead of hard delete
await api.data.deleteOne("posts", id);

// Use soft delete
await api.data.updateOne("posts", id, {
  deleted_at: new Date().toISOString(),
});

Deleting Without Confirmation

Problem: Users accidentally delete data.

Fix: Always confirm destructive actions:

// In frontend
function handleDelete(id: number) {
  if (!confirm("Delete this post? This cannot be undone.")) {
    return;
  }
  api.data.deleteOne("posts", id);
}

Verification

After deleting, verify the record is gone:

const { ok } = await api.data.deleteOne("posts", 1);

if (ok) {
  const { data } = await api.data.readOne("posts", 1);
  console.log("Record exists:", data !== null);  // Should be false
}

Or via admin panel: Admin Panel > Data > Select Entity > Search for deleted record.

DOs and DON'Ts

DO:

  • Check ok before assuming success
  • Verify record exists before deleting
  • Use soft delete for user data
  • Handle foreign key constraints
  • Confirm with user before destructive actions
  • Check count before bulk deletes
  • Consider archiving before permanent delete

DON'T:

  • Assume deleteOne always succeeds
  • Delete parent before children with FK constraints
  • Hard delete user data without confirmation
  • Forget to revalidate caches after delete
  • Use deleteMany without specific where clause
  • Delete without authentication on protected entities
  • bknd-crud-read - Verify records before deleting
  • bknd-crud-update - Update instead of delete (soft delete)
  • bknd-crud-create - Recreate accidentally deleted records
  • bknd-define-relationship - Understand FK constraints
  • bknd-bulk-operations - Large-scale delete patterns

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon