defineResource()

The runtime resource definition.

9 minmedium

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

apps/admin/app/resources/products/page.tsx
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.56
format: 'date' → 2026-06-12 (or relative: "2 hours ago")
format: 'number' → 1,234
format: '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 input
textarea multi-line
number HTML number input
money formatted with currency
switch boolean toggle
select dropdown with options[]
date date picker
file upload — saves to S3 via the Grit storage package
Eight field types covers ~95% of admin forms. When you need a one-off custom input (multi-step wizard, dependent dropdown), pass a React component: { name: 'thing', component: MyCustom }. The FormBuilder slots it in.

Customising — pass overrides

defineResource({
model: 'products',
columns: [...],
form: [...],
// Override page actions
actions: [
{ label: 'Export CSV', icon: Download, onClick: exportProducts },
],
// Add bulk operations
bulkActions: [
{ label: 'Mark as featured', onClick: bulkFeature },
],
// Custom filters
filters: [
{ 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

You want the Products admin page to show a 'Total inventory value' summary at the top. What's the right approach?

Try it

Build a working products admin page:

  1. Ensure Product is generated and migrated (Concepts course ch.4).
  2. Create apps/admin/app/resources/products/page.tsx with defineResource.
  3. Visit localhost:3001/resources/products.
  4. Create three products via the New button.
  5. 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