# Building a ShelfLife Household Inventory Tracker - Backend 

In many shared households, managing groceries becomes messy over time.

Multiple people live together,

*   Everyone buys food
    
*   Items get stored in different places
    
*   Expiry dates are ignored
    
*   Food gets wasted
    

The real problem isn’t buying food — it’s **tracking and managing it efficiently as a group**.

* * *

### What are we building?

**ShelfLife** is a collaborative expiry tracking application designed for shared households.

It helps roommates:

*   Track groceries in one place
    
*   Get notified before items expire
    
*   Collaborate within a household
    
*   Compete to reduce food waste
    

* * *

### Goal

To reduce food wastage by bringing **visibility, accountability, and collaboration** into everyday household management.

Instead of:

> “Who bought this?”  
> “Is this still good?”  
> “Why is this expired again?”

We move to:

> “We know what we have.”  
> “We know when it expires.”  
> “We waste less.”

* * *

### **Core Features**

1.  **Authentication & User Management**  
    Every user starts here:
    
    *   Users can **sign up** with credentials
        
    *   Users can **log in securely**
        
    
    This ensures every action is tied to an authenticated user.
    
2.  **Household System**
    
    Why household?
    
    Because this is **collaborative**, not individual.
    
    *   One user creates a household
        
    *   Gets a **6-char invite code**
        
    *   Others join using code
        
    *   User can **view all other household members**.
        
    *   Any member has the flexibility to **leave the household** at any time.
        
3.  **Inventory Management**
    
    Users can manage grocery items within a household:
    
    *   Add items with:
        
        *   **name**
            
        *   **category**
            
        *   **expiry date**
            
        *   **quantity**
            
    *   The system automatically assigns a **status** to each item:
        
        *   **Fresh** → expiry date is far away
            
        *   **Expiring Soon** → within 3 days
            
        *   **Expired** → past expiry date
            
    *   Users can:
        
        *   **Edit items**
            
        *   **Delete items**
            
4.  **Automated Status Updates (Cron Job)**
    
    Item status changes over time, so it cannot be static.
    
    Example:
    
    *   Today → Fresh
        
    *   After 2 days → Expiring Soon
        
    *   After expiry → Expired
        
    
    To handle this automatically:
    
    *   A **daily cron job** runs in the background
        
    *   It checks all items in the database
        
    *   Updates their status based on the current date and expiry date
        
    
    This ensures:
    
    *   Status is always **accurate**
        
    *   No manual updates are required
        
    *   Users get **real-time visibility** of item conditions
        

* * *

### Step 1: Initialize Project

```javascript
npm init -y
```

It Creates `package.json`

* * *

### Step 2: Install Dependencies

```plaintext
npm install express
npm install --save-dev nodemon
npm install dotenv // This package is used to load environment variables from the .env file.
```

* * *

### Step 3: Setup App Entry (src/app.js)

Create `src/app.js`

```javascript
import express from "express";

const app = express();

app.use(express.json());

export default app;
```

This file is responsible for:

*   Initializing express app
    
*   Adding global middlewares
    

* * *

### Step 4: Environment Variables

Create `.env` file:

```javascript
PORT=3000
NODE_ENV=development
MONGODB_URI=your_mongodb_connection_string
```

* * *

### Step 5: Server Entry Point (server.js)

create `server.js`

```javascript
import "dotenv/config"; //This automatically loads .env variables into process.env
import app from "./src/app.js";

const PORT = process.env.PORT || 3000;

const start = async () => {
    app.listen(PORT, () => {
        console.log(`Server is running at ${PORT} in ${process.env.NODE_ENV} mode`);
    });
};

start().catch((err) => {
    console.error("Failed to start server", err);
    process.exit(1);
});
```

This file is responsible for:

*   Starting the server
    
*   Handling startup errors
    

* * *

### Step 6: Scripts Setup

Update `package.json`:

```javascript
"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}
```

Use:

```plaintext
npm run dev
```

* * *

### Step 7: Setup Database Connection

Create:

```plaintext
src/common/config/db.js
```

```js
import mongoose from "mongoose";
import dns from "dns";

dns.setServers(["1.1.1.1", "8.8.8.8"]);

const connectDB = async () => {
    const conn = await mongoose.connect(process.env.MONGODB_URI);

    console.log(`MongoDB connected: ${conn.connection.host}`);
};

export default connectDB;
```

Initially, I tried connecting to **MongoDB (cloud)** but faced a DNS issue

**Problem:**

MongoDB connection was failing due to DNS resolution.

**Fix:**

After watching a YouTube video: , I added this:

```javascript
const dns = require("dns");
dns.setServers(["1.1.1.1", "8.8.8.8"]);
```

This fixed the issue and I was able to connect to cloud MongoDB.

Update `server.js` :

```javascript
import "dotenv/config"; //This automatically loads .env variables into process.env
import app from "./src/app.js";

import connectDB from "./src/common/config/db.js"

const PORT = process.env.PORT || 3000;

const start = async () => {
    await connectDB();
    app.listen(PORT, () => {
        console.log(`Server is running at ${PORT} in ${process.env.NODE_ENV} mode`);
    });
};

start().catch((err) => {
    console.error("Failed to start server", err);
    process.exit(1);
});
```

* * *

### Step 8: Standard API Response Structure

**Create:**

```plaintext
src/common/utils/api-response.js
```

```javascript
class ApiResponse {
    static ok(res, message, data = null) {
        return res.status(200).json({
            success: true,
            message,
            data
        });
    }
}

export default ApiResponse;
```

**Why This is Important**

Instead of writing:

```javascript
res.status(200).json({ message: "Success" });
```

