This is what I learned from App Router: Mutating Data. The diagrams greatly helped me understand the CRUD operations better.

In this section, I explored
- C: create✨
- U: update♻️
- D: delete🧹
💻Server Function
Before diving into the details, it’s important to understand that Next.js has 2 types of components:
- client component
- server component

Since the C, U, D all interact with the database, they are implemented as server function. There are 2 patterns to define a server function in Next.js.
1️⃣ mark the function
import Button from './Button';
function EmptyNote () {
async function createNoteAction() {
// Server Function
'use server'; //👈this directive marks this function as server function
await db.notes.create();
}
return <Button onClick={createNoteAction}/>;
}2️⃣ mark the file
Alternatively, you can create a file like actions.ts and add the directive 'use server' at the top,
'use server';
export async function createInvoice(formData: FormData) {}
export async function updateInvoice(id: string, formData: FormData) {}
export async function deleteInvoice(id: string) {}all functions in this file will be server functions. This approach keeps all server functions organized in a single file, which is a great way to maintain clarity. 👍
🗂️Schema
The data submitted through forms is FormData. To ensure proper type validation, for example a number twelve is 12 but not "12", we use Zod to define and validate the schema.
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(['pending', 'paid']),
date: z.string(),
});✨Create
Let’s examine the “Create” workflow.

There are 2 main routes:
- 🟥User clicks the “Create Invoice” button, they are navigated to
...com/dashboard/invoices/create - 🟦After the form is filled out and submitted to the database, the user is redirected back to
...com/dashboard/invoices
The first route is straightforward, using a <Link/> component to navigate. I am particularly interested in the second one:
- how do we ensure the data is fresh when returning to the
...com/dashboard/invoices - how do we go back to
...com/dashboard/invoices
The answer lies in the server function createInvoice() which is bound to the form.
<form action={createInvoice}>
//all the data wrapped in FormData
</form>When user clicks the “Create Invoice” button in 🟦, this server function will:
- parse the form data
- execute the sql code to insert the data into the database
revalidatePath()will “refresh” the table, ensuring it displays the latestredirect()will navigate back to the invoices page
export async function createInvoice(formData: FormData) {
//parse FormData
//..
//SQL
await sql`...`;
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}♻️Update

The “Update” workflow is quite similar.
export async function updateInvoice(id: string, formData: FormData) {
//parse FormData
//..
//SQL
await sql`...`;
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}One key aspect to highlight is Dynamic Routes. The invoice UUID c6a48b88-67b8-4747-8fb1-295aa1074a58 is dynamic and only known at runtime. To handle this, we use the following file structure.
- 📂invoice
- 📂[id]
- 📂edit
- 📄page.tsx
This structure results in a URL like ..com/dashboard/invoices/[id]/edit
🧹Delete
The “Delete” workflow follows a similar pattern, but there is an important consideration common to all Create/Update/Delete operations.
The following approach is not safe.
import { updateInvoice } from '@/app/lib/actions';
export function DeleteInvoice({ id }: { id: string }) {
return (
<form action={deleteInvoice(id)}>
//...
</form>
);
}Since the Create/Update/Delete buttons are all client component, passing the id directly exposes it in the client-side code, which is dangerous💀!
Instead, use the bind() to securely pass the id to the server function:
import { updateInvoice } from '@/app/lib/actions';
export function DeleteInvoice({ id }: { id: string }) {
const deleteInvoiceWithId = deleteInvoice.bind(null, id);
return (
<form action={deleteInvoiceWithId}>
//...
</form>
);
}Using bind() ensures the id is safely encoded and not exposed in the client-side HTML.