Admin CRUD Interface
Website Rebuild with Next.js
Chapter 10 · Admin CRUD Interface
This chapter builds the real admin interface by hand — Next.js ships no automatic scaffolding of any kind — and finally pays off Chapter 2's own deferred cost: recalculating an entire subtree's own fullPath when a page is moved.
No Free Admin Panel
| Next.js | Django | Laravel | Rails | Astro | Express | |
|---|---|---|---|---|---|---|
| Free admin/scaffold? | No — hand-built | Yes — admin.site.register(), live | No by default (Nova/Filament optional) | Partial — rails generate scaffold, one-time codegen | No — hand-built | No — hand-built |
Django's own admin app is a live, runtime-introspected interface generated from the model definition itself — genuinely different in kind from Rails' scaffold generator, which produces real, editable source files once and then stops helping. Next.js has neither. Every line of this chapter's own interface is written directly.
The Parent Picker: A Searchable Flat List
fullPath, filterable as the admin types. This chapter uses the flat list: at this site's realistic page count, typing a few letters to jump straight to the right page beats clicking through several levels of a tree, and a flat list is genuinely simpler to build in the first place. Laravel Rebuild's, Astro Rebuild's, and Rails Rebuild's own Chapter 10s all reuse this identical reasoning, citing it directly as "established since the Next.js rebuild's own Chapter 10."
Reparenting: Paying Off Chapter 2's Own Deferred Cost
$queryRaw's recursive CTE — a real improvement over walking the tree level by level in application code, since Chapter 6's own include is fixed-depth and couldn't express "every descendant, however deep" at all. The write step stays honestly sequential regardless: each descendant's own new fullPath depends on its own parent's already-updated path, so the loop above genuinely can't be parallelized. The improvement is real, and it's real specifically at the discovery step, not the whole operation.
include, where, and the rest — has no way to express a recursive query at all; recursion isn't part of its vocabulary. $queryRaw is Prisma's own documented escape hatch for exactly this situation: real SQL, still returned as typed rows, for the specific cases the query builder genuinely can't reach.
Delete Refusal: From Raw Exception to a Friendly Message
P2003 is Prisma's own documented error code for a foreign key constraint violation — the raw database refusal from Chapter 2's own onDelete: Restrict, surfacing here as a real exception that has to be caught, not a clean return value. This matches Laravel's try/catch around a raw QueryException and Astro's own equivalent, since all three sit on a database-level constraint. Django's PROTECT and Rails' dependent: :restrict_with_error are both application-level instead — Django still raises an exception to catch, but Rails' own destroy simply returns false with no exception-catching needed at all, a genuine ergonomic advantage specific to Rails' own API design, not a universal trait of every application-level approach.
Hands-On Exercises
Build ParentPicker and confirm typing a partial path (e.g. "prog") correctly filters the list down to only matching pages, client-side, with no new server request per keystroke.
📄 View solutionMove a page with at least two real levels of descendants to a new parent, and confirm every descendant's own fullPath is correctly recalculated — not just the moved page itself.
📄 View solutionAttempt to delete a page that still has at least one child, and confirm the friendly error message appears rather than an unhandled Prisma exception crashing the request.
📄 View solutionChapter 10 Quick Reference
- No free admin panel — hand-built, matching Laravel/Astro/Express's own shared finding
- Searchable flat-list parent picker — the design decision every sibling course credits to this chapter
movePage()/ recursive CTE via$queryRaw— one query discovers every descendant; the write step stays honestly sequentialP2003— Prisma's own foreign key error code, caught and converted into a real UI message- Next chapter: Deployment