Full-Stack Next.js App
Part of the Full Stack Developer Roadmap
In short
Time to build one app where the UI, the backend and the database live in one Next.js project. An expense tracker is perfect: small enough to finish, real enough to show in interviews.
A complete application built entirely within Next.js — Server Components for data fetching, Server Actions or API routes for mutations, and a real database — your first taste of a truly unified full-stack codebase.

Think of it like this
Your own food truck
A food truck is a whole restaurant on wheels. The stove, the counter and the cash box sit in one vehicle, and you run all of it. Your Next.js app is the same: pages, server code and database access in one project you can drive anywhere.
Watch it happen
Build plan, one layer at a time
1/5Define an Expense model in Prisma: id, amountPaise, category, spentOn, userId. Run the first migration.
The big ideas
What you need to know
Add and delete expenses
A form with amount, category and date, saved through a server action, plus a delete button on each row.
Monthly view
A server component that totals the month and groups by category. A query param like ?month=2026-10 makes it shareable.
Your data only
Every read and write filters by the session user. Test it by signing in as a second user.
Live on the internet
Deploy with a real database and environment variables. A live link beats a screenshot on your resume.
See it in code
Walk through the code
1"use server";2import { z } from "zod";3import { revalidatePath } from "next/cache";4import { auth } from "@/auth";5const Expense = z.object({6 rupees: z.coerce.number().positive().max(1_000_000),7 category: z.enum(["food", "travel", "rent", "other"]),8 spentOn: z.coerce.date(),9});10export async function createExpense(formData: FormData) {11 const session = await auth();12 if (!session?.user?.id) throw new Error("Please sign in");13 const { rupees, category, spentOn } = Expense.parse(Object.fromEntries(formData));14 await db.expense.create({ data: { amountPaise: Math.round(rupees * 100), category, spentOn, userId: session.user.id } });15 revalidatePath("/expenses");16}1/4Form values arrive as strings. z.coerce turns them into a number and a date, and rejects junk.
app/expenses/actions.ts. auth comes from your Auth.js setup and db is your Prisma client. Use it as <form action={createExpense}>.
Save yourself the pain
Common mistakes
Oops
Storing money as a float like 199.99.
Do this instead
Store whole paise as an integer. Floats cause rounding errors when you add up a month of expenses.
Oops
Forgetting the userId filter in one query, so users see each other's expenses.
Do this instead
Write one helper like getUserExpenses(userId) and use it everywhere, so the filter can't be skipped.
Oops
Building every feature before deploying even once.
Do this instead
Deploy on day one with just the list page. Fixing deploy problems early is far easier.
Your mission
First milestone: list and add
Quick check
After createExpense saves a row, why call revalidatePath('/expenses')?
Go deeper
- 1Fetch real data from your database directly in a Server Component.
- 2Handle a form submission with a Server Action that writes to the database.
- 3Deploy it to Vercel and confirm the whole thing works end-to-end in production.
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.