Skip to main content

Overview

Expenses are the core transactions in BillBuddy. They represent money spent by one person that needs to be split among multiple group members. Each expense is categorized, tracked, and automatically calculated to determine how much each person owes.

Expense Model

The Expense model is defined in backend/models/Expense.js:3-39:
Expense Schema
const ExpenseSchema = new mongoose.Schema({
  description: {
    type: String,
    required: [true, 'Please provide a description'],
    trim: true,
    maxlength: [100, 'Description cannot be more than 100 characters']
  },
  amount: {
    type: Number,
    required: [true, 'Please provide an amount'],
    min: [0, 'Amount cannot be negative']
  },
  paidBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  splitAmong: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  }],
  group: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Group',
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  },
  category: {
    type: String,
    enum: ['Food', 'Travel', 'Shopping', 'Entertainment', 'Utilities', 'Other'],
    default: 'Other'
  }
});

Expense Categories

Food

Restaurants, groceries, and food delivery

Travel

Transportation, hotels, and trip costs

Shopping

Retail purchases and household items

Entertainment

Movies, concerts, and leisure activities

Utilities

Electricity, water, internet, and bills

Other

Miscellaneous expenses (default)

How Expense Splitting Works

BillBuddy uses equal splitting for all expenses. The total amount is divided equally among all members in the splitAmong array.
1

Someone Pays

One person pays the full amount of an expense (tracked in paidBy field)
2

Split Calculation

The amount is divided equally by the number of people in splitAmong
3

Balance Update

The payer’s balance increases, and each person’s balance decreases by their share
Example: If Alice pays $60 for dinner and splits it among Alice, Bob, and Charlie:
  • Per person share: 60÷3=60 ÷ 3 = 20
  • Alice’s balance: +60(shepaid)60 (she paid) - 20 (her share) = +$40
  • Bob’s balance: -$20
  • Charlie’s balance: -$20
Per Person Share Calculation
// Method to calculate per person share
ExpenseSchema.methods.calculatePerPersonShare = function() {
  return this.amount / this.splitAmong.length;
};

API Endpoints

Create a New Expense

POST /api/expenses

Add a new expense to a group. The authenticated user is automatically set as the payer.
Request Body:
{
  "description": "Dinner at Italian restaurant",
  "amount": 85.50,
  "group": "507f1f77bcf86cd799439011",
  "splitAmong": [
    "507f1f77bcf86cd799439012",
    "507f1f77bcf86cd799439013",
    "507f1f77bcf86cd799439014"
  ],
  "category": "Food"
}
Response:
{
  "_id": "507f1f77bcf86cd799439020",
  "description": "Dinner at Italian restaurant",
  "amount": 85.50,
  "paidBy": "507f1f77bcf86cd799439012",
  "splitAmong": [
    "507f1f77bcf86cd799439012",
    "507f1f77bcf86cd799439013",
    "507f1f77bcf86cd799439014"
  ],
  "group": "507f1f77bcf86cd799439011",
  "category": "Food",
  "date": "2026-03-03T18:30:00.000Z"
}
Implementation (from backend/routes/expenses.js:34-60):
router.post('/', protect, async (req, res) => {
  try {
    const { description, amount, group, splitAmong, category } = req.body;

    // Create new expense
    const expense = new Expense({
      description,
      amount,
      paidBy: req.user.id, // Automatically set to current user
      group,
      splitAmong,
      category
    });

    await expense.save();

    // Add expense to group
    await Group.findByIdAndUpdate(group, {
      $push: { expenses: expense._id }
    });

    res.status(201).json(expense);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Server error' });
  }
});
When an expense is created, it’s automatically added to the group’s expense list, making it immediately visible to all group members.

Get Recent Expenses

GET /api/expenses/recent

Retrieve the 10 most recent expenses where the user either paid or is splitting the cost.
Response:
[
  {
    "_id": "507f1f77bcf86cd799439020",
    "description": "Dinner at Italian restaurant",
    "amount": 85.50,
    "paidBy": {
      "_id": "507f1f77bcf86cd799439012",
      "name": "Alice Smith",
      "email": "[email protected]"
    },
    "splitAmong": [
      {
        "_id": "507f1f77bcf86cd799439012",
        "name": "Alice Smith",
        "email": "[email protected]"
      },
      {
        "_id": "507f1f77bcf86cd799439013",
        "name": "Bob Jones",
        "email": "[email protected]"
      }
    ],
    "group": {
      "_id": "507f1f77bcf86cd799439011",
      "name": "Roommates 2026"
    },
    "category": "Food",
    "date": "2026-03-03T18:30:00.000Z"
  }
]

Get Expenses by Group

GET /api/expenses/group/:groupId

Retrieve all expenses for a specific group, sorted by date (newest first).
Response: Returns an array of expense objects with populated user and group information. Authorization:
  • Only group members can view group expenses
  • Returns 403 error if user is not a member
