Build a MERN Expense Tracker
Build a full-stack expense tracker with React, Express, and MongoDB.
Introduction
30 Second Summary
Everyone knows the feeling of checking their bank balance and wondering where all the money went. Keeping track of daily expenses is one of the simplest ways to take control of your finances.
In this project, you will build a full-stack expense tracker using the MERN stack. You will create a backend API that stores expenses in a database, and a frontend that lets you log and view them by category.
What You'll Build
A personal expense tracker with a Node.js/Express API connected to MongoDB and a React frontend built with Vite.
By the end of this project, you'll have:
- A running Express API that stores and retrieves expenses from MongoDB.
- A React frontend that displays expenses and lets you add new ones with a category and amount.
- A working full-stack MERN application where the frontend and backend communicate via REST API.
- Secret Mission: Add a delete button to remove expenses and watch your totals update in real time.
Are there any prerequisites?
This project assumes you have VS Code, Node.js, and MongoDB installed locally. Step 1 will walk you through verifying your setup and scaffolding the project folders.
Set Up the Project Structure
A MERN application has two codebases that work together: a backend server and a frontend client. Each lives in its own folder with its own dependencies.
In this step, you will create the project folder structure, install the backend packages (Express, Mongoose, and cors), and scaffold the React frontend using Vite.
In this step, get ready to:
- Create the project folder with separate server and client directories.
- Install backend dependencies (Express, Mongoose, cors).
- Scaffold the React frontend with Vite and verify everything starts.
Create the project folder and install backend packages
Every MERN project needs a root folder to hold both the server and client code. You will create this structure on your Desktop so it is easy to find later.
- Open your integrated terminal in VS Code by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the root project folder, move into it, and create the server subfolder by running these commands:
mkdir expense-tracker
cd expense-tracker
mkdir server
cd server
- Initialize a new Node.js project inside the server folder by running:
npm init -y
What does this command do?
The -y flag tells npm to accept all default values and generate a package.json file immediately. This file tracks your project's metadata and dependencies.
- Install the three backend packages your server needs by running:
npm install express mongoose cors
What does this code do?
- express is the web framework that handles HTTP requests and responses.
- mongoose connects your server to MongoDB and lets you define data schemas.
- cors allows the React frontend (running on a different port) to communicate with your backend.
- Confirm the packages installed by running:
ls node_modules
You should see a long list of folders printed in the terminal. This confirms your dependencies are installed.
Seeing "command not found" for npm?
This means Node.js is not available in this terminal session. Close the terminal, reopen it, and try node --version to confirm Node.js is accessible.
help me fix npm command not found in my terminal
Scaffold the React frontend using Vite
Vite is a modern build tool that scaffolds a React project with a fast development server. You will run a single command from the project root to create the entire client folder.
- Navigate back to the expense-tracker root folder by running:
cd ..
- Scaffold the React frontend by running:
npm create vite@latest client -- --template react
What should I see?
Your terminal may ask you to install the create-vite package the first time you run this command. Type y and press Enter to proceed.
Once it finishes, you will see a Done. message and a new client folder is created inside expense-tracker.
- Move into the client folder and install its dependencies by running:
cd client
npm install
You should see output confirming packages were added. This installs React and the Vite dev server tooling.
Install failed with a network error?
Check your internet connection and run npm install again. Temporary registry timeouts are common and resolve on retry.
help me fix npm install failing in my Vite project
Verify both sides start
Before writing real application code, you need to confirm that both the backend and frontend can start without errors. You will create a minimal server file, then run each side in its own terminal.
- Open the expense-tracker folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Select the expense-tracker folder on your Desktop, then click Open.
You should now see both server and client folders in the VS Code Explorer sidebar on the left.
- In the Explorer sidebar, right-click the server folder.
- Click New File.
- Type index.js and press Enter.
- Paste the following code into server/index.js:
const express = require('express');
const app = express();
const PORT = 5000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- const express = require('express') imports the Express framework.
- const app = express() creates an Express application instance.
- app.listen(PORT, ...) starts the server on port 5000 and prints a message when it is ready.
This is the minimum code needed to confirm Express works. You will add routes and database connections in the next step.
- Save server/index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open a new terminal in VS Code by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Start the backend server by running:
cd server
node index.js
You should see Server running on http://localhost:5000 printed in the terminal.
- Open a second terminal by clicking the + icon in the terminal panel.
- Start the Vite development server by running:
cd client
npm run dev
You should see a local URL (like http://localhost:5173/) printed in the terminal output. This confirms the React dev server is running.
Stop the servers when done
Press Ctrl+C in each terminal to stop the running server. You will restart them later when building features.
Server won't start?
- If you see Cannot find module 'express', make sure you are in the server folder and that npm install completed successfully earlier.
- If the Vite server shows an error, confirm you ran npm install inside the client folder.
help me fix my Express or Vite server not starting
✔️ Awesome, I've got everything!
Great. Double check you have saved server/index.js and that both servers started without errors.
ⓧ I'd like to double check the full code
Here is the complete server/index.js file at this point:
const express = require('express');
const app = express();
const PORT = 5000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your project skeleton is ready with both a backend and frontend in place. Next up, you will build the Express API that stores and retrieves expenses from MongoDB.
Build the Express API
Your project structure is ready with both server/ and client/ folders in place. Now it's time to build the backend that your React frontend will talk to.
The Express API is the bridge between your frontend and your database. It receives HTTP requests, processes them, and stores or retrieves data from MongoDB.
In this step, get ready to:
- Define a Mongoose schema and model for expenses.
- Set up Express routes to create and retrieve expenses.
- Test the API end-to-end with MongoDB.
Create the Mongoose schema and model
A schema defines the shape of your data. Every expense needs an amount, a category, a description, and a date. Mongoose enforces this structure so nothing unexpected slips into your database.
- In VS Code, open server/index.js.
- Select all existing content and replace it with the following server foundation code:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
const PORT = 5000;
// Enable cross-origin requests and JSON body parsing
app.use(cors());
app.use(express.json());
// Connect to the local MongoDB database called expense-tracker
mongoose.connect('mongodb://127.0.0.1:27017/expense-tracker')
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
What does this code do?
The first three lines import the packages your server needs. express handles HTTP requests, mongoose talks to MongoDB, and cors allows your React app (on a different port) to call this server.
The app.use(express.json()) line tells Express to automatically parse incoming JSON request bodies. The mongoose.connect call connects to your local MongoDB and creates the expense-tracker database if it doesn't exist yet.
Why 127.0.0.1 instead of localhost?
On some systems (especially Windows), localhost resolves to an IPv6 address that MongoDB doesn't listen on by default. Using 127.0.0.1 forces an IPv4 connection and avoids cryptic connection errors.
- Add the schema, model, and server listener below the mongoose.connect block by pasting this code at the bottom of the file:
// Define the shape of an expense document
const expenseSchema = new mongoose.Schema({
amount: { type: Number, required: true },
category: { type: String, required: true },
description: { type: String, required: true },
date: { type: Date, default: Date.now }
});
// Create a model from the schema (maps to the 'expenses' collection)
const Expense = mongoose.model('Expense', expenseSchema);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
The expenseSchema defines four fields every expense must have. amount is a number, category and description are strings, and date defaults to the current timestamp if not provided.
The mongoose.model('Expense', expenseSchema) call creates a model that Mongoose uses to read and write documents in a collection called expenses (it automatically lowercases and pluralizes the name).
- Save server/index.js.
Seeing a syntax error on save?
Make sure the closing }); for app.listen is the very last line in the file. Check that every opening brace { has a matching close }.
help me find the syntax error in my server/index.js
Set up the Express routes
A REST API uses HTTP methods to perform different operations on data. Your expense tracker needs two routes: one to fetch all expenses (GET) and one to create a new expense (POST).
- In server/index.js, find this line:
app.listen(PORT, () => {
- Add the GET route directly above that line:
// Fetch all expenses, newest first
app.get('/api/expenses', async (req, res) => {
const expenses = await Expense.find().sort({ date: -1 });
res.json(expenses);
});
What does this route do?
When a GET request hits /api/expenses, the handler queries MongoDB for all expense documents. The .sort({ date: -1 }) sorts them so the most recent expense appears first. Then res.json(expenses) sends the results back as JSON.
- Add the POST route between the GET route and app.listen:
// Create a new expense from the request body
app.post('/api/expenses', async (req, res) => {
const expense = new Expense(req.body);
await expense.save();
res.status(201).json(expense);
});
What does this route do?
When a POST request hits /api/expenses, the handler creates a new Expense document from the JSON body the client sent. Calling .save() writes it to MongoDB. The response sends back the saved document with a 201 (Created) status code.
- Save server/index.js.
Not sure you placed the routes correctly?
Both route handlers must appear after the Expense model definition and before app.listen. If either route is placed after app.listen, it will never be registered.
help me check my route placement in server/index.js
✔️ Awesome, I've got everything!
Great. Double check you have saved server/index.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
const PORT = 5000;
app.use(cors());
app.use(express.json());
mongoose.connect('mongodb://127.0.0.1:27017/expense-tracker')
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('MongoDB connection error:', err));
const expenseSchema = new mongoose.Schema({
amount: { type: Number, required: true },
category: { type: String, required: true },
description: { type: String, required: true },
date: { type: Date, default: Date.now }
});
const Expense = mongoose.model('Expense', expenseSchema);
app.get('/api/expenses', async (req, res) => {
const expenses = await Expense.find().sort({ date: -1 });
res.json(expenses);
});
app.post('/api/expenses', async (req, res) => {
const expense = new Expense(req.body);
await expense.save();
res.status(201).json(expense);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Test the API with your terminal
Your API code is written, but you need to confirm it actually works. You will start the server, create an expense with a POST request, then fetch it back with GET.
- Make sure MongoDB is running on your machine.
- Open a terminal in VS Code by pressing Cmd+` (macOS) or Ctrl+` (Windows).
- Navigate to the server folder and start the Express server by running:
cd server
node index.js
- You should see two messages in your terminal:
Server running on http://localhost:5000
Connected to MongoDB
Seeing a MongoDB connection error?
Make sure your local MongoDB service is running. On macOS, run brew services start mongodb-community if you installed via Homebrew. On Windows, check that the MongoDB service is running in the Services app.
help me fix my MongoDB connection error
- Open a second terminal in VS Code by clicking the + icon in the terminal panel.
- Create a test expense by running this command:
curl -X POST http://localhost:5000/api/expenses -H "Content-Type: application/json" -d "{\"amount\": 12.50, \"category\": \"food\", \"description\": \"Lunch\"}"
What does this command do?
This sends a POST request to your API with a JSON body containing an expense. The -H flag sets the Content-Type header so Express knows to parse the body as JSON. The -d flag provides the JSON data.
- You should see a JSON response with your expense data plus an _id and date field that MongoDB added automatically.
- Retrieve all expenses by running:
curl http://localhost:5000/api/expenses
- You should see a JSON array containing the expense you just created.
You can also test in the browser
Open http://localhost:5000/api/expenses in your browser to see the GET response. Browsers send GET requests by default, so this displays the same JSON array.
Getting an empty array back?
If the POST command succeeded but GET returns [], check that your MongoDB connection message appeared in the first terminal. Without a successful connection, data is not being saved.
help me debug why my expenses aren't saving to MongoDB
Your API is live and talking to MongoDB. Next up, you will build the React frontend that calls these endpoints and displays your expenses in the browser.
Build the React Frontend
Your Express API is live and can store expenses in MongoDB. The missing piece is a visual interface for users to actually interact with it.
In this step, you will build the React frontend. You will create a form component for adding expenses, a list component for displaying them, and an App component that orchestrates data flow between the frontend and your API.
In this step, get ready to:
- Create an ExpenseForm component with inputs and a submit button.
- Create an ExpenseList component that displays expenses from the API.
- Wire the form submission to POST to the API and refresh the list.
Create the ExpenseForm component
The ExpenseForm component will hold three form fields (amount, category, and description) and send the data to your Express API when the user clicks submit.
- In VS Code, open client/src/App.jsx from the Explorer sidebar.
- Select all of the existing template code and delete it.
- Replace it with the following imports and ExpenseForm component:
import { useState, useEffect } from 'react';
import './App.css';
function ExpenseForm({ onExpenseAdded }) {
const [amount, setAmount] = useState('');
const [category, setCategory] = useState('food');
const [description, setDescription] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:5000/api/expenses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: Number(amount), category, description })
});
if (response.ok) {
setAmount('');
setCategory('food');
setDescription('');
onExpenseAdded();
}
};
return (
<form onSubmit={handleSubmit} className="expense-form">
<h2>Add Expense</h2>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="food">Food</option>
<option value="transport">Transport</option>
<option value="entertainment">Entertainment</option>
<option value="utilities">Utilities</option>
<option value="other">Other</option>
</select>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
<button type="submit">Add Expense</button>
</form>
);
}
What does this code do?
useState creates three state variables (amount, category, description) that hold the current value of each form field. When the user types, the state updates and React re-renders the input with the new value.
handleSubmit sends a POST request to your Express API with the form data as JSON. If the request succeeds, it clears the form fields and calls onExpenseAdded() to tell the parent component to refresh the expense list.
- Save App.jsx.
- You won't see a change yet. The next chunk adds the ExpenseList component that displays data from the API.
Create the ExpenseList component
The ExpenseList component receives an array of expenses as props and renders each one as a list item showing the category, description, and formatted amount.
- In client/src/App.jsx, add the following code below the ExpenseForm function's closing brace:
function ExpenseList({ expenses }) {
return (
<div className="expense-list">
<h2>Expenses</h2>
{expenses.length === 0 ? (
<p>No expenses yet. Add one above!</p>
) : (
<ul>
{expenses.map((expense) => (
<li key={expense._id}>
<span className="expense-category">{expense.category}</span>
<span className="expense-description">{expense.description}</span>
<span className="expense-amount">${expense.amount.toFixed(2)}</span>
</li>
))}
</ul>
)}
</div>
);
}
What does this code do?
The component uses a ternary expression to show either a "no expenses" message or the actual list. Each expense is rendered with expense._id as the React key. MongoDB generates this unique ID for every document.
toFixed(2) formats the amount to two decimal places, so 12 displays as $12.00.
- Save App.jsx.
- You won't see a change yet. The next chunk adds the App component that fetches data and passes it to these two components.
Wire the form submission to the API
The App component is the parent that owns the expenses data. It fetches expenses from the API on load, passes them down to ExpenseList, and gives ExpenseForm a callback to refresh the data after a new expense is created.
- In client/src/App.jsx, add the following code below the ExpenseList function's closing brace:
function App() {
const [expenses, setExpenses] = useState([]);
const fetchExpenses = async () => {
const response = await fetch('http://localhost:5000/api/expenses');
const data = await response.json();
setExpenses(data);
};
useEffect(() => {
fetchExpenses();
}, []);
return (
<div className="app">
<h1>Expense Tracker</h1>
<div className="container">
<ExpenseForm onExpenseAdded={fetchExpenses} />
<ExpenseList expenses={expenses} />
</div>
</div>
);
}
export default App;
What does this code do?
useState([]) creates an empty array to hold expenses. The fetchExpenses function calls your Express GET endpoint and stores the response in state.
useEffect with an empty dependency array [] runs fetchExpenses once when the component first mounts. This loads existing expenses from MongoDB immediately when the page opens.
Passing fetchExpenses as the onExpenseAdded prop means that every time the form submits successfully, the full expense list re-fetches from the API. The new expense appears without a page reload.
- Save App.jsx.
- Make sure your Express server is still running in a terminal. If it stopped, run node server/index.js from the server folder.
- Start the Vite dev server by running this command from the client folder:
npm run dev
- Open http://localhost:5173 in your browser. You should see the Expense Tracker heading with an Add Expense form.
- Type 25 in the amount field, select Food from the category dropdown, type Lunch in the description field, and click Add Expense.
- The expense should appear in the Expenses list below showing food, Lunch, and $25.00.
Not seeing your expense appear?
- Make sure your Express server is running in a separate terminal window from your Vite dev server. Both need to be active at the same time.
- Check your browser console for CORS errors. If you see one, confirm that app.use(cors()) is in your server/index.js file.
- Verify that the fetch URL in your App.jsx matches http://localhost:5000/api/expenses exactly.
Help me debug why my expense form isn't saving data to the API.
✔️ Awesome, I've got everything!
Great. Double check you have saved App.jsx and both servers are running.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react';
import './App.css';
function ExpenseForm({ onExpenseAdded }) {
const [amount, setAmount] = useState('');
const [category, setCategory] = useState('food');
const [description, setDescription] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:5000/api/expenses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: Number(amount), category, description })
});
if (response.ok) {
setAmount('');
setCategory('food');
setDescription('');
onExpenseAdded();
}
};
return (
<form onSubmit={handleSubmit} className="expense-form">
<h2>Add Expense</h2>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="food">Food</option>
<option value="transport">Transport</option>
<option value="entertainment">Entertainment</option>
<option value="utilities">Utilities</option>
<option value="other">Other</option>
</select>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
<button type="submit">Add Expense</button>
</form>
);
}
function ExpenseList({ expenses }) {
return (
<div className="expense-list">
<h2>Expenses</h2>
{expenses.length === 0 ? (
<p>No expenses yet. Add one above!</p>
) : (
<ul>
{expenses.map((expense) => (
<li key={expense._id}>
<span className="expense-category">{expense.category}</span>
<span className="expense-description">{expense.description}</span>
<span className="expense-amount">${expense.amount.toFixed(2)}</span>
</li>
))}
</ul>
)}
</div>
);
}
function App() {
const [expenses, setExpenses] = useState([]);
const fetchExpenses = async () => {
const response = await fetch('http://localhost:5000/api/expenses');
const data = await response.json();
setExpenses(data);
};
useEffect(() => {
fetchExpenses();
}, []);
return (
<div className="app">
<h1>Expense Tracker</h1>
<div className="container">
<ExpenseForm onExpenseAdded={fetchExpenses} />
<ExpenseList expenses={expenses} />
</div>
</div>
);
}
export default App;
Your React frontend is now talking to your Express API. Next up, you will add a category summary that calculates totals from your expense data.
Add a Category Summary
Your expense tracker can add and display expenses. But a list of raw numbers does not tell you much about your spending habits.
In this step, you will build a Summary component that calculates your total spending and breaks it down by category. You will also replace the default Vite styles with a clean custom design.
In this step, get ready to:
- Create a Summary component with totals and per-category breakdown.
- Style the app with custom CSS for a polished layout.
- Test the full end-to-end flow across multiple categories.
Create the Summary component
JavaScript's reduce method is perfect for turning an array of expenses into a single total or a grouped object. Your Summary component will use two reduce calls to calculate both.
- In client/src/App.jsx, find the closing brace of the ExpenseList function (just above function App()).
- Add the following Summary component between ExpenseList and App:
function Summary({ expenses }) {
// Calculate the grand total by summing all expense amounts
const total = expenses.reduce((sum, exp) => sum + exp.amount, 0);
// Group expenses by category and sum each category's total
const byCategory = expenses.reduce((acc, exp) => {
acc[exp.category] = (acc[exp.category] || 0) + exp.amount;
return acc;
}, {});
return (
<div className="summary">
<h2>Summary</h2>
<p className="total">Total: ${total.toFixed(2)}</p>
<ul>
{Object.entries(byCategory).map(([category, amount]) => (
<li key={category}>
{category}: ${amount.toFixed(2)}
</li>
))}
</ul>
</div>
);
}
What does this code do?
The first reduce loops through every expense and adds each amount to a running sum, starting from 0. The result is a single number representing total spending.
The second reduce builds an object where each key is a category name and each value is that category's total. The || 0 handles the first time a category appears by defaulting it to zero before adding.
Now you need to render the Summary component inside the App function so it appears on the page.
- Scroll down to the App function's return statement.
- Find this section:
<ExpenseForm onExpenseAdded={fetchExpenses} />
<ExpenseList expenses={expenses} />
- Add <Summary expenses={expenses} /> between the two lines, so it now looks like this:
<ExpenseForm onExpenseAdded={fetchExpenses} />
<Summary expenses={expenses} />
<ExpenseList expenses={expenses} />
- Save App.jsx.
- Check your browser. You should see a "Summary" heading with "Total: $0.00" below the form.
Don't see the Summary section?
- Make sure the Summary function is placed between the ExpenseList function and the App function, not inside either one.
- Confirm you passed expenses={expenses} as a prop to <Summary /> in the App return.
- Check the terminal running Vite for any syntax errors.
Still stuck? Help me debug why my Summary component is not rendering.
Style the app with custom CSS
The default Vite styles make the app look generic. A custom stylesheet will give the form, list, and summary clear visual separation with card-like containers.
- Open client/src/App.css in VS Code.
- Select all existing content and delete it.
- Replace it with the following stylesheet:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.app h1 {
text-align: center;
margin-bottom: 2rem;
color: #1a1a1a;
}
.container {
display: grid;
gap: 1.5rem;
}
/* Form card styling */
.expense-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.expense-form h2 {
margin-bottom: 1rem;
}
.expense-form input,
.expense-form select {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.expense-form button {
width: 100%;
padding: 0.75rem;
background-color: #4f46e5;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.expense-form button:hover {
background-color: #4338ca;
}
/* Summary card styling */
.summary {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.summary h2 {
margin-bottom: 1rem;
}
.summary .total {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.75rem;
color: #4f46e5;
}
.summary ul {
list-style: none;
}
.summary ul li {
padding: 0.25rem 0;
text-transform: capitalize;
}
/* Expense list card styling */
.expense-list {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.expense-list h2 {
margin-bottom: 1rem;
}
.expense-list ul {
list-style: none;
}
.expense-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
}
.expense-list li:last-child {
border-bottom: none;
}
.expense-category {
background-color: #e0e7ff;
color: #4f46e5;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
text-transform: capitalize;
}
.expense-description {
flex: 1;
margin: 0 1rem;
}
.expense-amount {
font-weight: bold;
}
What does this stylesheet do?
- The global reset (* { margin: 0; padding: 0; }) removes default browser spacing so your layout is consistent.
- Each section (.expense-form, .summary, .expense-list) gets a white card with rounded corners and a subtle shadow.
- The .container uses CSS Grid with a gap to space the cards evenly without manual margins.
- Category badges (.expense-category) get a soft purple background to visually tag each expense.
- Save App.css and check your browser.
- You should see three distinct white cards on a light gray background: the form at the top, the summary in the middle, and the expense list at the bottom.
Styles not applying?
- Make sure you deleted all the old Vite styles before pasting. Leftover rules can conflict with your new styles.
- Confirm the file is saved. Vite hot-reloads on save, so unsaved changes will not appear.
- Also check that client/src/index.css is not overriding your styles. If it is, clear its contents as well.
Need help? Help me figure out why my CSS styles are not showing up.
✔️ Awesome, I've got everything!
Great. Double check you have saved both App.jsx and App.css.
ⓧ I'd like to double check the full code
Here is the complete App.jsx at this point:
import { useState, useEffect } from 'react';
import './App.css';
function ExpenseForm({ onExpenseAdded }) {
const [amount, setAmount] = useState('');
const [category, setCategory] = useState('food');
const [description, setDescription] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:5000/api/expenses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: Number(amount), category, description })
});
if (response.ok) {
setAmount('');
setCategory('food');
setDescription('');
onExpenseAdded();
}
};
return (
<form onSubmit={handleSubmit} className="expense-form">
<h2>Add Expense</h2>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="food">Food</option>
<option value="transport">Transport</option>
<option value="entertainment">Entertainment</option>
<option value="utilities">Utilities</option>
<option value="other">Other</option>
</select>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
<button type="submit">Add Expense</button>
</form>
);
}
function ExpenseList({ expenses }) {
return (
<div className="expense-list">
<h2>Expenses</h2>
{expenses.length === 0 ? (
<p>No expenses yet. Add one above!</p>
) : (
<ul>
{expenses.map((expense) => (
<li key={expense._id}>
<span className="expense-category">{expense.category}</span>
<span className="expense-description">{expense.description}</span>
<span className="expense-amount">${expense.amount.toFixed(2)}</span>
</li>
))}
</ul>
)}
</div>
);
}
function Summary({ expenses }) {
const total = expenses.reduce((sum, exp) => sum + exp.amount, 0);
const byCategory = expenses.reduce((acc, exp) => {
acc[exp.category] = (acc[exp.category] || 0) + exp.amount;
return acc;
}, {});
return (
<div className="summary">
<h2>Summary</h2>
<p className="total">Total: ${total.toFixed(2)}</p>
<ul>
{Object.entries(byCategory).map(([category, amount]) => (
<li key={category}>
{category}: ${amount.toFixed(2)}
</li>
))}
</ul>
</div>
);
}
function App() {
const [expenses, setExpenses] = useState([]);
const fetchExpenses = async () => {
const response = await fetch('http://localhost:5000/api/expenses');
const data = await response.json();
setExpenses(data);
};
useEffect(() => {
fetchExpenses();
}, []);
return (
<div className="app">
<h1>Expense Tracker</h1>
<div className="container">
<ExpenseForm onExpenseAdded={fetchExpenses} />
<Summary expenses={expenses} />
<ExpenseList expenses={expenses} />
</div>
</div>
);
}
export default App;
And here is the complete App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.app h1 {
text-align: center;
margin-bottom: 2rem;
color: #1a1a1a;
}
.container {
display: grid;
gap: 1.5rem;
}
.expense-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.expense-form h2 {
margin-bottom: 1rem;
}
.expense-form input,
.expense-form select {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.expense-form button {
width: 100%;
padding: 0.75rem;
background-color: #4f46e5;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.expense-form button:hover {
background-color: #4338ca;
}
.summary {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.summary h2 {
margin-bottom: 1rem;
}
.summary .total {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.75rem;
color: #4f46e5;
}
.summary ul {
list-style: none;
}
.summary ul li {
padding: 0.25rem 0;
text-transform: capitalize;
}
.expense-list {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.expense-list h2 {
margin-bottom: 1rem;
}
.expense-list ul {
list-style: none;
}
.expense-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
}
.expense-list li:last-child {
border-bottom: none;
}
.expense-category {
background-color: #e0e7ff;
color: #4f46e5;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
text-transform: capitalize;
}
.expense-description {
flex: 1;
margin: 0 1rem;
}
.expense-amount {
font-weight: bold;
}
Test the full flow end-to-end
The best way to confirm everything works is to add expenses across several categories and watch the summary recalculate in real time.
- In the browser, add an expense with amount 12.50, category **Food**, and description Lunch.
- Add a second expense with amount 35.00, category **Transport**, and description Uber.
- Add a third expense with amount 8.99, category **Entertainment**, and description Movie rental.
After adding all three, the Summary card should show a total of $56.49. It should also list each category with its own subtotal: food at $12.50, transport at $35.00, and entertainment at $8.99.
Why does the summary update automatically?
When you submit a new expense, the onExpenseAdded callback triggers fetchExpenses, which updates the expenses state in App. Since both Summary and ExpenseList receive expenses as a prop, React re-renders both components with the latest data.
Your expense tracker now gives you real spending insights at a glance. Next up, take on the Secret Mission and add the ability to delete expenses for full CRUD functionality.
Secret mission
Add Delete Functionality
Your expense tracker can add expenses, but what about removing them? In this secret mission, you will add full delete capability across the entire stack. You will create a new API endpoint, wire a delete button in the frontend, and watch your summary totals update in real time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The expense-tracker project folder on your Desktop (containing server/ and client/ directories).
- The expense-tracker MongoDB database with an expenses collection.
- The Express server running on port 5000.
- The Vite dev server running on port 5173.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the expense tracker.
- Your Express server and Vite dev server will keep running until you close the terminal windows or shut down your computer.
- Your MongoDB database persists locally and is available whenever the mongod service is running.
- Your project files remain in the expense-tracker folder on your Desktop.
Pause - I'll come back to this later
Stop the running servers to free up terminal sessions and memory. Your files and database stay intact for when you return.
- Switch to the terminal running the Express server and press Ctrl+C to stop it.
- Switch to the terminal running the Vite dev server and press Ctrl+C to stop it.
- Your project folder and MongoDB database will still be available when you come back.
Delete - I don't want to use this again
Remove all project files and database data to start completely fresh.
- Stop both running servers by pressing Ctrl+C in each terminal.
- Delete the project folder by running this command:
cd ~/Desktop
rm -rf expense-tracker
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q expense-tracker
- Drop the MongoDB database by opening a new terminal and running:
mongosh
- Inside the mongosh shell, run these commands to switch to the database and drop it:
use expense-tracker
db.dropDatabase()
- Type exit to close the mongosh shell.
Nice Work!
Nice Work!
You just built a full-stack expense tracker using the MERN stack. Your app handles the complete data lifecycle, from a form submission in the browser all the way to a MongoDB write and back.
You've learned how to:
- Build a Node.js/Express REST API with GET and POST endpoints connected to MongoDB using Mongoose schemas and models.
- Scaffold a React frontend with Vite and build interactive components using useState for form handling and state management.
- Wire the frontend to the backend using fetch for full-stack data flow, including a category summary that computes totals from expense data.
- Secret Mission: Added DELETE functionality for full CRUD capability, proving you can wire a new endpoint from database to UI.
Ready to quiz yourself?