DEV Community

Cover image for Day 58 of #100DayOfCode — Building Task Management CRUD App with Next.js
M Saad Ahmad
M Saad Ahmad

Posted on

Day 58 of #100DayOfCode — Building Task Management CRUD App with Next.js

For Day 58 of my #100DaysOfCode journey, my goal was to build a basic but fully functional Task Management App using Next.js. Up until this point, I had been learning individual Next.js concepts: server and client components, routing with the App Router, data fetching and caching, API routing, server actions, and connecting to a MongoDB database. I felt confident with each concept in isolation, but I hadn't yet put them all together in a single project.

So today's goal was simple: build something real, something CRUD-based, that forces me to use everything I've learned so far in one place.


What I Built

The app is a simple Task Manager where you can create, view, edit, and delete tasks. Each task has a title, description, and a status (pending, in-progress, or completed). The app has three pages and three components:

Pages:

  • /tasks — displays all tasks stored in the database
  • /tasks/new — a form to create a new task
  • /tasks/[id] — a pre-filled form to edit an existing task

Components:

  • TaskList — receives all tasks and maps over them
  • TaskCard — displays an individual task with Edit and Delete options
  • TaskForm — a reusable form used on both the create and edit pages

🛠 Concepts Used

  • App Router — folder-based routing with dynamic segments like [id]
  • Server Components — pages that fetch data directly from MongoDB without any client-side fetching
  • Client Components — components like TaskForm and TaskCard that handle user interaction and form inputs
  • Server Actions — functions that run on the server, connected directly to forms via the action prop
  • MongoDB — storing and querying tasks using clientPromise without Mongoose
  • revalidatePath — to refresh the page cache after every mutation so the UI always reflects the latest database state
  • redirect — to send the user back to /tasks after creating or updating a task

Folder Structure

next-crud-app/
├── app/
│   ├── page.js               --> Landing page
│   └── tasks/
│       ├── page.js           --> Lists all tasks (Server Component)
│       ├── new/
│       │   └── page.js       --> Create task form page (Server Component)
│       └── [id]/
│           └── page.js       --> Edit task form page (Server Component)
├── actions/
│   └── tasks.js              --> All server actions (CRUD)
│
├── db/
│   └── mongodb.js            --> MongoDB connection
│
│
├── components/
│   ├── TaskCard.jsx          --> Single task display (Client Component)
│   ├── TaskForm.jsx          --> Reusable form (Client Component)
│   └── TaskList.jsx          --> Maps over tasks (Server Component)
Enter fullscreen mode Exit fullscreen mode
  • db/mongodb.js — sets up and exports the MongoDB client connection used across all server actions
  • actions/tasks.js — the heart of the app; contains all five CRUD functions that interact with MongoDB
  • tasks/page.js — an async server component that fetches all tasks and passes them down to TaskList
  • tasks/[id]/page.js — receives the dynamic id from params, fetches that specific task, and passes it to TaskForm for pre-filling

The Interesting Part — actions/tasks.js

The most interesting and challenging file to write was actions/tasks.js. This is where all the CRUD logic lives and where everything connects together.

What made it challenging was that I initially wrote it based on how I thought it would work, similar to how I had written a contact form previously. I passed plain objects to functions and queried MongoDB by id as a plain string. But I quickly ran into several issues I wasn't aware of:

  • Server actions receive FormData, not plain objects.

    When a server action is connected to a form via the action prop, Next.js automatically passes FormData to it. You have to use formData.get("fieldName") to extract values — you can't just pass a plain object.

  • MongoDB _id is an ObjectId, not a string.

    When querying, updating, or deleting by _id, you need to wrap the id with new ObjectId(id) from the mongodb package. A plain string won't match anything in the database.

  • revalidatePath and redirect are required.

    Without revalidatePath, the page cache doesn't update after a mutation, so the UI won't reflect changes. Without redirect, the user just stays on the form page after submitting.

  • Serialization is needed when returning data.

    MongoDB returns special document objects that can't be passed directly as props to client components. Wrapping the result in JSON.parse(JSON.stringify(data)) converts it to a plain JavaScript object that Next.js can safely pass across the server-client boundary.

"use server";
import clientPromise from "../db/mongo";
import { ObjectId } from "mongodb";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function createTask(formData) {
  const client = await clientPromise;
  const db = client.db("taskapp");
  await db.collection("tasks").insertOne({
    title: formData.get("title"),
    description: formData.get("description"),
    status: formData.get("status"),
    createdAt: new Date(),
  });
  revalidatePath("/tasks");
  redirect("/tasks");
}
Enter fullscreen mode Exit fullscreen mode

What I Struggled With

Two things genuinely confused me during this build:

1. Understanding which file uses which component and why.
With multiple pages and components, I kept getting confused about who imports what and why. For example, I wasn't sure if TaskList should also import TaskForm, or whether TaskForm belonged on the page directly. Mapping out the dependency chain clearly helped a lot:

tasks/page.js → TaskList → TaskCard
tasks/new/page.js → TaskForm
tasks/[id]/page.js → TaskForm
actions/tasks.js → used by TaskCard and TaskForm
Enter fullscreen mode Exit fullscreen mode

2. Understanding how actions/tasks.js actually works.
I initially wrote the file by instinct based on my contact form experience. But server actions in a CRUD context behave differently; they receive FormData, need ObjectId for queries, and require revalidatePath to keep the UI in sync. Once I understood these gaps, the file made complete sense.


Conclusion

What made this project unique as part of my #100DaysOfCode journey is that it wasn't about learning something new; it was about connecting everything I already knew into one working app. It's one thing to understand server actions in isolation, and another thing entirely to wire them up to a form, pass the right data, hit the database, and have the UI update automatically.

This project gave me that full picture for the first time. It also revealed the gaps in my understanding that I wouldn't have found without actually building something.


What's Next

The app is fully functional but completely unstyled. The next step is to add Tailwind CSS to style the UI and then deploy it on Vercel.

Stay tuned for Day 59!

Top comments (0)