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
TaskFormandTaskCardthat handle user interaction and form inputs -
Server Actions — functions that run on the server, connected directly to forms via the
actionprop -
MongoDB — storing and querying tasks using
clientPromisewithout 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/tasksafter 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)
-
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 toTaskList -
tasks/[id]/page.js— receives the dynamicidfromparams, fetches that specific task, and passes it toTaskFormfor 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
actionprop, Next.js automatically passesFormDatato it. You have to useformData.get("fieldName")to extract values — you can't just pass a plain object. -
MongoDB
_idis anObjectId, not a string.When querying, updating, or deleting by
_id, you need to wrap the id withnew ObjectId(id)from themongodbpackage. A plain string won't match anything in the database. -
revalidatePathandredirectare required.Without
revalidatePath, the page cache doesn't update after a mutation, so the UI won't reflect changes. Withoutredirect, 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");
}
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
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)