(This is Part 5 of my series on building scalable infrastructure. Catch up on Part 1: Bridging Drizzle & TanStack, Part 2: The Engine-Adapter Pattern, Part 3: Dynamic Query Compilers, and Part 4: Cross-Relational Search).
📖 The Chef, The Waiter, and The Menu
Imagine running a restaurant. The Chef (your Backend) invents a brand new dish.
If your restaurant operates like most tech stacks, the Chef just throws the food out the window. The Waiter (your Frontend) has to catch the food, inspect it, guess what the ingredients are, and write down a manual description for the customer. If the Chef changes the recipe tomorrow, the Waiter serves the wrong description and the customer gets angry.
The Smart Way: The Chef prints a Menu (Metadata). When the dish changes, the Menu changes automatically. The Waiter just reads the Menu.
🛑 The Problem: The Type-Sync Hell
If you are a full-stack developer, you know the pain of adding a single column to a database table.
- You add
statusto your Postgres database. - You update your Drizzle ORM schema.
- You update your Zod validation schema.
- You update your backend API response type.
- You switch to the frontend repository.
- You update the TypeScript interface for the API fetcher.
- You update the TanStack Table
columnsdefinition array so the table actually renders the new column.
You are acting as a human translator between your backend and your frontend. This is exactly how engineering teams slow down and bugs slip into production.
When I built TableCraft, I wanted to eliminate steps 3 through 7 entirely.
If I define a table on the backend, the frontend should just know how to render it. I needed a Backend-Driven Metadata Protocol.
Here is how you build a system where your frontend automatically writes its own code based on your backend database.
🧠 The Core Concept: Schema Reflection
In traditional REST APIs, the server only returns data.
In a Metadata Protocol, the server returns data AND the shape of the data.
When we define a table in TableCraft, we pass it the Drizzle schema object. Drizzle objects contain hidden metadata about the SQL types (e.g., is this a varchar, a boolean, or a timestamp?).
We can extract this metadata and serve it via a dedicated /meta endpoint.
1. The Metadata Builder
Instead of just parsing queries, our engine needs to inspect the database schema and build a JSON representation of what is allowed.
import { getTableColumns } from "drizzle-orm";
export function buildTableMetadata(tableDefinition: any) {
const columns = getTableColumns(tableDefinition.schema);
const metadata = {
columns: {},
searchableFields: tableDefinition.allowedSearchFields,
sortableFields: tableDefinition.allowedSortFields,
};
for (const [key, col] of Object.entries(columns)) {
// If we hid the column (like 'password'), skip it!
if (tableDefinition.hiddenColumns.includes(key)) continue;
metadata.columns[key] = {
type: col.dataType, // 'string', 'number', 'boolean'
nullable: col.notNull === false,
primaryKey: col.primary,
};
}
return metadata;
}
Now, every TableCraft endpoint automatically exposes a GET /api/engine/users/meta route. It returns a pure JSON schema of what the frontend is allowed to do.
🏗️ The Codegen CLI: Writing Code with Code
Serving JSON is great, but we want strict TypeScript types on our frontend. We don't want to parse JSON at runtime; we want intellisense in VS Code.
To solve this, we build a Codegen CLI.
The CLI fetches the /meta endpoint from your local development server, reads the JSON, and writes a .ts file directly into your frontend codebase.
// packages/codegen/src/generator.ts
import fs from "fs";
export async function generateFrontendTypes(apiUrl: string, outputDir: string) {
// 1. Fetch the metadata from the backend
const response = await fetch(`${apiUrl}/meta`);
const meta = await response.json();
// 2. Start generating the TypeScript string
let tsCode = `// ⚠️ AUTO-GENERATED BY TABLECRAFT. DO NOT EDIT.\n\n`;
tsCode += `export interface UsersRow {\n`;
for (const [colName, colMeta] of Object.entries(meta.columns)) {
const tsType = mapSqlTypeToTypeScript(colMeta.type);
const optionalMarker = colMeta.nullable ? "?" : "";
tsCode += ` ${colName}${optionalMarker}: ${tsType};\n`;
}
tsCode += `}\n`;
// 3. Write it to the React frontend directory!
fs.writeFileSync(`${outputDir}/tablecraft-types.ts`, tsCode);
}
Now, instead of manually typing interface User { ... }, the developer just runs bun run tablecraft generate.
The backend dictates the types. The frontend obeys.
🚀 Zero-Config UI (The Holy Grail)
Because the frontend now has access to this metadata, the React client doesn't need you to manually define TanStack Table columns.
The @tablecraft/table React package reads the generated metadata and constructs the UI dynamically:
import { DataTable, createTableCraftAdapter } from "@tablecraft/table";
// The adapter reads the generated metadata internally!
const adapter = createTableCraftAdapter({
baseUrl: "/api/engine",
table: "users",
});
// You do not pass columns. You do not pass types.
// It renders the table, the filter dropdowns, and the search box automatically.
export function UsersPage() {
return <DataTable adapter={adapter} />;
}
If you add an is_active boolean column to your Postgres database, you just restart your server. The metadata updates, the codegen CLI pulls the new types, and your React table instantly displays an is_active column with a True/False filter toggle.
Zero manual frontend work.
🛑 The Shift to Backend-Driven UI
If you are a creator building full-stack tools, you need to think about Reflection.
Your backend is the source of truth. Stop making your users manually duplicate that truth on the frontend. Expose the metadata, build a codegen tool, and let the machines write the boilerplate.
If you want to see exactly how the Codegen CLI generates TanStack Table definitions and Zod schemas on the fly, dig into the open-source codebase here:
👉 Study the Metadata Codegen Protocol in TableCraft (GitHub) (jacksonkasi1/TableCraft)
🔄 What's Next in the Series?
We now have a dynamic backend compiler and an auto-generated frontend. But how do they talk to each other safely?
When a user clicks "Sort by Date", how does the React state seamlessly convert into the ?sort=-date URL parameter without creating messy useEffect loops in your components?
In Part 6, we will dive into The Smart Client SDK—how to build framework-agnostic fetch adapters that handle state synchronization, debouncing, and secure input serialization.
Hit the Follow button so you don't miss it.
Question for the full-stack devs: Are you currently using a Codegen tool (like GraphQL Codegen, tRPC, or OpenAPI), or are you still hand-typing your frontend interfaces? Let's discuss in the comments.
Top comments (0)