WDD 430: Web Full-Stack Development

Mutating Data

Overview

In the previous activity, you implemented search and pagination using URL search params and Next.js APIs. In this activity, you will continue building the portfolio project by adding the ability to create, update, and delete project information.

This activity focuses on the core patterns from Next.js Learn Chapter 11, adapted to your portfolio context:

Prepare

Server Actions

React Server Actions are asynchronous functions that run on the server. In Next.js, they let you mutate data without creating separate REST endpoints for each write operation. In your portfolio app, this means you can create, update, and delete project records directly from form submissions.

Server-side mutations keep credentials and database logic on the backend. Even so, all incoming data should still be validated before it is written to the database.

// app/lib/actions.ts
'use server';

import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';

export async function deleteProject(id: string) {
  await sql`DELETE FROM projects WHERE id = ${id}`;
  revalidatePath('/projects');
}

Using Forms with Server Actions

A form can invoke a Server Action directly through the action attribute. On submit, Next.js passes the native FormData object to your server function.

// app/projects/create/page.tsx
import { createProject } from '@/app/lib/actions';

export default function Page() {
  return (
    <form action={createProject}>
      <label htmlFor="title">Title</label>
      <input id="title" name="title" required />

      <label htmlFor="description">Description</label>
      <textarea id="description" name="description" required />

      <label htmlFor="technologies">Technologies (comma-separated)</label>
      <input id="technologies" name="technologies" required />

      <button type="submit">Save Project</button>
    </form>
  );
}

This pattern supports progressive enhancement, so the form can still submit in constrained browser environments.

Best Practices with FormData and Type Validation

FormData values are untyped and may be missing or malformed. Parse and validate input before writing to the database. Schema tools such as zod make this process reliable.

// app/lib/actions.ts
'use server';

import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

const ProjectFormSchema = z.object({
  title: z.string().min(2),
  description: z.string().min(10),
  technologies: z.string().min(2),
});

export async function createProject(formData: FormData) {
  const raw = {
    title: formData.get('title'),
    description: formData.get('description'),
    technologies: formData.get('technologies'),
  };

  const parsed = ProjectFormSchema.safeParse(raw);
  if (!parsed.success) {
    throw new Error('Invalid project input.');
  }

  const { title, description, technologies } = parsed.data;

  await sql`
    INSERT INTO projects (title, description, technologies)
    VALUES (${title}, ${description}, ${technologies})
  `;

  revalidatePath('/projects');
  redirect('/projects');
}

Revalidating the Client Cache with revalidatePath

After a create, update, or delete action, cached route data may become stale. Call revalidatePath inside the Server Action so users see fresh data after mutation.

revalidatePath('/projects');

Dynamic Route Segments for Editing Specific IDs

Use a dynamic segment to edit a specific project record:

app/projects/[id]/edit/page.tsx

Read the id from route params, load that project, and submit the edit form to an update action bound to the same ID.

// app/projects/[id]/edit/page.tsx
import { updateProject } from '@/app/lib/actions';

export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;

  // Fetch project by id and render the edit form...
  return <form action={updateProject.bind(null, id)}>...</form>;
}

Activity Instructions

  1. Create a form to capture the user's input for a new project.

    Where should these files go? In your portfolio project, create the new route and form files inside the app/projects folder. A common structure is app/projects/create/page.tsx for the create form route and app/projects/[id]/edit/page.tsx for the edit form route.

  2. Create a createProject Server Action and connect it to your create-project form using the form action attribute.
  3. Add validation for title, description, and technologies values from FormData before writing to the database (use the ProjectFormSchema + safeParse pattern from the Prepare section).
  4. Create an edit route at app/projects/[id]/edit/page.tsx and wire it to an updateProject Server Action (use params.id and pass it to the action with updateProject.bind(null, id) as shown above).
  5. Add a deleteProject Server Action and connect it to a delete button for each project row/card (submit the project id so each delete only removes one record).
  6. Call revalidatePath('/projects') after each create, update, and delete action, then verify that the projects list refreshes immediately (place it right after the database write, then redirect('/projects') when appropriate).