Better Tables

Architecture

Layers of the Better Tables monorepo and how data moves from schema to UI.

Better Tables keeps database concerns, table contracts, and React rendering in separate packages so you can swap adapters and own your UI source.

Package map

PackageRole
@better-tables/coreColumn builders, filter/sort/pagination managers, adapter contract types. No React, no DB driver.
@better-tables/adapters-drizzleSchema introspection, JOIN/filter/pagination query generation for Postgres/MySQL/SQLite.
@better-tables/adapters-toolkitORM-agnostic machinery for adapter authors.
@better-tables/uiReact table/filter components (shadcn-style). Distributed via CLI copy, not npm.
@better-tables/clibetter-tables init copies UI into a consumer project.

Data flow

defineTable() columns
        ↓
betterTables({ database: adapter })
        ↓
adapter.fetchData / getFacetedValues / …
        ↓
@better-tables/ui (copied) renders + manages client state
        ↓
optional URL sync (filters, sort, page)

Flagship API surface

Prefer these entry points everywhere:

  • betterTables({ database, defaults? }) — factory for fetchData, define, cell-edit actions
  • defineTable<typeof tables>()('name', (t) => ({ columns })) — RSC-safe table definitions
  • t.text / t.number / t.date / t.option / … — path builders with fluent modifiers
  • tables.fetchData(table, params) — server/query boundary

For tables without a database — playgrounds, tests, client-only arrays — memoryAdapter(rows) runs the same pipeline in memory, and defineTableRow<Row>() defines a table from hand-built columns. See Custom adapters and Columns.

Plugins (data hooks)

betterTables({ plugins: [...] }) takes plugins — instance-level data hooks that wrap the query path. Each plugin can implement beforeFetch (rewrite the params before the adapter sees them) and/or afterFetch (transform the result). They run in array order around tables.fetchData; a throwing hook fails the fetch (nothing is swallowed), and the no-plugin path is unaffected.

import { betterTables, logPlugin } from '@better-tables/core';

const tables = betterTables({
  database: adapter,
  plugins: [
    logPlugin({ onFetch: ({ table, rowCount, total }) => track(table, rowCount, total) }),
    {
      name: 'only-active',
      // AND `activeFilter` onto whatever filters are present — preserving an
      // existing group (a bare `[]` fallback would silently drop an OR tree).
      beforeFetch: ({ params }) => ({
        ...params,
        filters: {
          kind: 'group',
          logic: 'and',
          children: [
            ...(Array.isArray(params.filters)
              ? params.filters
              : params.filters
                ? [params.filters]
                : []),
            activeFilter,
          ],
        },
      }),
    },
  ],
});

Plugins vs. modules. Plugins are npm-distributed data hooks on betterTables({ plugins }) (this section). Modules are copied UI source added with better-tables add — see UI & CLI. Different tiers, kept distinct.

Authoritative demos

Shipped examples live under apps/marketing — especially src/lib/demo/support/ and the routes under /examples. Those use the same 0.6 API this handbook teaches.

On this page