Implementation (from backend/routes/expenses.js:65-92):
router.get('/group/:groupId', protect, async (req, res) => {
  try {
    const group = await Group.findById(req.params.groupId);

    if (!group) {
      return res.status(404).json({ message: 'Group not found' });
    }

    // Check if user is a member of the group
    const isMember = group.members.some(
      member => member.user.toString() === req.user.id
    );

    if (!isMember) {
      return res.status(403).json({ message: 'Not authorized to access this group' });
    }

    const expenses = await Expense.find({ group: req.params.groupId })
      .populate('paidBy', 'name email')
      .populate('splitAmong', 'name email')
      .sort('-date'); // Sort by date descending

    res.json(expenses);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Server error' });
  }
});

Get Single Expense

GET /api/expenses/:id

Retrieve detailed information about a specific expense.
Response:
{
  "_id": "507f1f77bcf86cd799439020",
  "description": "Dinner at Italian restaurant",
  "amount": 85.50,
  "paidBy": {
    "_id": "507f1f77bcf86cd799439012",
    "name": "Alice Smith",
    "email": "[email protected]"
  },
  "splitAmong": [...],
  "group": "507f1f77bcf86cd799439011",
  "category": "Food",
  "date": "2026-03-03T18:30:00.000Z"
}
Only users involved in the expense (either as the payer or in the split) can view expense details. Others will receive a 403 Forbidden error.

Update Expense

PUT /api/expenses/:id

Update an existing expense. Only the person who paid can update it.
Request Body:
{
  "description": "Updated dinner description",
  "amount": 90.00,
  "splitAmong": [
    "507f1f77bcf86cd799439012",
    "507f1f77bcf86cd799439013"
  ],
  "category": "Entertainment"
}
The paidBy, group, and date fields cannot be changed after creation.

Delete Expense

DELETE /api/expenses/:id

Delete an expense permanently. Only the person who paid can delete it.
Response:
{
  "message": "Expense removed"
}
Implementation (from backend/routes/expenses.js:159-184):
router.delete('/:id', protect, async (req, res) => {
  try {
    const expense = await Expense.findById(req.params.id);

    if (!expense) {
      return res.status(404).json({ message: 'Expense not found' });
    }

    // Check if user is the one who paid
    if (expense.paidBy.toString() !== req.user.id) {
      return res.status(403).json({ message: 'Not authorized to delete this expense' });
    }

    // Remove expense from group
    await Group.findByIdAndUpdate(expense.group, {
      $pull: { expenses: expense._id }
    });

    await expense.remove();

    res.json({ message: 'Expense removed' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Server error' });
  }
});

Example Usage

const token = localStorage.getItem('token');

const response = await fetch('/api/expenses', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({
    description: 'Grocery shopping',
    amount: 142.50,
    group: '507f1f77bcf86cd799439011',
    splitAmong: [
      '507f1f77bcf86cd799439012', // Me
      '507f1f77bcf86cd799439013', // Alice
      '507f1f77bcf86cd799439014'  // Bob
    ],
    category: 'Shopping'
  })
});

const expense = await response.json();
console.log('Expense created:', expense);
console.log('Per person share:', expense.amount / 3);

Permission System

Any group member can create an expense. The authenticated user is automatically set as the payer.
Users can view expenses if:
  • They are the payer (paidBy)
  • They are included in the split (splitAmong)
  • They are viewing expenses for a group they’re a member of
Only the user who paid for the expense can update it. This prevents others from modifying expense details that affect the payer’s balance.
Only the user who paid for the expense can delete it. When deleted, the expense is also removed from the group’s expense list.

Best Practices

Clear Descriptions

Use descriptive names like “Grocery shopping - Whole Foods” instead of just “Shopping”

Proper Categories

Select the most appropriate category to help with expense tracking and analysis

Include Everyone

Make sure to include all people who benefited from the expense in the splitAmong array

Verify Amounts

Double-check amounts before creating expenses, as they affect everyone’s balances

Error Handling

Status CodeMessageCause
400Amount cannot be negativeNegative expense amount
403Not authorized to access this groupNon-member trying to view group expenses
403Not authorized to access this expenseNon-involved user trying to view expense
403Not authorized to update this expenseNon-payer trying to update
403Not authorized to delete this expenseNon-payer trying to delete
404Expense not foundInvalid expense ID
404Group not foundInvalid group ID
500Server errorInternal server error

Understanding Balance Impact

When you create an expense, here’s how it affects balances:
// Example: Alice pays $90 for dinner split among Alice, Bob, and Charlie

Expense:
- Amount: $90
- Paid by: Alice
- Split among: [Alice, Bob, Charlie]

Calculations:
- Per person share: $90 / 3 = $30

Balance changes:
- Alice: +$90 (paid) - $30 (her share) = +$60 (owed by others)
- Bob: -$30 (owes Alice)
- Charlie: -$30 (owes Alice)

Net result:
- Bob and Charlie each owe $30
- Alice is owed $60 total ($30 from Bob + $30 from Charlie)
If you’re the payer and you want to exclude yourself from the split, simply don’t include your user ID in the splitAmong array. This is useful for expenses you’re covering entirely for others.

Build docs developers (and LLMs) love