Thuta Learning
ProjectsWeb Developmentintermediate

Mini Project — Link Stash (Part 2): Add/Delete Feature and Filtering

Relax. We'll talk through this in plain words — no textbook voice.

What you'll walk away with

  • Apply Mini Project — Link Stash (Part 2): Add/Delete Feature and Filtering in a real project
  • Write and run the code yourself
  • Build up a whole project step by step

Take a moment to think about this

In this part, we'll turn Part 1's static list into a data-driven app. Bookmark data will be managed through a lib/db.ts data layer that simulates a module-level array, and form submissions via Server Actions will insert/delete directly on the server. You'll also learn progressive enhancement, so the form keeps working even with client-side JavaScript disabled. On top of that, we'll add filtering to the home page via search params like ?tag=docs, and use revalidatePath so the UI updates automatically whenever the data changes.

Let's build it

Create lib/db.ts, keeping the bookmarks array at module scope and exporting getBookmarks(), addBookmark(), and deleteBookmark() functions (a real project could use SQLite/Prisma, but here we'll keep it simple with an in-memory array). In app/actions.ts, write createBookmark and removeBookmark Server Actions with the "use server" directive at the top, and call revalidatePath("/"). In app/bookmarks/new/page.tsx, create a form and wire it to action={createBookmark}. Update app/page.tsx to accept the searchParams prop and filter the list by the tag query. For each bookmark card, wire up a delete button to the removeBookmark action.

Code Example

typescript
// lib/db.ts
type Bookmark = { id: string; title: string; url: string; tags: string[] };

let bookmarks: Bookmark[] = [
  { id: "1", title: "Next.js Docs", url: "https://nextjs.org", tags: ["docs"] },
];

export function getBookmarks(tag?: string) {
  if (!tag) return bookmarks;
  return bookmarks.filter((b) => b.tags.includes(tag));
}

export function addBookmark(data: Omit<Bookmark, "id">) {
  bookmarks.push({ id: crypto.randomUUID(), ...data });
}

export function deleteBookmark(id: string) {
  bookmarks = bookmarks.filter((b) => b.id !== id);
}

// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { addBookmark, deleteBookmark } from "@/lib/db";

export async function createBookmark(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  const url = String(formData.get("url") ?? "").trim();
  if (!title || !url) return;
  addBookmark({ title, url, tags: [] });
  revalidatePath("/");
}

export async function removeBookmark(id: string) {
  deleteBookmark(id);
  revalidatePath("/");
}
You should see
Submitting the form adds a new bookmark card to the home page, and the list filters correctly by the tag query param.

5-Minute Try-It

Switch between /?tag=docs and /?tag=reference in the URL bar and confirm within 5 minutes that the filtered results differ.

A quick word of caution

The in-memory array loses its data on every server restart — for a real production project, you should use an actual database like Prisma/SQLite, as covered in the database-layer lesson.

Easy traps

  • Forgetting the "use server" directive at the top of the Server Action file, causing an error when importing it from a client component
  • Forgetting to call revalidatePath() after changing data, so the UI keeps showing stale data unless you manually refresh

Now Try It Yourself

Switch between /?tag=docs and /?tag=reference in the URL bar and confirm within 5 minutes that the filtered results differ.

You'll know it worked when: Submitting the form adds a new bookmark card to the home page, and the list filters correctly by the tag query param.

Mini Project — Link Stash (Part 2): Add/Delete Feature and Filtering | Thuta Learning