defineResource()
The runtime resource definition.
defineResource() is the Grit admin's killer primitive. One call gives you list + create + edit + delete pages — DataTable, FormBuilder, routing, everything. This lesson covers the shape; next two lessons cover the components it renders.
The call
import { defineResource } from '@/components/admin/define-resource'import { Package } from 'lucide-react'export default defineResource({model: 'products',label: 'Products',icon: Package,columns: [{ key: 'name', label: 'Name', searchable: true },{ key: 'price', label: 'Price', format: 'money' },{ key: 'stock_quantity', label: 'Stock', format: 'number' },{ key: 'created_at', label: 'Created', format: 'date' },],form: [{ name: 'name', type: 'text', required: true },{ name: 'price', type: 'money', required: true, currency: 'USD' },{ name: 'stock_quantity', type: 'number', default: 0 },{ name: 'description', type: 'textarea' },{ name: 'is_active', type: 'switch', default: true },],})
That's the whole admin page. List page + create form + edit form + delete confirmation — all generated.
What "model" means
model: 'products' maps to your API's /api/products/* CRUD endpoints. Grit's code generator (covered in the Concepts course) already wired these when you ran grit generate resource Product.
Convention: model name is the plural form used in API routes.
The 4 generated pages
/resources/products List (DataTable)/resources/products/new Create (FormBuilder)/resources/products/[id]/edit Edit (FormBuilder)/resources/products/[id] View (read-only)
Four pages from one function call.
Column format helpers
Each column's format tells the DataTable how to render the value:
format: 'money' → $1,234.56format: 'date' → 2026-06-12 (or relative: "2 hours ago")format: 'number' → 1,234format: 'boolean' → ✓ or ✗format: 'badge' → colored pill (statuses)format: 'image' → thumbnail
Add custom formatters by passing a function: format: (v) => v.toUpperCase().
Form field types
text regular text inputtextarea multi-linenumber HTML number inputmoney formatted with currencyswitch boolean toggleselect dropdown with options[]date date pickerfile upload — saves to S3 via the Grit storage package
{ name: 'thing', component: MyCustom }. The FormBuilder slots it in.Customising — pass overrides
defineResource({model: 'products',columns: [...],form: [...],// Override page actionsactions: [{ label: 'Export CSV', icon: Download, onClick: exportProducts },],// Add bulk operationsbulkActions: [{ label: 'Mark as featured', onClick: bulkFeature },],// Custom filtersfilters: [{ type: 'select', name: 'status', label: 'Status', options: [...] },],})
Each override slots into the generated page where you'd expect — actions go in the top-right toolbar, filters in the filter bar, bulk actions appear when rows are selected.
Quick check
Try it
Build a working products admin page:
- Ensure Product is generated and migrated (Concepts course ch.4).
- Create
apps/admin/app/resources/products/page.tsxwithdefineResource. - Visit
localhost:3001/resources/products. - Create three products via the New button.
- Edit one; delete one.
Paste the list view (with your three products) in notes.md.
What's next
Next two lessons go deeper on the rendered components: DataTable (sort, filter, paginate) then FormBuilder (the field types in action).
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub