TL;DR
- Build a complete CRUD data grid app using Next.js (Server Components + Server Actions), Prisma ORM (type-safe DB access), and AG Grid (inline editing, sorting, virtual scroll).
- Server Components fetch data on the server; Server Actions let clients call server-side functions like regular async functions — no REST API needed.
- Prisma provides auto-generated TypeScript types from your schema, making DB queries fully type-safe.
- AG Grid's
applyTransactionAPI enables optimistic updates — the UI updates instantly, and rolls back on failure.
Key Concepts
Next.js: Server Components & Server Actions
Server Components run on the server and can call business logic directly:
// app/page.tsx — Server Component
export default async function Home() {
const orders = await getOrders(); // Direct DB call
return <OrderGrid orders={orders} />;
}
Server Actions let client components call server functions with a simple 'use server' directive:
'use server';
export async function createOrderAction(input: CreateOrderInput) {
return createOrder(input);
}
The client calls it like a normal function — Next.js handles the HTTP layer internally.
Prisma ORM: Type-Safe Database Access
Define your schema in Prisma's DSL, then generate a fully typed client:
model Order {
id Int @id @default(autoincrement())
price Int
qty Int
}
CRUD operations are concise and type-safe:
// Create
await prisma.order.create({ data: { price, qty } });
// Read with filters
await prisma.order.findMany({
where: { price: { gt: 1000 } },
skip: 20,
take: 20,
});
// Update
await prisma.order.update({ where: { id }, data: { price, qty } });
// Delete
await prisma.order.delete({ where: { id } });
AG Grid: Optimistic Updates with applyTransaction
The optimistic update pattern — update the UI first, then sync with the server:
async function handleCreate() {
const tempId = -Date.now();
const tempRow: Order = { id: tempId, price: 0, qty: 0 };
// Optimistic: add temp row immediately
gridRef.current?.api.applyTransaction({ add: [tempRow] });
try {
const created = await createOrderAction({ price: 0, qty: 0 });
// Replace temp row with real data
gridRef.current?.api.applyTransaction({
remove: [tempRow],
add: [created],
});
} catch {
// Rollback on failure
gridRef.current?.api.applyTransaction({ remove: [tempRow] });
}
}
The same pattern applies to update (rollback cell value on failure) and delete (re-add the row on failure).
Code Examples
Setting up the AG Grid component with inline editing and a delete button:
const colDefs: ColDef<Order>[] = useMemo(() => [
{ field: 'id', editable: false, sort: 'asc' },
{ field: 'price', editable: true },
{ field: 'qty', editable: true },
{
headerName: '',
width: 100,
cellRenderer: (p: ICellRendererParams<Order>) => (
<button onClick={() => handleDelete(p.data!)}>Delete</button>
),
},
], []);
<AgGridReact
ref={gridRef}
rowData={orders}
columnDefs={colDefs}
getRowId={(p) => String(p.data.id)}
onCellValueChanged={handleCellValueChanged}
/>
This is a summary of my original 3-part series written in Korean.
For the full articles with step-by-step setup instructions, check out the originals:
👉 Part 1: Next.js Basics
👉 Part 2: Prisma ORM Setup & CRUD
👉 Part 3: AG Grid & Optimistic Updates
Top comments (0)