Resource Definitions
Resources are the building blocks of the Grit admin panel. Define your data model once in TypeScript and get a complete admin interface — data table, forms, filters, sidebar navigation, and dashboard widgets.
The defineResource() API
Every admin resource is created with the defineResource() function. It accepts a single configuration object that describes the resource name, API endpoint, table columns, form fields, and optional dashboard widgets.
interface ResourceConfig {
// Identity
name: string // Singular name, PascalCase (e.g. "Invoice")
slug?: string // URL slug, auto-derived if omitted (e.g. "invoices")
endpoint: string // Go API base URL (e.g. "/api/invoices")
icon: string // Lucide icon name (e.g. "FileText")
label?: { // Display labels (auto-derived from name)
singular: string // "Invoice"
plural: string // "Invoices"
}
// Table configuration
table: TableConfig
// Form configuration
form: FormConfig
// Dashboard widgets (optional)
dashboard?: DashboardConfig
// Access control (optional)
permissions?: {
roles: string[] // Roles that can access this resource
}
}Resource Configuration
Name, Slug, and Endpoint
The name field is the singular PascalCase name of your resource (e.g. "Invoice"). Grit auto-derives the plural form, URL slug, and display labels from it. You can override any of these with the slug and label fields.
The endpoint must match the API route registered in your Go backend. For a resource named "Invoice", the endpoint is typically /api/invoices. The admin panel appends /:id for single-item operations automatically.
Icon
Pass any Lucide icon name as a string. The sidebar renders it next to the resource label. Common choices: "Users", "FileText", "ShoppingCart", "CreditCard", "Mail".
Table Configuration
The table object controls how data appears in the resource's data table — which columns are shown, how they are formatted, what filters are available, and which row actions are enabled.
interface TableConfig {
columns: ColumnDef[]
filters?: FilterDef[]
pageSize?: number // Default: 20
defaultSort?: {
key: string
direction: 'asc' | 'desc'
}
searchable?: boolean // Enable global search (default: true)
actions?: Action[] // 'create' | 'edit' | 'delete' | 'view' | 'export'
bulkActions?: BulkAction[] // 'delete' | 'export' | custom string
}Column Definitions
Each column maps a field from the API response to a table column. The column definition controls sorting, searching, formatting, and custom rendering.
interface ColumnDef {
key: string // JSON field name (supports dot notation: "customer.name")
label: string // Column header text
sortable?: boolean // Allow sorting by this column
searchable?: boolean // Include in global search
format?: ColumnFormat // Display format
badge?: BadgeConfig // Badge-style rendering for status fields
relation?: string // Load from a related resource
hidden?: boolean // Hidden by default (show/hide toggle)
}
type ColumnFormat =
| 'text' // Plain text (default)
| 'number' // Formatted number (1,234)
| 'currency' // Currency ($1,234.00)
| 'boolean' // Check/X icon
| 'date' // Formatted date (Jan 15, 2026)
| 'relative' // Relative time (3 hours ago)
| 'badge' // Colored badge
| 'image' // Thumbnail image
interface BadgeConfig {
[value: string]: {
color: 'green' | 'yellow' | 'red' | 'blue' | 'purple' | 'gray'
label: string
}
}Column Format Types
The format property determines how the cell value is rendered:
| Format | Input | Rendered As |
|---|---|---|
| text | "Hello World" | Hello World |
| number | 1234567 | 1,234,567 |
| currency | 99.5 | $99.50 |
| boolean | true / false | Green check / Red X icon |
| date | "2026-01-15T..." | Jan 15, 2026 |
| relative | "2026-01-15T..." | 3 weeks ago |
| badge | "active" | Colored pill with label |
| image | "https://..." | 32x32 rounded thumbnail |
Badge Columns
For status-like fields, use the badge property instead of (or in addition to) format. It maps each possible value to a colored label:
{
key: 'status',
label: 'Status',
badge: {
paid: { color: 'green', label: 'Paid' },
pending: { color: 'yellow', label: 'Pending' },
overdue: { color: 'red', label: 'Overdue' },
},
}Filters
Filters appear above the data table and let users narrow down results. Three filter types are available:
interface FilterDef {
key: string // Field to filter on
type: 'select' | 'date-range' | 'number-range'
label?: string // Display label (defaults to key)
options?: string[] // For 'select' type
}
// Example filters
filters: [
{ key: 'status', type: 'select', options: ['paid', 'pending', 'overdue'] },
{ key: 'created_at', type: 'date-range' },
{ key: 'amount', type: 'number-range' },
]Form Configuration
The form object defines the fields that appear in create and edit modals. Grit supports a wide range of field types and validates input using Zod schemas from the shared package.
interface FormConfig {
fields: FieldDef[]
layout?: 'single' | 'two-column' // Default: 'single'
validation?: string // Zod schema name from shared package
}
interface FieldDef {
key: string // JSON field name
label: string // Display label
type: FieldType // Input type
required?: boolean // Required field (default: false)
placeholder?: string // Placeholder text
default?: any // Default value for create mode
options?: string[] | { label: string; value: string }[]
min?: number // For number type
max?: number // For number type
step?: number // For number type
rows?: number // For textarea type
span?: 'full' | 'half' // Column span in two-column layout
}
type FieldType =
| 'text'
| 'textarea'
| 'number'
| 'select'
| 'date'
| 'datetime'
| 'toggle'
| 'checkbox'
| 'radio'
| 'image' // Single image upload
| 'images' // Multiple image upload
| 'video' // Single video upload
| 'videos' // Multiple video upload
| 'file' // Single file upload
| 'files' // Multiple file upload
| 'richtext' // Rich text editor
| 'relationship-select' // Select from related resource
| 'multi-relationship-select' // Multi-select from related resourceForm Field Types
Each field type renders a different input component. Here is a quick reference:
| Type | Component | Extra Props |
|---|---|---|
| text | Text input | placeholder |
| textarea | Multi-line textarea | rows, placeholder |
| number | Numeric input | min, max, step |
| select | Dropdown select | options |
| date | Date picker | -- |
| datetime | Date & time picker | -- |
| toggle | Toggle switch | -- |
| checkbox | Checkbox | -- |
| radio | Radio button group | options |
| image | Single image upload | accept |
| images | Multiple image upload | accept |
| video | Single video upload | accept |
| videos | Multiple video upload | accept |
| file | Single file upload | accept |
| files | Multiple file upload | accept |
| richtext | Rich text editor | -- |
| relationship-select | Resource select | resource, displayKey |
| multi-relationship-select | Resource multi-select | resource, displayKey |
Dashboard Configuration
Each resource can optionally define widgets that appear on the admin dashboard. Widgets pull data from your Go API and display stats, charts, or activity feeds.
interface DashboardConfig {
widgets: WidgetDef[]
}
interface WidgetDef {
type: 'stat' | 'chart' | 'activity'
label: string
query: string // Server-side query (e.g. "sum:amount")
format?: 'number' | 'currency' | 'percent'
color?: string // Accent color for the widget
// Chart-specific
chartType?: 'line' | 'bar' // For type: 'chart'
}Complete Example
Here is a full resource definition for a Posts resource with table columns, filters, form fields, and dashboard widgets:
import { defineResource } from '@grit/admin'
export default defineResource({
name: 'Post',
endpoint: '/api/posts',
icon: 'FileText',
label: {
singular: 'Blog Post',
plural: 'Blog Posts',
},
table: {
columns: [
{ key: 'id', label: 'ID', sortable: true, hidden: true },
{ key: 'title', label: 'Title', sortable: true, searchable: true },
{ key: 'author.name', label: 'Author', relation: 'author' },
{ key: 'category', label: 'Category', sortable: true },
{ key: 'status', label: 'Status', badge: {
published: { color: 'green', label: 'Published' },
draft: { color: 'yellow', label: 'Draft' },
archived: { color: 'gray', label: 'Archived' },
}},
{ key: 'views', label: 'Views', format: 'number', sortable: true },
{ key: 'published_at', label: 'Published', format: 'date' },
{ key: 'created_at', label: 'Created', format: 'relative' },
],
filters: [
{ key: 'status', type: 'select', options: ['published', 'draft', 'archived'] },
{ key: 'category', type: 'select', options: ['tech', 'design', 'business'] },
{ key: 'published_at', type: 'date-range' },
],
pageSize: 25,
defaultSort: { key: 'created_at', direction: 'desc' },
actions: ['create', 'edit', 'delete', 'export'],
bulkActions: ['delete', 'export'],
},
form: {
layout: 'two-column',
fields: [
{ key: 'title', label: 'Title', type: 'text', required: true,
placeholder: 'Enter post title', span: 'full' },
{ key: 'slug', label: 'Slug', type: 'text',
placeholder: 'auto-generated-from-title' },
{ key: 'category', label: 'Category', type: 'select',
options: ['tech', 'design', 'business'] },
{ key: 'content', label: 'Content', type: 'richtext', span: 'full' },
{ key: 'excerpt', label: 'Excerpt', type: 'textarea', rows: 3,
span: 'full' },
{ key: 'status', label: 'Status', type: 'select',
options: ['published', 'draft', 'archived'], default: 'draft' },
{ key: 'featured', label: 'Featured Post', type: 'toggle' },
{ key: 'cover_image', label: 'Cover Image', type: 'file', span: 'full' },
],
},
dashboard: {
widgets: [
{ type: 'stat', label: 'Total Posts', query: 'count', color: 'purple' },
{ type: 'stat', label: 'Published', query: 'count:status=published',
color: 'green' },
{ type: 'stat', label: 'Total Views', query: 'sum:views',
format: 'number' },
{ type: 'chart', label: 'Posts Over Time', chartType: 'line',
query: 'count:by:month' },
],
},
permissions: {
roles: ['admin', 'editor'],
},
})Resource Registry
After creating a resource definition file, you need to register it in the resource registry. This file is the single source of truth for all admin resources — the sidebar, router, and dashboard all read from it.
import users from './users'
import posts from './posts'
import invoices from './invoices'
// All registered resources — sidebar and routes are generated from this array
export const resources = [users, posts, invoices]
// Helper to look up a resource by slug
export function getResource(slug: string) {
return resources.find((r) => r.slug === slug)
}When you run grit generate resource, the CLI automatically adds the import and registration to this file using marker-based code injection. You never need to edit it manually unless you want to reorder the sidebar items.
How Resources Auto-Register in the Sidebar
The admin sidebar component imports the resources array from the registry and renders a navigation link for each one. Each resource'sicon and label.plural (or auto-derived name) are used as the sidebar text. The active item is highlighted based on the current URL path.
The order of resources in the registry array determines their order in the sidebar. System pages (Dashboard, Jobs, Files, Settings) are rendered separately and always appear at fixed positions.