@talltydev/crud-kit
v0.4.18
Published
A powerful React CRUD framework with ZenStack integration and shadcn/ui compatibility
Maintainers
Readme
@talltydev/crud-kit
A powerful, declarative React CRUD framework with ZenStack integration and shadcn/ui compatibility.
Features
- 🚀 Declarative CRUD UI - Build full-featured CRUD interfaces with minimal code
- 🔗 ZenStack Integration - Auto-generated TanStack Query hooks from Prisma schema
- 🎨 shadcn/ui Compatible - Uses copied UI components for full customization
- 📊 Multiple Views - Table, Grid, List views with declarative
<Crud.Column>definitions - 🔍 Advanced Search - Smart search with field-specific syntax and filters
- 🔐 Permission Control - UI-level access control with
withPermissions() - ⚡ Type-Safe - Full TypeScript support with Prisma model types
- 🎯 Framework Agnostic - Works with Next.js App Router, Pages Router, and Vite
Quick Start
Installation (Recommended: CLI)
The recommended way to use CRUD Kit is through the @talltydev/cli which automatically:
- Copies shadcn/ui components to your project
- Configures TypeScript path aliases
- Sets up ZenStack integration
- Creates example CRUD pages
# Create a new project with CRUD Kit
npx @talltydev/cli create my-app
# Or add to existing project
npx @talltydev/cli add crudManual Installation (Advanced)
If you prefer to install manually:
npm install @talltydev/crud-kit
# or
pnpm add @talltydev/crud-kit⚠️ Important: Manual installation requires:
- shadcn/ui components copied to
src/components/ui/ - TypeScript path alias
@/components/ui/*configured intsconfig.json - ZenStack with TanStack Query integration
See Manual Setup for detailed instructions.
Architecture
Component Strategy: Copy Components
CRUD Kit follows the shadcn/ui philosophy of copying components directly into your project:
your-project/
├── src/
│ ├── components/
│ │ └── ui/ # shadcn/ui components (copied)
│ │ ├── button.tsx
│ │ ├── table.tsx
│ │ └── ...
│ └── ...
└── node_modules/
└── @talltydev/crud-kit/ # CRUD logic (imported)Why this approach?
✅ Full Customization - Modify UI components directly in your project ✅ No Version Lock - Not tied to specific shadcn/ui versions ✅ Flexibility - Mix copied components with NPM packages ✅ Type Safety - TypeScript resolves components at compile time
How It Works
CRUD Kit uses path aliases instead of direct NPM imports:
// CRUD Kit source code
import { Button } from '@/components/ui/button' // ✅ Path alias
// NOT: import { Button } from '@talltydev/ui' // ❌ Direct importDuring Build (tsup with esbuild plugin):
// dist/index.js - path aliases preserved
import { Button } from '@/components/ui/button' // External referenceIn Your Project (TypeScript resolves):
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// Resolves to: src/components/ui/button.tsxThis means:
- 🎯 CRUD Kit components use your project's UI components
- 📦 No UI components bundled in
@talltydev/crud-kitNPM package - 🎨 100% customizable without forking CRUD Kit
Usage
1. Define Resource (Data Layer)
Create a resource configuration that connects your Prisma model with ZenStack hooks:
💡 导入路径说明:tallty-kit 基于 monorepo 架构,数据库相关代码统一从共享的
@repo/databasepackage 导入。
// apps/web/src/resources/user-resource.ts
import { defineResource } from '@talltydev/crud-kit'
import type { User } from '@repo/database/types'
import * as userHooks from '@repo/database/hooks/user'
import metadata from '@repo/database/hooks/__model_meta'
export const userResource = defineResource<User>('User')
// Connect ZenStack hooks
.withHooks(userHooks)
// Schema metadata for auto-generation
.withMetadata(metadata.models.user)
// Search configuration
.withSearchableFields(['name', 'email'])
// Default query options
.withBase({
list: {
orderBy: [{ createdAt: 'desc' }]
}
})
// Permission control
.withPermissions({
canCreate: (session) => !!session?.user,
canEdit: (session) => session?.user?.role === 'admin',
canDelete: (session) => session?.user?.role === 'admin'
})
// Computed fields
.withComputed({
displayName: {
compute: (user) => user.name || user.email
}
})2. Build CRUD UI (Presentation Layer)
Use the <Crud> component with declarative <Crud.Column> definitions:
// src/app/(admin)/users/page.tsx
'use client'
import { Crud } from '@talltydev/crud-kit'
import { userResource } from '@/resources/user-resource'
export default function UsersPage() {
return (
<Crud
resource={userResource}
actions={{
create: { as: 'modal', size: 'lg' },
edit: { as: 'modal', size: 'lg' },
view: { as: 'drawer', size: 'lg' }
}}
>
{/* Header */}
<Crud.Header>
<Crud.Title>Users</Crud.Title>
<Crud.Actions>
<Crud.CreateButton>Add User</Crud.CreateButton>
</Crud.Actions>
</Crud.Header>
{/* Toolbar */}
<Crud.Toolbar>
<Crud.ToolbarLeft>
<Crud.Search
fields={['name', 'email']}
placeholder="Search users..."
/>
<Crud.Filters>
<Crud.Filter field="role" label="Role" type="select" />
<Crud.DateRange field="createdAt" label="Created" />
</Crud.Filters>
</Crud.ToolbarLeft>
<Crud.ToolbarRight>
<Crud.ViewSwitcher views={['table', 'grid']} default="table" />
<Crud.ExportButton />
</Crud.ToolbarRight>
</Crud.Toolbar>
{/* Views System */}
<Crud.Views>
{/* Table View */}
<Crud.View name="table">
<Crud.Table>
<Crud.Column field="name" label="Name" sortable />
<Crud.Column field="email" label="Email" sortable />
<Crud.Column field="role" label="Role" />
<Crud.Column field="createdAt" label="Created" format="date" sortable />
<Crud.RowActions>
<Crud.ViewAction as="drawer" />
<Crud.EditAction as="modal" />
<Crud.DeleteAction
confirm
confirmTitle="Delete User"
confirmDescription="This action cannot be undone."
/>
</Crud.RowActions>
</Crud.Table>
</Crud.View>
{/* Grid View */}
<Crud.View name="grid">
<Crud.Grid
columns={{ default: 1, sm: 2, md: 3, lg: 4 }}
renderItem={(user) => (
<div className="rounded-lg border p-4">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
)}
/>
</Crud.View>
</Crud.Views>
{/* Pagination */}
<Crud.Pagination />
</Crud>
)
}3. Key Concepts
Resource vs UI Separation
Resource (Model Layer) - defineResource():
- ZenStack hooks binding
- Default query parameters
- Permissions
- Computed fields
- Data transformations
CRUD Component (Presentation Layer) - <Crud>:
- UI layout and styling
- Column definitions with
<Crud.Column> - View switching (table/grid/list)
- Search and filters
- User interactions
Declarative Column Definitions
Instead of programmatic column config, use JSX:
{/* ✅ Declarative - Recommended */}
<Crud.Table>
<Crud.Column field="name" label="Name" sortable />
<Crud.Column field="email" label="Email" sortable />
<Crud.Column field="status" label="Status" />
</Crud.Table>
{/* ❌ Programmatic - Old API */}
<Crud.Table
columns={[
{ field: 'name', label: 'Name', sortable: true },
{ field: 'email', label: 'Email', sortable: true }
]}
/>Benefits:
- More readable and maintainable
- Better TypeScript inference
- Easier to customize individual columns
- Follows React patterns
API Reference
defineResource<T>(modelName: string)
Create a type-safe resource configuration.
Methods
.withHooks(hooks: ZenStackHooks)
Bind ZenStack-generated TanStack Query hooks.
import * as userHooks from '@repo/database/hooks/user'
defineResource<User>('User')
.withHooks(userHooks)Required hooks:
findMany- List recordsfindUnique- Get single recordcreate- Create recordupdate- Update recorddelete- Delete record
.withMetadata(metadata: ModelMetadata)
Provide schema metadata for auto-generation features.
import metadata from '@repo/database/hooks/__model_meta'
defineResource<User>('User')
.withMetadata(metadata.models.user)Enables:
- Automatic field type detection
- Enum options extraction
- Validation rules
- Default values
.withSearchableFields(fields: string[])
Configure which fields are searchable.
defineResource<User>('User')
.withSearchableFields(['name', 'email', 'username'])Enables smart search syntax:
email:john@- Search specific fieldname:"John Doe"- Exact phraserole:admin status:active- Multiple fields
.withBase(config: BaseQueryConfig)
Set default query parameters.
defineResource<User>('User')
.withBase({
list: {
orderBy: [{ createdAt: 'desc' }],
where: { deletedAt: null }
}
}).withPermissions(permissions: PermissionConfig)
Control UI-level access.
defineResource<User>('User')
.withPermissions({
canCreate: (session) => !!session?.user,
canEdit: (session) => session?.user?.role === 'admin',
canDelete: (session) => session?.user?.role === 'admin',
canView: (session, record) => {
// Row-level permission
return session?.user?.id === record.id || session?.user?.role === 'admin'
}
})Note: These are UI permissions. Database-level security is handled by ZenStack access policies.
.withFieldMeta(meta: FieldMetaConfig)
Configure field options and value mappings.
defineResource<User>('User')
.withFieldMeta({
role: {
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
{ label: 'Guest', value: 'guest' }
],
valueMap: {
'admin': 'Administrator',
'user': 'Standard User',
'guest': 'Guest User'
}
}
}).withComputed(computed: ComputedFieldsConfig)
Add derived fields.
defineResource<User>('User')
.withComputed({
displayName: {
compute: (user) => user.name || user.email
},
isAdmin: {
compute: (user) => user.role === 'admin'
}
})Computed fields are:
- Cached automatically
- Type-safe
- Accessible in UI like regular fields
.withCallbacks(callbacks: CallbacksConfig)
Add data transformation hooks.
defineResource<User>('User')
.withCallbacks({
beforeCreate: async (data) => {
// Set defaults
data.role = data.role || 'user'
return data
},
afterList: async (users) => {
console.log(`Loaded ${users.length} users`)
return users
},
beforeUpdate: async (data) => {
// Validate or transform
return data
}
})Available callbacks:
beforeCreate,afterCreatebeforeUpdate,afterUpdatebeforeDelete,afterDeletebeforeList,afterList
<Crud> Component
Main CRUD interface component.
<Crud
resource={userResource}
actions={{
create: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' },
edit: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' },
view: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' }
}}
>
{/* Children: Header, Toolbar, Views, Pagination */}
</Crud>Sub-Components
<Crud.Column>
Define table columns declaratively:
<Crud.Column
field="name" // Field name from model
label="Name" // Display label
sortable // Enable sorting
format="date" // Format type: 'text' | 'date' | 'datetime' | 'time' | 'number' | 'currency' | 'boolean' | 'percent' | 'email' | 'url' | 'json' | 'relation' | 'count' | 'richtext'
render={(value, row) => ( // Custom render function
<span>{value}</span>
)}
/>Field Formats:
The format prop automatically transforms field values for display:
| Format | Description | Example |
|--------|-------------|---------|
| text | Plain text (default) | Any string value |
| date | Date only (year-month-day) | 2024-01-15 |
| datetime | Date and time | 2024-01-15 14:30:00 |
| time | Time only | 14:30:00 |
| number | Formatted number with localization | 1,234.56 |
| currency | Currency format | ¥1,234.56 (CNY) |
| boolean | Badge with Yes/No | Yes / No |
| percent | Percentage | 45.2% |
| email | Clickable mailto link | [email protected] |
| url | Clickable external link | https://example.com |
| json | Formatted JSON in code block | { "key": "value" } |
| relation | Display related object name | Shows name, title, or label field |
| count | Count of array items | 5 items |
| richtext | HTML content with styling | Rich text with formatted images |
Rich Text Format (richtext):
The richtext format renders HTML content with proper typography and image styling:
<Crud.Column field="content" label="Content" format="richtext" />Features:
- ✅ Tailwind Typography: Elegant prose styling
- ✅ Responsive Images: Auto-sized images with borders and rounded corners
- ✅ Dark Mode: Automatic dark theme support
- ✅ Auto-detection: Fields named
content,description, orbodywith HTML are auto-detected
For custom styling:
import { renderRichText } from '@talltydev/crud-kit'
<Crud.Column
field="content"
render={({ value }) => renderRichText(value, {
className: 'prose-lg [&_img]:shadow-lg'
})}
/>Security Note: Rich text uses dangerouslySetInnerHTML. Ensure content is from trusted sources or properly sanitized.
<Crud.Table>
Table view with declarative columns:
<Crud.Table>
<Crud.Column field="id" label="ID" />
<Crud.Column field="name" label="Name" sortable />
<Crud.RowActions>
<Crud.ViewAction />
<Crud.EditAction />
<Crud.DeleteAction confirm />
</Crud.RowActions>
</Crud.Table><Crud.Grid> and <Crud.List>
Alternative view layouts:
<Crud.Grid
columns={{ default: 1, sm: 2, md: 3, lg: 4 }}
gap={4}
renderItem={(item) => <CustomCard data={item} />}
/>
<Crud.List
renderItem={(item) => <CustomListItem data={item} />}
/>Manual Setup
If you're not using the CLI, follow these steps:
1. Install shadcn/ui Components
# Initialize shadcn/ui
npx shadcn@latest init
# Install required components
npx shadcn@latest add button table card dialog drawer form input label select badge2. Configure TypeScript Path Aliases
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/ui/*": ["./src/components/ui/*"],
"@/lib/*": ["./src/lib/*"]
}
}
}3. Install CRUD Kit and Dependencies
npm install @talltydev/crud-kit @tanstack/react-query @zenstackhq/runtime4. Set Up ZenStack
# Install ZenStack
npm install -D zenstack @zenstackhq/tanstack-query
# Initialize ZenStack
npx zenstack init// schema.zmodel
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
plugin hooks {
provider = '@zenstackhq/tanstack-query'
output = 'src/hooks/model'
target = 'react'
version = 'v5'
}
model User {
id String @id @default(cuid())
name String?
email String @unique
role String @default("user")
createdAt DateTime @default(now())
@@allow('all', true) // Configure your access policies
}# Generate Prisma client and ZenStack hooks
npx zenstack generate5. Wrap App with Providers
// Next.js App Router: app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { CrudAdaptersProvider, createNextAdapter } from '@talltydev/crud-kit'
import { useRouter } from 'next/navigation'
import { getSession } from '@/lib/auth'
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()
const adapter = createNextAdapter({
router: {
push: router.push,
replace: router.replace,
back: router.back,
refresh: router.refresh,
getPath: () => window.location.pathname
},
getSession
})
return (
<QueryClientProvider client={queryClient}>
<CrudAdaptersProvider value={adapter}>
{children}
</CrudAdaptersProvider>
</QueryClientProvider>
)
}// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Framework Adapters
Next.js App Router
import { createNextAdapter } from '@talltydev/crud-kit/adapters/next'
import { useRouter } from 'next/navigation'
const adapter = createNextAdapter({
router: {
push: router.push,
replace: router.replace,
back: router.back,
refresh: router.refresh,
getPath: () => window.location.pathname
},
getSession: async () => {
const session = await getServerSession()
return session
}
})Vite / React Router
import { createViteAdapter } from '@talltydev/crud-kit/adapters/vite'
import { useNavigate, useLocation } from 'react-router-dom'
const navigate = useNavigate()
const location = useLocation()
const adapter = createViteAdapter({
router: {
push: navigate,
replace: (path) => navigate(path, { replace: true }),
back: () => navigate(-1),
getPath: () => location.pathname
},
getSession: async () => {
// Your auth logic
return session
}
})FAQ
Why path aliases instead of NPM imports?
Short answer: To follow shadcn/ui's copy-components philosophy.
CRUD Kit uses components from your project (src/components/ui/), not from an NPM package. This gives you:
- ✅ Full customization - Modify any UI component directly
- ✅ No version conflicts - Not locked to specific shadcn/ui versions
- ✅ Type safety - TypeScript resolves to your actual components
- ✅ Zero runtime overhead - No component proxies or injection
Can I use @talltydev/ui NPM package instead?
No. As of v0.2.0, @talltydev/ui is deprecated. CRUD Kit only supports the copy-components approach.
Migration: If you're upgrading from an older version:
# Remove old UI package
npm uninstall @talltydev/ui
# Install shadcn/ui components
npx shadcn@latest add button table card dialog drawer form input label select badge
# Update imports in your code
# Before: import { Button } from '@talltydev/ui/button'
# After: Already using @/components/ui/button (no changes needed in CRUD Kit)Do I need to configure tsconfig paths manually?
No, if using CLI - The @talltydev/cli automatically configures paths.
Yes, if manual install - You must add to tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}How does CRUD Kit find my UI components?
During development (TypeScript):
@/components/ui/button → src/components/ui/button.tsx (via tsconfig paths)At runtime (JavaScript):
Your bundler (Next.js/Vite) resolves path aliases using tsconfig.jsonIn CRUD Kit package (dist files):
// dist/index.js - path alias preserved as external reference
import { Button } from '@/components/ui/button'What if I get "Cannot find module '@/components/ui/button'"?
Cause: TypeScript path alias not configured or shadcn/ui component not installed.
Fix:
# 1. Verify tsconfig.json has paths
cat tsconfig.json # Should show "@/*": ["./src/*"]
# 2. Install missing component
npx shadcn@latest add button
# 3. Verify component exists
ls src/components/ui/button.tsxHow do permissions work with ZenStack?
Two layers of security:
Database-level (ZenStack access policies in
.zmodel):model User { // ... @@allow('read', true) @@allow('create', auth() != null) @@allow('update', auth() == this) @@allow('delete', auth().role == 'admin') }UI-level (Resource permissions in
defineResource):.withPermissions({ canCreate: (session) => !!session?.user, canDelete: (session) => session?.user?.role === 'admin' })
Best practice: Rely on ZenStack for security, use Resource permissions for UX (hiding disabled actions).
Can I use CRUD Kit without ZenStack?
Not currently. CRUD Kit is designed to work with ZenStack-generated hooks.
However, you could potentially adapt it by:
- Creating hook adapters that match ZenStack's API
- Wrapping TanStack Query hooks manually
This is not officially supported. Consider using TanStack Table directly if you don't want ZenStack.
How do I customize the generated forms?
Forms are generated from Prisma schema metadata. To customize:
// Option 1: Use field metadata
defineResource<User>('User')
.withFieldMeta({
bio: {
type: 'textarea', // Override auto-detected type
placeholder: 'Tell us about yourself...'
}
})
// Option 2: Use custom form components
<Crud.Form>
<Crud.Field name="name" />
<Crud.Field name="bio" as="textarea" rows={4} />
<Crud.Field name="avatar" as={CustomFileUpload} />
</Crud.Form>How do I add custom actions?
Use the actions system:
<Crud resource={userResource}>
<Crud.Toolbar>
<Crud.ToolbarRight>
<Button onClick={customAction}>Custom Action</Button>
<Crud.ExportButton />
</Crud.ToolbarRight>
</Crud.Toolbar>
<Crud.Table>
<Crud.Column field="name" label="Name" />
<Crud.RowActions>
<Crud.EditAction />
<Crud.DeleteAction />
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
customRowAction(row.original)
}}
>
Custom
</Button>
</Crud.RowActions>
</Crud.Table>
</Crud>Can I use CRUD Kit with Prisma directly (without ZenStack)?
CRUD Kit requires ZenStack because it relies on:
- Auto-generated TanStack Query hooks (
useFindManyUser,useCreateUser, etc.) - Model metadata for form generation
- Type-safe model definitions
ZenStack is a thin layer over Prisma that adds:
- Access control policies
- React Query hooks generation
- No runtime overhead for basic operations
Development
See DEV_GUIDE.md for local development setup with YALC.
Contributing
Contributions are welcome! Please read our contributing guidelines first.
License
MIT © Tallty