To avoid repeating response logic across multiple APIs, I implemented a reusable utility using the DRY (Don't Repeat Yourself) principle:

```javascript
ApiResponse.ok(res, "Success", data);
```

* * *

### Step 9: Standard API Error Structure

While building APIs, handling errors properly is **as important as handling success responses**.

1.  **Create Custom Error Class**
    

```js
src/common/utils/api-error.js
```

```javascript
class ApiError extends Error {
    // Inherit from built-in JavaScript Error
    constructor(statusCode, message) {
        super(message);

        this.statusCode = statusCode;

        // Capture clean stack trace
        Error.captureStackTrace(this, this.constructor);
    }

    static badRequest(message = "Bad Request") {
        return new ApiError(400, message);
    }

    static forbidden(message = "Forbidden") {
        return new ApiError(403, message);
    }

    static notFound(message = "Not Found") {
        return new ApiError(404, message);
    }
}

export default ApiError;
```

**Why Extend** `Error`**?**

JavaScript provides built-in errors like:

*   `Error`
    
*   `TypeError`
    
*   `ReferenceError`
    

But they **don’t include HTTP status codes**, which are required in APIs.

**2\. Global Error Middleware**

Create:

```plaintext
src/common/middleware/error.middleware.js
```

```javascript
import ApiError from "../utils/api-error.js";

const errorHandler = (err, req, res, next) => {

    // Handle known (custom) errors
    if (err instanceof ApiError) {
        return res.status(err.statusCode).json({
            success: false,
            message: err.message
        });
    }

    // Handle unknown errors
    return res.status(500).json({
        success: false,
        message: "Internal Server Error"
    });
};

export default errorHandler;
```

**3\. Register Middleware in App**

Update `src/app.js`:

```javascript
import express from "express";
import authRoute from "./modules/auth/auth.routes.js";
import errorHandler from "./common/middleware/error.middleware.js";

const app = express();
app.use(express.json());

app.use(errorHandler); 

export default app;
```

This must be the **last middleware** in the app.

**Final Error Flow**

```plaintext
Request → Route → Controller → Service → throws ApiError → Passes to errorHandler middleware → JSON response 
```

* * *

### Step 10: Authentication Module (Modular Structure)

Now that we have:

*   Clean folder structure
    
*   Database setup
    
*   Standard response & error handling
    

Next step is to build a **modular authentication system**

* * *

**Module Structure**

Inside `src/modules/auth/`, I created:

```plaintext
auth/
├── auth.controller.js
├── auth.middleware.js
├── auth.model.js
├── auth.routes.js
├── auth.service.js
```

* * *

**Why This Structure?**

Each layer has a clear responsibility:

*   **Model** → Database schema
    
*   **Routes** → Define API endpoints
    
*   **Controller** → Handle request/response
    
*   **Service** → Business logic
    
*   **Middleware** → Authentication / validation
    

This makes code **clean, maintainable, and scalable**

* * *

**Create User Schema (**`auth.model.js`**)**

```javascript
import mongoose from "mongoose";

const userSchema = new mongoose.Schema(
  {
    firstName: {
      type: String,
      required: true,
      trim: true,
    },

    lastName: {
      type: String,
      required: true,
      trim: true,
    },

    username: {
      type: String,
      required: true,
      unique: true,
      trim: true,
    },

    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
    },

    phoneNumber: {
      type: String,
      required: true,
      trim: true,
    },

    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true },
);

const userModel = mongoose.model("users", userSchema);

export default userModel;
```

`timestamps` automatically adds:

*   `createdAt`
    
*   `updatedAt`
    

* * *

**Register Auth Routes in App.js**

Update `src/app.js`:

```javascript
import express from "express";
import authRoute from "./modules/auth/auth.routes.js";
import errorMiddleware from "./common/middleware/error.middleware.js";

const app = express();

app.use(express.json());

// Register routes
app.use("/api/auth", authRoute);

// Global error handler
app.use(errorMiddleware);

export default app;
```

All auth APIs will now be available under:

```plaintext
/api/auth/*
```

* * *

All auth APIs will now be available under:

```plaintext
/api/auth/*
```

* * *

**Define Routes (**`auth.routes.js`**)**

```javascript
import { Router } from "express";
import * as controller from "./auth.controller.js";

const router = Router();

router.post("/register", controller.signup);

export default router;
```

* * *

**Controller Layer (**`auth.controller.js`**)**

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as authService from "./auth.service.js";

const register = async (req, res, next) => {
  try {
    const user = await authService.register(req.body);

    ApiResponse.ok(res, "User get Created", user);
  } catch (error) {
    next(error); // pass error to global error middleware
  }
};
export { register };
```

**Responsibility of Controller**

*   Calls service layer
    
*   Sends response using `ApiResponse`
    
*   Keeps logic minimal
    

* * *

**Business Logic (**`auth.service.js`**)**

```javascript
import bcrypt from 'bcrypt';
import userModel from "./auth.model.js";
import ApiError from "../../common/utils/api-error.js";

const register = async ({
  firstName,
  lastName,
  username,
  email,
  phoneNumber,
  password,
}) => {
  const userExist = await userModel.findOne({
    email,
  });

  if (userExist) {
    throw ApiError.forbidden("User Already Exists");
  }

  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = await userModel.create({
    firstName,
    lastName,
    username,
    email,
    phoneNumber,
    password: hashedPassword,
  });

  return {
    userId: newUser._id,
    firstName: newUser.firstName,
    lastName: newUser.lastName,
    username: newUser.username,
    email: newUser.email,
    phoneNumber: newUser.phoneNumber,
  };
};
```

* * *

**Final Result: Testing /api/auth/register API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/8e4d54f5-cd97-4a48-ae00-852175973f8e.png align="center")

* * *

**Request Flow**

```plaintext
POST /api/auth/register
  ↓
auth.routes.js
  ↓
auth.controller.js
  ↓
auth.service.js
  ↓
MongoDB
```

* * *

### Step 11: Implement Login

Now that the register API is ready, the next step is to allow users to login securely using JWT-based authentication.

Instead of using a single token, we improve security by implementing:

*   Access Token
    
*   Refresh Token
    

This follows modern authentication practices used in production applications.

* * *

**Why Use Access Token + Refresh Token?**

Previously, authentication systems often used a single JWT token.

Problem:

If that token gets stolen, the attacker can access protected APIs until the token expires.

To improve security, authentication is usually split into two tokens:

| Token | Purpose | Expiry |
| --- | --- | --- |
| Access Token | Access protected APIs | Short-lived (10–15 mins) |
| Refresh Token | Generate new access tokens | Long-lived (7–30 days) |

* * *

**Why Access Token Is Short-Lived**

Even if an attacker steals the access token:

*   it expires quickly
    
*   attacker loses access soon
    

But refresh tokens live much longer, so they require stronger protection.

* * *

**JWT Utility Functions**

Instead of writing JWT logic multiple times, I created reusable utility functions.

Create:

`src/common/utils/jwt.utils.js`

```js
import jwt from "jsonwebtoken";

const generateAccessToken = (payload) => {
  return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, {
    expiresIn: process.env.JWT_ACCESS_EXPIRES_IN || "15m",
  });
};

const generateRefreshToken = (payload) => {
  return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
    expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "7d",
  });
};

const verifyAccessToken = (token) => {
  return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
};

export {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
};
```

* * *

**Update Environment Variables**

Update `.env`

```plaintext
JWT_ACCESS_SECRET=your_access_secret
JWT_REFRESH_SECRET=your_refresh_secret

JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
```

* * *

**Define Routes**

Inside `auth.route.js`, we define the login route:

```javascript
import { Router } from "express";
import * as controller from "./auth.controller.js";

const router = Router();

router.post("/register", controller.register);
router.post("/login", controller.login)

export default router;
```

This creates an endpoint:

```plaintext
POST /api/auth/login
```

* * *

**Controller Layer**

Now we handle the request in the controller `auth.controller.js`.

```js
import ApiResponse from "../../common/utils/api-response.js";

import * as authService from "./auth.service.js";

const login = async (req, res, next) => {
  try {
    const { accessToken, refreshToken, user } = await authService.login(
      req.body,
    );
    res.cookie("refreshToken", refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    });
    ApiResponse.ok(res, "Login Successful", { accessToken, user });
  } catch (error) {
    next(error);
  }
};

export { login };
```

* * *

**What Are Cookies?**

Cookies are small pieces of data stored by the browser for a website.

* * *

**Why Cookies Exist**

HTTP is stateless.

That means every request is independent.

Example:

```plaintext
Request 1 → Login

