.agents/skills/bknd-crud-read/SKILL.md
Use when querying and retrieving data from Bknd entities via SDK or REST API. Covers readOne, readMany, readOneBy, filtering (where clause), sorting, field selection, loading relations (with/join), and response handling.
npx skillsauth add cameronapak/freedom-stack-v3 bknd-crud-readInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Query and retrieve data from your Bknd database using the SDK or REST API.
bknd-create-entity, bknd-crud-create)UI steps: Admin Panel > Data > Select Entity > Browse/search records
import { Api } from "bknd";
const api = new Api({
host: "http://localhost:7654",
});
// If auth required:
api.updateToken("your-jwt-token");
Use readOne(entity, id, query?):
const { ok, data, error } = await api.data.readOne("posts", 1);
if (ok) {
console.log("Post:", data.title);
} else {
console.error("Not found or error:", error.message);
}
Use readOneBy(entity, query) to find by field value:
const { data } = await api.data.readOneBy("users", {
where: { email: { $eq: "[email protected]" } },
});
if (data) {
console.log("Found user:", data.id);
}
Use readMany(entity, query?):
const { ok, data, meta } = await api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
offset: 0,
});
console.log(`Found ${meta.total} total, showing ${data.length}`);
The response object structure:
type ReadResponse = {
ok: boolean;
data?: T | T[]; // Single object or array
meta?: { // For readMany
total: number; // Total matching records
limit: number; // Current page size
offset: number; // Current offset
};
error?: {
message: string;
code: string;
};
};
// Equality (implicit or explicit)
{ where: { status: "published" } }
{ where: { status: { $eq: "published" } } }
// Not equal
{ where: { status: { $ne: "deleted" } } }
// Numeric comparisons
{ where: { age: { $gt: 18 } } } // Greater than
{ where: { age: { $gte: 18 } } } // Greater or equal
{ where: { price: { $lt: 100 } } } // Less than
{ where: { price: { $lte: 100 } } } // Less or equal
// LIKE patterns (% = wildcard)
{ where: { title: { $like: "%hello%" } } } // Contains (case-sensitive)
{ where: { title: { $ilike: "%hello%" } } } // Contains (case-insensitive)
// Convenience methods
{ where: { name: { $startswith: "John" } } }
{ where: { email: { $endswith: "@gmail.com" } } }
{ where: { bio: { $contains: "developer" } } }
// In array
{ where: { id: { $in: [1, 2, 3] } } }
// Not in array
{ where: { type: { $nin: ["archived", "deleted"] } } }
// Is NULL
{ where: { deleted_at: { $isnull: true } } }
// Is NOT NULL
{ where: { published_at: { $isnull: false } } }
// AND (implicit - multiple fields)
{
where: {
status: { $eq: "published" },
category: { $eq: "news" },
}
}
// OR
{
where: {
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
]
}
}
// Combined AND/OR
{
where: {
category: { $eq: "news" },
$or: [
{ status: { $eq: "published" } },
{ author_id: { $eq: currentUserId } },
]
}
}
// Object syntax (preferred)
{ sort: { created_at: "desc" } }
{ sort: { name: "asc", created_at: "desc" } } // Multi-sort
// String syntax (- prefix = descending)
{ sort: "-created_at" }
{ sort: "name,-created_at" }
Reduce payload by selecting specific fields:
const { data } = await api.data.readMany("users", {
select: ["id", "email", "name"],
});
// data[0] only has id, email, name
// Simple - load relations
{ with: "author" }
{ with: ["author", "comments"] }
{ with: "author,comments" }
// Nested with subquery options
{
with: {
author: {
select: ["id", "name", "avatar"],
},
comments: {
where: { approved: { $eq: true } },
sort: { created_at: "desc" },
limit: 10,
with: ["user"], // Nested loading
},
}
}
Result structure:
const { data } = await api.data.readOne("posts", 1, {
with: ["author", "comments"],
});
console.log(data.author.name); // Nested object
console.log(data.comments[0].text); // Nested array
Use join to filter by related fields:
const { data } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" }, // Filter by joined field
},
sort: "-author.created_at", // Sort by joined field
});
| Feature | with | join |
|---------|--------|--------|
| Query method | Separate queries | SQL JOIN |
| Return structure | Nested objects | Flat (unless also with) |
| Use case | Load related data | Filter by related fields |
| Performance | Multiple queries | Single query |
// Page 1 (records 0-19)
{ limit: 20, offset: 0 }
// Page 2 (records 20-39)
{ limit: 20, offset: 20 }
// Generic page formula
{ limit: pageSize, offset: (page - 1) * pageSize }
Default limit is 10 if not specified.
async function paginate<T>(
entity: string,
page: number,
pageSize: number,
query: object = {}
) {
const { data, meta } = await api.data.readMany(entity, {
...query,
limit: pageSize,
offset: (page - 1) * pageSize,
});
return {
data,
page,
pageSize,
total: meta.total,
totalPages: Math.ceil(meta.total / pageSize),
hasNext: page * pageSize < meta.total,
hasPrev: page > 1,
};
}
# Basic
curl http://localhost:7654/api/data/posts
# With query params
curl "http://localhost:7654/api/data/posts?limit=20&offset=0&sort=-created_at"
# With where clause
curl "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22published%22%7D"
curl http://localhost:7654/api/data/posts/1
For complex queries, use POST to /api/data/:entity/query:
curl -X POST http://localhost:7654/api/data/posts/query \
-H "Content-Type: application/json" \
-d '{
"where": {"status": {"$eq": "published"}},
"sort": {"created_at": "desc"},
"limit": 20,
"with": ["author"]
}'
# Get user's posts
curl http://localhost:7654/api/data/users/1/posts
import { useApp } from "bknd/react";
import { useEffect, useState } from "react";
function PostsList() {
const { api } = useApp();
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
}).then(({ data }) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
import { useApp } from "bknd/react";
import useSWR from "swr";
function PostsList() {
const { api } = useApp();
const { data: posts, isLoading, error } = useSWR(
"posts-published",
() => api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
}).then((r) => r.data)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading posts</div>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function PostDetail({ postId }: { postId: number }) {
const { api } = useApp();
const { data: post, isLoading } = useSWR(
`post-${postId}`,
() => api.data.readOne("posts", postId, {
with: ["author", "comments"],
}).then((r) => r.data)
);
if (isLoading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author?.name}</p>
<div>{post.content}</div>
<h2>Comments ({post.comments?.length})</h2>
</article>
);
}
import { useState, useMemo } from "react";
import { useApp } from "bknd/react";
import useSWR from "swr";
import { useDebouncedValue } from "@mantine/hooks"; // or custom hook
function SearchPosts() {
const { api } = useApp();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const { data: results, isLoading } = useSWR(
debouncedSearch ? `search-${debouncedSearch}` : null,
() => api.data.readMany("posts", {
where: { title: { $ilike: `%${debouncedSearch}%` } },
limit: 10,
}).then((r) => r.data)
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search posts..."
/>
{isLoading && <p>Searching...</p>}
<ul>
{results?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Get single post with relations
const { data: post } = await api.data.readOne("posts", 1, {
with: {
author: { select: ["id", "name"] },
tags: true,
},
});
console.log(post.title, "by", post.author.name);
// Find user by email
const { data: user } = await api.data.readOneBy("users", {
where: { email: { $eq: "[email protected]" } },
});
// List published posts with pagination
const { data: posts, meta } = await api.data.readMany("posts", {
where: {
status: { $eq: "published" },
deleted_at: { $isnull: true },
},
sort: { created_at: "desc" },
limit: 10,
offset: 0,
with: ["author"],
});
console.log(`Page 1 of ${Math.ceil(meta.total / 10)}`);
// Complex query: posts by admin authors in category
const { data: adminPosts } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" },
category: { $eq: "announcements" },
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
],
},
select: ["id", "title", "created_at"],
sort: "-created_at",
});
const { data } = await api.data.count("posts", {
status: { $eq: "published" },
});
console.log(`${data.count} published posts`);
const { data } = await api.data.exists("users", {
email: { $eq: "[email protected]" },
});
if (data.exists) {
console.log("Email already registered");
}
// Always exclude soft-deleted
const { data } = await api.data.readMany("posts", {
where: { deleted_at: { $isnull: true } },
});
// Using readManyByReference
const { data: userPosts } = await api.data.readManyByReference(
"users", userId, "posts",
{ sort: { created_at: "desc" }, limit: 10 }
);
Problem: Assuming data exists.
Fix: Always check ok or handle undefined:
// Wrong
const { data } = await api.data.readOne("posts", 999);
console.log(data.title); // Error if not found!
// Correct
const { ok, data } = await api.data.readOne("posts", 999);
if (!ok || !data) {
console.log("Post not found");
return;
}
console.log(data.title);
Problem: Using operators incorrectly.
Fix: Wrap values in operator object:
// Wrong
{ where: { age: ">18" } }
// Correct
{ where: { age: { $gt: 18 } } }
Problem: Filtering by related field without join.
Fix: Add join clause:
// Wrong - won't work
{ where: { "author.role": { $eq: "admin" } } }
// Correct - add join
{
join: ["author"],
where: { "author.role": { $eq: "admin" } }
}
Problem: Loading relations in a loop.
Fix: Use with to load relations in batch:
// Wrong - N+1 queries
const { data: posts } = await api.data.readMany("posts");
for (const post of posts) {
const { data: author } = await api.data.readOne("users", post.author_id);
}
// Correct - single batch query
const { data: posts } = await api.data.readMany("posts", {
with: ["author"],
});
posts.forEach(p => console.log(p.author.name));
Problem: $like is case-sensitive.
Fix: Use $ilike for case-insensitive:
// Case-sensitive (may miss results)
{ where: { title: { $like: "%React%" } } }
// Case-insensitive
{ where: { title: { $ilike: "%react%" } } }
Test queries in admin panel first:
Or log response in code:
const response = await api.data.readMany("posts", query);
console.log("Response:", JSON.stringify(response, null, 2));
DO:
ok before accessing datawith for loading relationsjoin when filtering by related fields$ilike for case-insensitive text searchselect to reduce payload sizeDON'T:
join when filtering by relation fields$like when case-insensitive neededwithdevelopment
Use btca (Better Context App) to efficiently query and learn from the bknd backend framework. Use when working with bknd for (1) Understanding data module and schema definitions, (2) Implementing authentication and authorization, (3) Setting up media file handling, (4) Configuring adapters (Node, Cloudflare, etc.), (5) Learning from bknd source code and examples, (6) Debugging bknd-specific issues
development
Use when configuring webhook integrations in Bknd. Covers receiving incoming webhooks via HTTP triggers, sending outgoing webhooks with FetchTask, event-triggered webhooks on data changes, signature verification, retry patterns, and async processing.
development
Use when encountering Bknd errors, getting error messages, something not working, or needing quick fixes. Covers error code reference, quick solutions, and common mistake patterns.
tools
Use when writing tests for Bknd applications, setting up test infrastructure, creating unit/integration tests, or testing API endpoints. Covers in-memory database setup, test helpers, mocking, and test patterns.