Jackson KasiMost startups don't die because of a bad idea. They die because of bad engineering velocity. If your...
Most startups don't die because of a bad idea. They die because of bad engineering velocity.
If your engineering team is spending days wiring up basic data tables, pagination, and sorting for your internal dashboards, you are bleeding money.
I know, because I audited my own workflow. Every time a PM asked for a new view in our B2B SaaS, it meant:
pageIndex, sortBy, and filters.?sort=-createdAt&status=active.It was a brittle, soul-crushing translation layer. And it had to be rewritten for every single table.
I decided to kill the translation layer entirely. I built TableCraft.
Here is the deep dive into the architecture, how it handles complex edge cases, and why building "systems" beats building "components."
TanStack Table is the gold standard for headless UI. Drizzle ORM is the gold standard for type-safe SQL.
But they don't speak the same language.
When a user filters a column on the client, TanStack spits out an array of filter objects. You have to send that to your backend, validate it, and write a manual Drizzle where: and(eq(...), like(...)) clause.
If you change a column name in your database, your frontend table breaks. If you add a new relation, you have to rewrite the API.
"We were spending an hour wiring up a single table. That is unacceptable for a team trying to find Product-Market Fit."
I structured TableCraft as a Turborepo monorepo to separate the core engine from the framework-specific adapters.
The goal was simple: Define the rules on the backend, and let the frontend automatically inherit them.
@tablecraft/engine)
Instead of writing manual API routes, you define the table behavior at the schema level.
import { defineTable } from "@tablecraft/engine";
import * as schema from "./db/schema";
// We define exactly what is allowed. Zero boilerplate.
export const usersConfig = defineTable(schema.users)
.hide("password") // Never leaks to the client
.search("email", "name")
.sort("-createdAt");
I built adapters for the most aggressive modern backend frameworks: Hono, Elysia, Express, and Next.js.
The adapter takes your defineTable configs and dynamically generates the REST endpoints. It handles input validation, parses the URL query params, and safely executes the Drizzle query.
import { createHonoApp } from "@tablecraft/adapter-hono";
// This single line generates all the filtering, sorting, and pagination APIs.
const app = createHonoApp({ db, schema, configs: { users: usersConfig } });
@tablecraft/table)
On the frontend, you don't write useReactTable from scratch anymore. The client SDK reads the metadata from the auto-generated API and constructs the table dynamically.
import { DataTable, createTableCraftAdapter } from "@tablecraft/table";
const adapter = createTableCraftAdapter({
baseUrl: "/api/engine",
table: "users",
});
// Drop this in. You're done.
export function UsersPage() {
return <DataTable adapter={adapter} />;
}
Building a basic table generator is easy. Building one that survives an enterprise production environment is hard. Here is how TableCraft handles the complexity:
core/cursorPagination builder that automatically switches to cursor-based queries for massive tables.Orders by the User.email. The core/relationBuilder recursively maps nested Drizzle relations so the frontend can query across joins effortlessly.core/softDelete and core/aggregationBuilder.By bridging the gap between Drizzle and TanStack natively, the time to deploy a fully functional, highly complex data table went from ~1 hour of manual wiring to exactly 5 minutes.
The frontend engineers no longer bother the backend engineers for new filter parameters. It is all inherited automatically.
The engine currently supports Hono, Next.js, and Express. The next phase is expanding the ecosystem:
If you are tired of writing the same pagination logic every week, stop building components. Start building systems.
👉 Fork TableCraft on GitHub (jacksonkasi1/TableCraft)
Let me know in the comments how your team currently handles the Drizzle-to-Frontend translation layer!