Request 2 → Fetch Profile
```

Normally, the server cannot remember that both requests came from the same user.

Cookies solve this problem.

* * *

**How Cookies Work :**

**Step 1: Server Sends Cookie**

Backend response:

```plaintext
Set-Cookie: refreshToken=abc123
```

Browser stores it automatically.

**Step 2: Browser Sends It Automatically**

Next request:

```plaintext
Cookie: refreshToken=abc123
```

Now the server can identify the user.

* * *

**Important Cookie Security Options**

**1\. httpOnly**

```plaintext
httpOnly: true
```

JavaScript cannot access the cookie.

Protects against XSS attacks.

**2\. secure**

```plaintext
secure: true
```

if `secure: true` Cookie is sent only over HTTPS and if `secure: false` cookies is send over HTTP

**3\. sameSite**

```plaintext
sameSite: "strict"
```

Controls cross-site cookie sharing.

Helps prevent CSRF attacks.

* * *

**Responsibility of Controller**

*   Receive request data (`req.body`)
    
*   Call service layer
    
*   Store refresh token in cookies
    
*   Send access token in response
    
*   Pass errors to global error handler
    

* * *

**Service Layer (Business Logic)**

Now the actual authentication logic lives in `auth.service.js`:

```javascript
import bcrypt from "bcrypt";

import userModel from "./auth.model.js";

import ApiError from "../../common/utils/api-error.js";

import {
  generateAccessToken,
  generateRefreshToken,
} from "../../common/utils/jwt.utils.js";

const login = async ({ email, password }) => {
  const userExist = await userModel.findOne({
    email: email,
  });

  if (!userExist) {
    throw ApiError.forbidden("User Not Found");
  }

  const correctPassword = await bcrypt.compare(password, userExist.password);

  if (correctPassword) {
    const accessToken = generateAccessToken({ userId: userExist._id });
    const refreshToken = generateRefreshToken({ userId: userExist._id });
    return {
      accessToken,
      refreshToken,
      user: {
        userId: userExist._id,
        firstName: userExist.firstName,
        lastName: userExist.lastName,
        username: userExist.username,
        email: userExist.email,
        phoneNumber: userExist.phoneNumber,
      },
    };
  } else {
    throw ApiError.forbidden("Password is invalid");
  }
};

export {
  login
};
```

* * *

**Final Result: Testing /api/auth/login API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/db3a06bf-6d1d-457a-b1b4-66b48c0a8b97.png align="center")

* * *

### Step 12: Refresh Access Token

Previously, during login:

*   Access Token was returned in the response
    
*   Refresh Token was stored securely in HTTP-only cookies
    

But access tokens are short-lived.

Example:

*   Access Token → expires in 15 minutes
    
*   Refresh Token → expires in 7 days
    

So after 15 minutes, protected APIs stop working because the access token becomes invalid.

Instead of asking the user to log in again, we use the refresh token to generate a new access token automatically.

* * *

**Why Refresh Tokens Are Important**

Without refresh tokens:

*   User would need to login repeatedly
    
*   Poor user experience
    
*   More authentication requests
    

With refresh tokens:

*   Access tokens remain short-lived (more secure)
    
*   Users stay logged in smoothly
    
*   Better production-level authentication flow
    

* * *

# Authentication Flow

Login  
↓  
Access Token generated  
↓  
Refresh Token stored in cookie  
↓  
Access Token expires  
↓  
Frontend calls /refresh  
↓  
Backend verifies refresh token  
↓  
New Access Token generated

* * *

**Update JWT Utility Functions**

Previously, we only added:

*   generateAccessToken()
    
*   generateRefreshToken()
    

Now we also need:

*   verifyRefreshToken()
    

Update:

`src/common/utils/jwt.utils.js`

```js
import jwt from "jsonwebtoken";

const generateAccessToken = (payload) => {
  return jwt.sign(
    payload,
    process.env.JWT_ACCESS_SECRET,
    {
      expiresIn:
        process.env.JWT_ACCESS_EXPIRES_IN || "15m",
    }
  );
};

const generateRefreshToken = (payload) => {
  return jwt.sign(
    payload,
    process.env.JWT_REFRESH_SECRET,
    {
      expiresIn:
        process.env.JWT_REFRESH_EXPIRES_IN || "7d",
    }
  );
};

const verifyAccessToken = (token) => {
  return jwt.verify(
    token,
    process.env.JWT_ACCESS_SECRET
  );
};

const verifyRefreshToken = (token) => {
  return jwt.verify(
    token,
    process.env.JWT_REFRESH_SECRET
  );
};

export {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken,
};
```

* * *

**Define Refresh Route**

Update:

`src/modules/auth/auth.routes.js`

```javascript
import { Router } from "express";
import * as controller from "./auth.controller.js";

const router = Router();

router.post("/register", controller.register);

router.post("/login", controller.login);

router.post("/refresh", controller.refresh);

export default router;
```

This creates:

```plaintext
POST /api/auth/refresh
```

* * *

**Controller Layer**

Now we handle refresh requests inside:

`src/modules/auth/auth.controller.js`

```javascript
const refresh = async (req, res, next) => {
  try {
    const { accessToken } =
      await authService.refresh(
        req.cookies.refreshToken
      );

    ApiResponse.ok(
      res,
      "Token refreshed successfully",
      { accessToken }
    );
  } catch (error) {
    next(error);
  }
};
```

**Why Read Token From Cookies?**

Because refresh tokens are sensitive.

Storing them inside:

```plaintext
httpOnly cookies
```

prevents JavaScript from accessing them.

This protects against:

*   XSS attacks
    
*   Token theft through frontend scripts
    

* * *

**Service Layer (Business Logic)**

Now the actual refresh logic lives inside:

`src/modules/auth/auth.service.js`

```javascript
const refresh = async (token) => {
  if (!token) {
    throw ApiError.unauthorized(
      "Refresh token missing"
    );
  }

  const decoded =
    verifyRefreshToken(token);

  const userExists =
    await userModel.findOne({
      _id: decoded.userId,
    });

  if (!userExists) {
    throw ApiError.notFound(
      "User Not found"
    );
  }

  const accessToken =
    generateAccessToken({
      userId: userExists._id,
    });

  return { accessToken };
};
```

* * *

**Final Refresh Flow**

Frontend Request  
↓  
/api/auth/refresh  
↓  
Read refreshToken cookie  
↓  
Verify refresh token  
↓  
Find user  
↓  
Generate new access token  
↓  
Send response

* * *

### Step 13: Get Current User & Logout APIs

After implementing:

*   Register
    
*   Login
    
*   Refresh Token
    

the next important step is handling:

*   Fetching logged-in user details
    
*   Logging users out securely
    

These are common authentication features used in almost every production application.

* * *

**Why Do We Need GetMe API?**

After login, the frontend often needs user information like:

*   Name
    
*   Email
    
*   Username
    
*   Phone number
    

Instead of storing all user data in frontend state permanently, we create a dedicated API:

```plaintext
GET /api/auth/getme
```

This endpoint:

*   Verifies the access token
    
*   Identifies the logged-in user
    
*   Returns fresh user details
    

This helps keep frontend authentication state synchronized with the backend.

* * *

**Why Do We Need Logout?**

Even though JWT authentication is stateless, users still need a secure way to end their session.

During login:

*   Access token is stored on frontend
    
*   Refresh token is stored inside cookies
    

If logout is not implemented:

*   Refresh token remains active
    
*   User session can continue unintentionally
    

So during logout we:

*   Clear refresh token cookie
    
*   End user session safely
    

* * *

**Define Routes**

Update:

`src/modules/auth/auth.routes.js`

```javascript
import { Router } from "express";
import authMiddleware from "./auth.middleware.js";

import * as controller from "./auth.controller.js";

const router = Router();

router.post("/register", controller.register);

router.post("/login", controller.login);

router.post("/refresh", controller.refresh);

router.get(
  "/getme",
  authMiddleware,
  controller.getMe
);

router.get(
  "/logout",
  authMiddleware,
  controller.logout
);

export default router;
```

* * *

**Controller Layer**

Update:

`src/modules/auth/auth.controller.js`

```js
const getMe = async (req, res, next) => {
  try {
    const userId = req.userId;

    console.log("userId", userId);

    const { user } =
      await authService.getMe(userId);

    ApiResponse.ok(
      res,
      "User get successfully",
      { user }
    );
  } catch (error) {
    next(error);
  }
};

const logout = async (req, res, next) => {
  try {
    res.clearCookie("refreshToken");

    ApiResponse.ok(
      res,
      "Logout Success"
    );
  } catch (error) {
    next(error);
  }
};
```

* * *

**What Does clearCookie() Do?**

```plaintext
res.clearCookie("refreshToken");
```

This removes the refresh token from browser cookies.

Result:

*   User can no longer refresh access tokens
    
*   Session is effectively terminated
    

* * *

### Service Layer (Business Logic)

### Update:

`src/modules/auth/auth.service.js`

```javascript
const getMe = async (userId) => {
  if (!userId) {
    throw ApiError.notFound(
      "user not found"
    );
  }

  const userExist =
    await userModel.findOne({
      _id: userId,
    });

  if (!userExist) {
    throw ApiError.notFound(
      "User Not found"
    );
  }

  return {
    user: {
      userId: userExist._id,
      firstName: userExist.firstName,
      lastName: userExist.lastName,
      username: userExist.username,
      email: userExist.email,
      phoneNumber:
        userExist.phoneNumber,
    },
  };
};
```

* * *

### **Step 14: Create household inside households Module**

Now that authentication is complete, the next step is to allow users to create household

* * *

**Requirements**

After logging in, a user should be able to:

*   **Create a new household** by providing:
    
    *   a valid **household name**
        
    *   a **6-character invite code** (chosen by the user)
        
*   Only **authenticated users** are allowed to create a household.
    

* * *

**Module Structure**

Inside `src/modules/`, create a new folder:

```plaintext
src/modules/households/
├── households.controller.js
├── households.middleware.js
├── households.model.js
├── households.routes.js
├── households.service.js
```

* * *

**Create HouseHold Schema**

Create:

```plaintext
src/modules/households/households.model.js
```

```javascript
import mongoose from "mongoose";

const houseHoldSchema = new mongoose.Schema({
    name: String,
    inviteCode: String,
    members: [mongoose.Types.ObjectId],
    createdBy: mongoose.Types.ObjectId
}, { timestamps: true })

const houseHoldModel = mongoose.model("households", houseHoldSchema)

export default houseHoldModel;
```

* * *

**Authentication Middleware**

To ensure only logged-in users can create organisations:

Inside:

```plaintext
src/modules/auth/auth.middleware.js
```

```javascript
import jwt from 'jsonwebtoken';

const authMiddleware = async (req, res, next) => {
    try{
        const token = req.headers.token;

        const decode = jwt.verify(token, process.env.JWT_SECRET);

        const userExists = await userModel.findOne({
            _id: decode.userId
        })

        if(!userExists){
            throw ApiError.notFound("User Not found");
        }
    
        req.userId = decode.userId;
    
        next();
    } catch (error){
        next(error)
    }
};

export default authMiddleware;
```

**Update Global Error Middleware**

Update your error middleware to handle JWT-specific errors:

```js
src/common/middleware/error-middleware.js
```

```js
import ApiError from "../utils/api-error.js";

const errorHandler = (err, req, res, next) => {

    // Handle known (custom) errors
    if (err instanceof ApiError) {
        return res.status(err.statusCode).json({
            success: false,
            message: err.message
        });
    }

    if (err.name === 'JsonWebTokenError') {
        return res.status(401).json({
            success: false,
            message: err.message
        });
    }



    // Handle unknown errors
    return res.status(500).json({
        success: false,
        message: "Internal Server Error"
    });
};

export default errorHandler;
```

* * *

**Register Household Routes**

Update `src/app.js`:

```javascript
import householdsRoute from './modules/households/households.routes.js'

app.use("/api/households", orgRoute);
```

Now all households APIs will be available under:

```plaintext
/api/households/* 
```

* * *

**Define Routes**

**Create:**

```plaintext
src/modules/households/households.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from './households.controller.js';

const router = Router();

router.post('/', authMiddleware, controller.createHouseHold);

export default router;
```

* * *

**Controller Layer**

**Create:**

```plaintext
src/modules/households/households.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as householdService from './households.service.js'

const createHouseHold = async (req, res, next) => {
    try {
        const data = await householdService.createHouseHold(req);
        ApiResponse.ok(res, "Household Created Successfully", data);
    } catch (err) {
        next(err);
    }
}

export {
    createHouseHold
}
```

**Responsibility of Controller**

*   Calls service layer
    
*   Sends success response
    
*   Passes errors to global middleware
    

* * *

**Business Logic (Service Layer)**

Create:

```plaintext
src/modules/households/households.service.js
```

```javascript
const createHouseHold = async (req) => {
  const householdName = req.body.householdName;
  const inviteCode = req.body.inviteCode;
  const createdBy = req.userId;

  // CHECK IF INVITE CODE ALREADY EXISTS
  const inviteCodeExists = await houseHoldModel.findOne({
    inviteCode,
  });

  if (inviteCodeExists) {
    throw ApiError.badRequest("Invite code already exists");
  }

  const members = [req.userId];
  const newHouseHold = await houseHoldModel.create({
    householdName,
    inviteCode,
    members,
    createdBy,
  });

  // UPDATE USER WITH HOUSEHOLD ID
  await userModel.findByIdAndUpdate(createdBy, {
    householdId: newHouseHold._id,
  });

  return {
    householdId: newHouseHold._id,
    name: newHouseHold.name,
    members: newHouseHold.members,
    inviteCode: newHouseHold.inviteCode,
  };
};
```

* * *

**Request Flow**

```plaintext
POST /api/households/
   ↓
auth.middleware.js (verify token)
   ↓
households.controller.js
   ↓
households.service.js
   ↓
MongoDB
   ↓
Response
```

* * *

**Final Result: Testing api/households/ API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/891b2c64-4804-470d-8d97-b0e111020009.png align="center")

* * *

### **Step 14:** Join Household Feature

After implementing household creation, the next step is to allow users to **join an existing household using an invite code**.

* * *

**Define Routes**

**Create:**

```plaintext
src/modules/households/households.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from './households.controller.js';

const router = Router();

router.post('/', authMiddleware, controller.createHouseHold);
router.post('/join', authMiddleware, controller.joinHouseHold);

export default router;
```

* * *

**Controller Layer**

**Create:**

```plaintext
src/modules/households/households.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as householdService from './households.service.js'

const joinHouseHold = async (req, res, next) => {
    try {
        const data = await householdService.joinHouseHold(req);
        ApiResponse.ok(res, "Household Joined Successfully", data);
    } catch (err) {
        next (err);
    }
}

export {
    joinHouseHold
}
```

**Responsibility of Controller**

*   Calls service layer
    
*   Sends success response
    
*   Passes errors to global middleware
    

* * *

**Business Logic (Service Layer)**

Create:

```plaintext
src/modules/households/households.service.js
```

```javascript
const joinHouseHold = async (req) => {
  const { inviteCode } = req.body;

  const houseHoldExists = await houseHoldModel.findOne({
    inviteCode,
  });

  if (!houseHoldExists) {
    throw ApiError.badRequest("Invalid Invite Code");
  }

  const newMember = req.userId;

  const memberExists = houseHoldExists?.members?.includes(newMember);

  if (memberExists) {
    throw ApiError.badRequest("Member already exists in household");
  }

  houseHoldExists.members.push(newMember);

  await houseHoldExists.save();

  // UPDATE USER
  await userModel.findByIdAndUpdate(newMember, {
    householdId: houseHoldExists._id,
  });

  return {
    householdId: houseHoldExists._id,
    name: houseHoldExists.name,
    members: houseHoldExists.members,
  };
};
```

* * *

**Request Flow**

```plaintext
POST /api/households/join
   ↓
auth.middleware.js (verify token)
   ↓
households.controller.js
   ↓
households.service.js
   ↓
MongoDB
   ↓
Response
```

* * *

**Final Result: Testing api/households/join API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/2bf6503e-5eaf-4a26-adcb-50342baf091d.png align="center")

* * *

### Step 15: Get All Household Members

After implementing join functionality, the next step is to allow users to **view all members of a household**.

* * *

**Update Household Model (Add Reference)**

To fetch full user details, we need to define a reference in the schema.

Update:

```plaintext
src/modules/households/households.model.js
```

```javascript
import mongoose from "mongoose";

const houseHoldSchema = new mongoose.Schema({
    name: String,
    inviteCode: String,
    members: [{
        type: mongoose.Types.ObjectId,
        ref: "users" // reference to users collection
    }],
    createdBy: mongoose.Types.ObjectId
}, { timestamps: true });

const houseHoldModel = mongoose.model("households", houseHoldSchema);

export default houseHoldModel;
```

**Why Add** `ref`**?**

*   MongoDB stores only **ObjectIds** in `members`
    
*   `ref: "users"` tells Mongoose:
    
    “These IDs belong to the users collection”
    
*   This enables the use of **populate** (explained below)
    

* * *

**Define Routes**

Update:

```plaintext
src/modules/households/households.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from './households.controller.js';

const router = Router();

router.post('/', authMiddleware, controller.createHouseHold);
router.post('/join', authMiddleware, controller.joinHouseHold);
router.get('/:id/members', authMiddleware, controller.getAllMembers)

export default router;
```

* * *

**Controller Layer**

Create / Update:

```plaintext
src/modules/households/households.controller.js
```

```javascript
const getAllMembers = async (req, res, next) => {
    try {
        const data = await householdService.getAllMembers(req);
        ApiResponse.ok(res, "All Members", data);
    } catch (err) {
        next(err);
    }
};
```

* * *

**Service Layer (Business Logic)**

Create / Update:

```plaintext
src/modules/households/households.service.js
```

```javascript
const getAllMembers = async (req) => {
    const houseHoldId = req.params.id;

    const houseHoldExists = await houseHoldModel
        .findById(houseHoldId)
        .populate('members', 'name email');

    if (!houseHoldExists) {
        throw ApiError.notFound("Household not found");
    }

    return houseHoldExists.members;
};
```

* * *

**What is** `.populate()`**?**

In MongoDB, we store references like this:

```javascript
members: [ObjectId("1234"), ObjectId("5678")]
```

If we fetch data without populate:

```javascript
{
  "members": ["1234", "5678"]
}
```

This is not useful for the frontend.

* * *

**Using Populate**

```javascript
.populate('members', 'name email')
```

This tells Mongoose:

> Replace member IDs with actual user data (only name and email)

* * *

**Output After Populate**

```javascript
{
  "members": [
    {
      "_id": "64abc...",
      "name": "Shubham",
      "email": "shubham@gmail.com"
    }
  ]
}
```

* * *

**Why Use Populate?**

*   Converts **ObjectIds → real data**
    
*   Works similar to **JOIN in SQL**
    
*   Reduces extra API calls from frontend
    

* * *

**Request Flow**

```plaintext
GET /api/households/:id/members
   ↓
auth.middleware.js (verify token)
   ↓
households.controller.js
   ↓
households.service.js
   ↓
MongoDB (findById + populate)
   ↓
Response
```

* * *

**Final Result: Testing /api/households/:id/members API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/2b5920cb-5112-4818-873a-a76cae16099b.png align="center")

* * *

### Step 16: Leave Household Feature

After implementing the ability to join a household, the next important feature is to allow users to **leave a household**.

* * *

**Define Routes**

Update:

```plaintext
src/modules/households/households.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import * as controller from './households.controller.js';

const router = Router();

router.post('/', authMiddleware, controller.createHouseHold);
router.post('/join', authMiddleware, controller.joinHouseHold);
router.get('/:id/members', authMiddleware, controller.getAllMembers)

// Leave household
router.delete('/:id/leave', authMiddleware, controller.leaveHouseHold);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/households/households.controller.js
```

```javascript
const leaveHouseHold = async (req, res, next) => {
    try {
        const data = await householdService.leaveHouseHold(req);
        ApiResponse.ok(res, "Left Household Successfully", data);
    } catch (err) {
        next(err);
    }
};
```

* * *

**Service Layer (Business Logic)**

Update:

```js
src/modules/households/households.service.js
```

```js
const leaveHouseHold = async (req) => {
    const houseHoldId = req.params.id;
    const userId = req.userId;

    const houseHold = await houseHoldModel.findById(houseHoldId);

    if (!houseHold) {
        throw ApiError.notFound("Household not found");
    }

    const isMember = houseHold.members.some(
        (member) => member.toString() === userId
    );

    if (!isMember) {
        throw ApiError.badRequest("User is not a member of this household");
    }

    houseHold.members = houseHold.members.filter(
        (member) => member.toString() !== userId
    );

    //If creator/admin leaves
    if (houseHold.createdBy.toString() === userId) {
        if (houseHold.members.length === 0) {
            await houseHold.deleteOne();
            return { message: "Household deleted as no members left" };
        }else{
            houseHold.createdBy = houseHold.members[0];
        }
    }

    await houseHold.save();

    return {
        householdId: houseHold._id,
        members: houseHold.members
    };
}
```

* * *

**Request Flow**

```plaintext
DELETE /api/households/:id/leave
   ↓
auth.middleware.js (verify token)
   ↓
households.controller.js
   ↓
households.service.js
   ↓
MongoDB (find → update → save/delete)
   ↓
Response
```

* * *

**Final Result: Testing /api/households/:id/leave API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/09452f0d-30f3-4808-8e11-810fa7e92373.png align="center")

* * *

### Step 17: Add Items Feature (Inventory Management)

Now that households are implemented, the next step is to allow users to **add grocery items inside a household**.

This is the **core functionality** of the application.

* * *

**Requirement**

A logged-in user should be able to:

*   Add items with:
    
    *   **name**
        
    *   **category**
        
    *   **quantity**
        
    *   **expiry date**
        
*   The system should automatically assign a **status**:
    
    *   `fresh`
        
    *   `expiring-soon` (≤ 3 days)
        
    *   `expired`
        

* * *

**Module Structure**

Created a new module:

```plaintext
src/modules/items/
├── items.controller.js
├── items.middleware.js
├── items.model.js
├── items.routes.js
├── items.service.js
```

* * *

**Item Schema**

Create:

```plaintext
src/modules/items/items.model.js
```

```javascript
import mongoose from "mongoose";

const itemsSchema = new mongoose.Schema({
    householdId: {
        type: mongoose.Types.ObjectId,
        ref: "households"
    },
    addedBy: {
        type: mongoose.Types.ObjectId,
        ref: "users"
    },
    name: {
        type: String,
        required: true
    },
    category: {
        type: String,
        enum: ['produce', 'dairy', 'meat', 'pantry', 'frozen']
    },
    quantity: Number,
    expiryDate: Date,
    status: {
        type: String,
        enum: ['fresh', 'expiring-soon', 'expired', 'used', 'wasted']
    }
}, { timestamps: true });

const itemsModel = mongoose.model("items", itemsSchema);

export default itemsModel;
```

* * *

**Why Enum?**

We use `enum` to **restrict values**.

Example:

*   Category must be one of:
    
    *   `produce`, `dairy`, `meat`, `pantry`, `frozen`
        

If any other value is passed → Mongoose throws a validation error.

* * *

**Household Middleware**

Before adding an item, we must ensure:

The user **belongs to a household**

Create:

```plaintext
src/modules/households/households.middleware.js
```

```javascript
import ApiError from '../../common/utils/api-error.js';
import houseHoldModel from './households.model.js';

const housholdMiddleware = async (req, res, next) => {
    try {
        const userId = req.userId;

        const householdExists = await houseHoldModel.findOne({
            members: userId
        });

        if (!householdExists) {
            throw ApiError.notFound("HouseHold Not found");
        }

        req.householdId = householdExists._id.toString();

        next();
    } catch (error) {
        next(error);
    }
};

export default housholdMiddleware;
```

* * *

**Why This Middleware?**

*   Ensures user is part of a household
    
*   Prevents unauthorized item creation
    
*   Attaches `householdId` to request
    

* * *

**Routes**

Create:

```plaintext
src/modules/items/items.routes.js
```

```javascript
import { Router } from "express";
import authMiddleware from "../auth/auth.middleware.js";
import housholdMiddleware from "../households/households.middleware.js";
import * as controller from './items.controller.js';

const router = Router();

router.post('/', authMiddleware, housholdMiddleware, controller.createItem);

export default router;
```

* * *

**Controller Layer**

Create:

```plaintext
src/modules/items/items.controller.js
```

```javascript
import ApiResponse from "../../common/utils/api-response.js";
import * as itemService from './items.service.js';

const createItem = async (req, res, next) => {
    try {
        const data = await itemService.createItem(req);
        ApiResponse.ok(res, "Item Created Successfully", data);
    } catch (err) {
        next(err);
    }
};

export {
    createItem
};
```

* * *

**Responsibility of Controller**

*   Calls service layer
    
*   Sends success response
    
*   Passes errors to middleware
    

* * *

**Service Layer (Business Logic)**

Create:

```plaintext
src/modules/items/items.service.js
```

```javascript
import ApiError from "../../common/utils/api-error.js";
import itemsModel from "./items.model.js";

const createItem = async (req) => {
    const { name, category, quantity, expiryDate } = req.body;
    const userId = req.userId;
    const householdId = req.householdId;

    if (!name || !category || !quantity || !expiryDate) {
        throw ApiError.badRequest("All fields are required");
    }

    const today = new Date();
    const expiry = new Date(expiryDate);

    // Normalize time (important for correct comparison)
    //Removes time part → avoids wrong comparisons
    today.setHours(0, 0, 0, 0);
    expiry.setHours(0, 0, 0, 0);

    const diffTime = expiry - today;
    // 1000 → milliseconds in 1 second
    // 60 → seconds in 1 minute
    // 60 → minutes in 1 hour
    // 24 → hours in 1 day
    //1 day = 86,400,000 milliseconds
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

    let status = "fresh";

    if (diffDays < 0) {
        status = "expired";
    } else if (diffDays <= 3) {
        status = "expiring-soon";
    }

    const newItem = await itemsModel.create({
        householdId,
        addedBy: userId,
        name,
        category,
        quantity,
        expiryDate,
        status
    });

    return {
        _id: newItem._id,
        name: newItem.name,
        status: newItem.status
    };
}

export {
    createItem
}
```

* * *

**Request Flow**

```plaintext
POST /api/items/
   ↓
auth.middleware.js
   ↓
household.middleware.js
   ↓
items.controller.js
   ↓
items.service.js
   ↓
MongoDB
   ↓
Response
```

* * *

**Final Result: Testing /api/items/ API Using Postman**

![](https://cdn.hashnode.com/uploads/covers/624226a5db84f8c50fa5b247/a2c973e9-b3b0-4cbc-913a-84dd538fa42c.png align="center")

* * *

### Step 18: Automated Status Updates (Using Cron Job)

Now that item creation is implemented, the next improvement is making item status **dynamic instead of static**.

Because expiry status changes over time, we use a background cron job.

* * *

**Why This is Needed?**

When an item is created:

*   It may be **fresh today**
    
*   But before 2 days of expiry date it becomes **expiring-soon**
    
*   After expiry → **expired**
    

So status must update automatically without user action.

* * *

**Solution :**

To solve this, we introduce a **background cron job**.

A cron job runs at a fixed interval and updates item status automatically based on current date.

We used:

```plaintext
node-cron
```

* * *

**Cron Job Setup (Testing Mode)**

Initially, I tested the cron job by running it every minute.

Create:

```plaintext
src/modules/items/items.cron.js
```

```javascript
import cron from "node-cron";

cron.schedule("* * * * *", () => {
  console.log(
    "Cron job running every minute at",
    new Date().toLocaleTimeString()
  );
});
```

* * *

**Understanding the Cron Expression**

```javascript
"* * * * *"
```

This means:

```plaintext
minute hour day month weekday
```

So the above expression runs:

*   Every minute
    

For production, this can later be changed to:

```javascript
0 6 * * *
```

Which means:

*   Run every day at 6 AM
    

For more details checkout node-cron documentation: [https://www.nodecron.com/getting-started.html](https://www.nodecron.com/getting-started.html)

* * *

**Implementing Status Update Logic**

Now the next step is to update item statuses automatically.

Inside the service layer, we created updateItemStatuses function to implement the logic:

*   Fetch all items from database(item collection)
    
*   Compare expiry date with current date
    
*   Calculate difference in days
    
*   Assign correct status
    

**Rules:**

*   diffDays < 0 → expired
    
*   diffDays <= 3 → expiring-soon
    
*   else → fresh
    

```plaintext
src/modules/items/items.service.js
```

```javascript
const updateItemStatuses = async () => {
  try {
    const items = await itemsModel.find({});

    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const bulkOperations = [];
    const expiringItems = [];

    for (const item of items) {
      const expiry = new Date(item.expiryDate);

      expiry.setHours(0, 0, 0, 0);

      const diffTime = expiry - today;

      const diffDays = Math.ceil(
        diffTime / (1000 * 60 * 60 * 24)
      );

      let newStatus = "fresh";

      if (diffDays < 0) {
        newStatus = "expired";
      } else if (diffDays <= 3) {
        newStatus = "expiring-soon";

        expiringItems.push(item);
      }

      // Update only if status changed
      if (item.status !== newStatus) {
        bulkOperations.push({
          updateOne: {
            filter: { _id: item._id },
            update: {
              $set: {
                status: newStatus,
              },
            },
          },
        });
      }
    }

    // Bulk update optimization
    if (bulkOperations.length > 0) {
      await itemsModel.bulkWrite(bulkOperations);

      console.log(
        `Updated ${bulkOperations.length} items`
      );
    }

    return expiringItems;
  } catch (error) {
    console.error("Cron job failed:", error);
  }
};
```

* * *

**Why Use bulkWrite (mongoDB) ?**

Instead of updating items one by one:

```plaintext
await item.save();
```

We use:

```plaintext
itemsModel.bulkWrite(bulkOperations);
```

Benefits:

*   Fewer database calls
    
*   Better performance
    
*   More scalable approach
    

Check out mongodb documentation on **bulkWrite**: [https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/) )

* * *

**Collecting Expiring Items**

While updating statuses, we also track items that are close to expiry.

These items are stored separately:

```javascript
expiringItems.push(item);
```

This allows us to later notify users about upcoming expirations.

* * *

**Connecting Cron with Service**

Now we connect the cron scheduler with the status update logic.

Update:

```plaintext
src/modules/items/items.cron.js
```

```javascript
import cron from "node-cron";
import { updateItemStatuses } from "./items.service.js";

cron.schedule("* * * * *", async () => {
  console.log("Cron job started...");

  await updateItemStatuses();
});
```

* * *

**Final Cron Flow**

```javascript
Cron Trigger
   ↓
Fetch all items
   ↓
Calculate expiry difference
   ↓
Update statuses
   ↓
Bulk update MongoDB
   ↓
Collect expiring items
```

* * *

### Step 19: Email Notification System for Expiring Items

Now that item statuses update automatically using a cron job, the next step is to notify household members when items are about to expire.

* * *

**Problem**

Even if the database status updates correctly:

*   Users may never open the app regularly
    
*   Expiring items can still go unnoticed
    
*   Food waste can still happen
    

So we need a notification mechanism.

* * *

**Solution**

Whenever the cron job finds items that are:

```plaintext
expiring-soon
```

The system automatically sends email notifications to all household members.

This ensures everyone in the household is informed in advance.

* * *

**Setting Up Email Service (Nodemailer)**

To send emails, I used:

```plaintext
nodemailer
```

Checkout **nodemailer** documentation: [https://nodemailer.com/](https://nodemailer.com/)

* * *

**Email Configuration**

Create:

```plaintext
src/common/config/email.js
```

```javascript
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "smtp.gmail.com",

  // SMTP port for secure connection
  port: 465,

  secure: true,

  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

const sendMail = async (to, subject, html) => {
  await transporter.sendMail({
    from: process.env.SMTP_FROM_EMAIL,
    to,
    subject,
    html,
  });
};

export { sendMail };
```

* * *

**Environment Variables**

Update .env

```plaintext
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
SMTP_FROM_EMAIL=your_email@gmail.com
```

* * *

**Why App Password?**

For Gmail SMTP:

*   Normal Gmail password does not work
    
*   Google requires App Passwords for secure SMTP access
    

* * *

**What is App Password?**

It is a special 16-digit password generated by Google that allows apps like Nodemailer to send emails safely.

* * *

**How to get App password?**

**1\. Enable 2-Step Verification**

Go to: [https://myaccount.google.com/security](https://myaccount.google.com/security)

Turn ON:

*   **2-Step Verification**
    

**2\. Generate App Password**

After enabling 2FA:

Go to: [https://myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords)

You will get something like:

```plaintext
abcd efgh ijkl mnop
```

**3\. Use it in Nodemailer**

```plaintext
auth: {
  user: process.env.SMTP_USER, // your gmail address
  pass: process.env.SMTP_PASS, // app password (NOT normal password)
}
```

* * *

**Notification Flow**

The notification system works like this:

```javascript
Cron Job
   ↓
Update item statuses
   ↓
Collect expiring items
   ↓
Group items by household
   ↓
Fetch household members
   ↓
Send emails
```

* * *

**Grouping Expiring Items by Household**

Inside the cron job schedular code:

```javascript
const grouped = {};

for (const item of expiringItems) {
  const key = item.householdId.toString();

  if (!grouped[key]) {
    grouped[key] = [];
  }

  grouped[key].push(item);
}
```

**Why Group by Household?**

Because:

*   Multiple households exist
    
*   Each household has different members
    
*   Notifications should only go to relevant users
    

* * *

**Fetching Household Members**

Now we fetch all members of a household.

```javascript
const household = await houseHoldModel
  .findById(householdId)
  .populate("members", "email");
```

Using `populate()` gives access to member emails directly.

* * *

**Generating Email Content**

Next, we generate  
the email body dynamically.

```javascript
const itemsList = grouped[householdId]
  .map(
    (i) =>
      `<li>
        ${i.name} - expires on 
        ${new Date(i.expiryDate).toDateString()}
      </li>`
  )
  .join("");
```

* * *

**Final Email Template**

```html
const html = `
  <h2>Expiring Items Alert</h2>

  <p> The following items will expire soon: </p>

  <ul> ${itemsList} </ul>
`;
```

* * *

**Sending Emails**

Finally, emails are sent to all household members.

```javascript
for (const email of emails) {
  await sendMail(
    email,
    "Items Expiring Soon",
    html
  );
```

* * *

**Final Cron Implementation**

Update:

```plaintext
src/modules/items/items.cron.js
```

```javascript
import cron from "node-cron";

import { updateItemStatuses } from "./items.service.js";

import houseHoldModel from "../households/households.model.js";

import { sendMail } from "../../common/config/email.js";

cron.schedule("* * * * *", async () => {
  try {
    console.log("cronJob started...");

    const expiringItems =
      await updateItemStatuses();

    if (!expiringItems.length) {
      console.log("No expiring items");

      return;
    }

    // Group items by household
    const grouped = {};

    for (const item of expiringItems) {
      const key = item.householdId.toString();

      if (!grouped[key]) {
        grouped[key] = [];
      }

      grouped[key].push(item);
    }

    // Send notifications
    for (const householdId of Object.keys(grouped)) {
      const household = await houseHoldModel
        .findById(householdId)
        .populate("members", "email");

      const emails = household.members.map(
        (u) => u.email
      );

      const itemsList = grouped[householdId]
        .map(
          (i) =>
            `<li>
              ${i.name} - expires on 
              ${new Date(i.expiryDate).toDateString()}
            </li>`
        )
        .join("");

      const html = `
        <h2>Expiring Items Alert</h2>

        <p>
          The following items will expire soon:
        </p>

        <ul>
          ${itemsList}</ul>
      `;

      for (const email of emails) {
        await sendMail(
          email,
          "Items Expiring Soon",
          html
        );
      }
    }
  } catch (err) {
    console.error("Cron failed:", err);
  }
});
```

* * *

### Outcome

With this implementation:

*   Household members receive automated expiry alerts
    
*   Expiring items are tracked proactively
    
*   Users can consume items before they expire
    
*   Food wastage is reduced significantly
    

* * *

### Step 20: Get All Items & Update Item APIs

After implementing item creation and automated expiry tracking, the next step was building inventory management APIs for the frontend dashboard.

These APIs allow users to:

*   View all household inventory items
    
*   Edit existing items directly from the inventory table
    

This became important after integrating the inventory dashboard using Material React Table.

**Why These APIs Were Needed**

The frontend inventory system required:

*   Fetching all items belonging to the logged-in user’s household
    
*   Displaying items inside a data table
    
*   Supporting inline editing
    
*   Updating items without refreshing the entire page manually
    

This enabled a much smoother inventory management experience.

* * *

**Get All Items API**

**Requirements**

A logged-in user should be able to:

*   Fetch all items from their household
    
*   View:
    
    *   name
        
    *   category
        
    *   quantity
        
    *   expiry date
        
    *   status
        

Only authenticated household members should access these items.

* * *

**Define Routes**

Update:

`src/modules/items/items.routes.js`

```javascript
import { Router } from "express";

import authMiddleware from "../auth/auth.middleware.js";

import housholdMiddleware from "../households/households.middleware.js";

import * as controller from "./items.controller.js";

const router = Router();

router.post(
  "/",
  authMiddleware,
  housholdMiddleware,
  controller.createItem
);

router.get(
  "/",
  authMiddleware,
  housholdMiddleware,
  controller.getItems
);

router.put(
  "/:id",
  authMiddleware,
  housholdMiddleware,
  controller.updateItem
);

export default router;
```

* * *

**Controller Layer**

Update:

`src/modules/items/items.controller.js`

```javascript
import ApiResponse from "../../common/utils/api-response.js";

import * as itemService from "./items.service.js";

const getItems = async (req, res, next) => {
  try {
    const data = await itemService.getItems(req);

    ApiResponse.ok(
      res,
      "Items fetched successfully",
      data
    );
  } catch (error) {
    next(error);
  }
};

const updateItem = async (req, res, next) => {
  try {
    const data = await itemService.updateItem(req);

    ApiResponse.ok(
      res,
      "Item updated successfully",
      data
    );
  } catch (error) {
    next(error);
  }
};

export {
  getItems,
  updateItem,
};
```

* * *

**Get All Items Service**

Create / Update:

`src/modules/items/items.service.js`

```plaintext
const getItems = async (req) => {
  const householdId = req.householdId;

  const page = Number(req.query.page) || 1;
  const limit = Number(req.query.limit) || 10;
  const search = req.query.search || "";

  const skip = (page - 1) * limit;

  const query = {
    householdId,
  };

  if (search) {
    query.$or = [
      {
        name: {
          $regex: search,
          $options: "i",
        },
      },
      {
        category: {
          $regex: search,
          $options: "i",
        },
      },
    ];
  }

  const items = await itemsModel
    .find(query)
    .populate("addedBy", "name email")
    .sort({ createdAt: -1 })
    .skip(skip)
    .limit(limit);

  const totalItems = await itemsModel.countDocuments({
    householdId,
  });

  return {
    items,
    totalItems,
  };
};
```

* * *

**Why Filter Using householdId?**

This ensures:

*   Users only see items belonging to their household
    
*   Data remains isolated between households
    
*   Unauthorized inventory access is prevented
    

Without this filtering, users could potentially access items from other households.

* * *

**Update Item Service**

Now users can edit inventory items directly from the dashboard table.

Update:

`src/modules/items/items.service.js`

```plaintext
const updateItem = async (req) => {
  const itemId = req.params.id;

  const {
    name,
    category,
    quantity,
    expiryDate,
  } = req.body;

  const item = await itemsModel.findById(itemId);

  if (!item) {
    throw ApiError.notFound("Item not found");
  }

  item.name = name;
  item.category = category;
  item.quantity = quantity;
  item.expiryDate = expiryDate;

  const today = new Date();

  const expiry = new Date(expiryDate);

  today.setHours(0, 0, 0, 0);

  expiry.setHours(0, 0, 0, 0);

  const diffDays = Math.ceil(
    (expiry - today) /
      (1000 * 60 * 60 * 24)
  );

  let status = "fresh";

  if (diffDays < 0) {
    status = "expired";
  } else if (diffDays <= 3) {
    status = "expiring-soon";
  }

  item.status = status;

  await item.save();

  return item;
};
```

* * *

**Why Recalculate Status During Update?**

Suppose a user edits:

*   expiry date
    
*   quantity
    
*   item details
    

Then the status may also change.

Example:

*   Fresh → Expiring Soon
    
*   Expiring Soon → Expired
    

So status recalculation ensures item state always stays accurate.

* * *

### Step 21: Delete Item API

After implementing the Get All Items and Update Item APIs, the next important feature was allowing users to remove inventory items directly from the dashboard.

This became necessary after integrating row actions inside the Material React Table inventory system.

The delete functionality allows users to:

*   Remove expired items
    
*   Delete mistakenly added inventory
    
*   Keep household inventory clean and updated
    

* * *

**Why Delete API Was Needed**

Inside the inventory dashboard, each row already had:

*   Edit action
    
*   Status display
    
*   Inline editing support
    

The next logical feature was:

```plaintext
Delete Item
    ↓
Remove item from database
    ↓
Refresh inventory table
    ↓
Show updated inventory list
```

This created a much better inventory management experience for users.

* * *

**Defining Delete Route**

Update:

```plaintext
src/modules/items/items.routes.js
```

```plaintext
import { Router } from "express";

import authMiddleware from "../auth/auth.middleware.js";

import housholdMiddleware from "../households/households.middleware.js";

import * as controller from "./items.controller.js";

const router = Router();

router.post(
  "/",
  authMiddleware,
  housholdMiddleware,
  controller.createItem
);

router.get(
  "/",
  authMiddleware,
  housholdMiddleware,
  controller.getItems
);

router.put(
  "/:id",
  authMiddleware,
  housholdMiddleware,
  controller.updateItem
);

router.delete(
  "/:itemId",
  authMiddleware,
  housholdMiddleware,
  controller.deleteItem
);

export default router;
```

* * *

**Controller Layer**

Update:

```plaintext
src/modules/items/items.controller.js
```

```plaintext
import ApiResponse from "../../common/utils/api-response.js";

import * as itemService from "./items.service.js";

const deleteItem = async (req, res, next) => {
  try {
    const data = await itemService.deleteItem(req);

    ApiResponse.ok(
      res,
      "Item Deleted Successfully",
      data
    );
  } catch (err) {
    next(err);
  }
};

export {
  getItems,
  updateItem,
  deleteItem,
};
```

* * *

**Delete Item Service**

Update:

```plaintext
src/modules/items/items.service.js
```

```plaintext
const deleteItem = async (req) => {
  const { itemId } = req.params;

  const householdId = req.householdId;

  const item = await itemsModel.findOne({
    _id: itemId,
    householdId,
  });

  if (!item) {
    throw ApiError.notFound("Item not found");
  }

  await itemsModel.deleteOne({
    _id: itemId,
    householdId,
  });

  return {
    itemId,
  };
};
```

* * *
