Skip to main content

Command Palette

Search for a command to run...

Building a ShelfLife Household Inventory Tracker - Backend

Updated
38 min read
Building a ShelfLife Household Inventory Tracker - Backend
S

I'm Shubham (@shubhamsinghbundela), I'm a Software Engineer, a Full-stack developer, a tech enthusiast, and a technical writer here on @Hashnode. I have a strong zeal to share my acquired knowledge and I am also willing to learn from others.

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

npm init -y

It Creates package.json


Step 2: Install Dependencies

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

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:

PORT=3000
NODE_ENV=development
MONGODB_URI=your_mongodb_connection_string

Step 5: Server Entry Point (server.js)

create server.js

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:

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

Use:

npm run dev

Step 7: Setup Database Connection

Create:

src/common/config/db.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:

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 :

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:

src/common/utils/api-response.js
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:

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:

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
src/common/utils/api-error.js
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:

src/common/middleware/error.middleware.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
        });
    }

    // 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:

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

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:

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)

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:

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:

/api/auth/*

All auth APIs will now be available under:

/api/auth/*

Define Routes (auth.routes.js)

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)

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)

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


Request Flow

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

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

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:

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:

POST /api/auth/login

Controller Layer

Now we handle the request in the controller auth.controller.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:

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:

Set-Cookie: refreshToken=abc123

Browser stores it automatically.

Step 2: Browser Sends It Automatically

Next request:

Cookie: refreshToken=abc123

Now the server can identify the user.


Important Cookie Security Options

1. httpOnly

httpOnly: true

JavaScript cannot access the cookie.

Protects against XSS attacks.

2. secure

secure: true

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

3. sameSite

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:

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


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

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

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:

POST /api/auth/refresh

Controller Layer

Now we handle refresh requests inside:

src/modules/auth/auth.controller.js

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:

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

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:

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

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

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?

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

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:

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

Create HouseHold Schema

Create:

src/modules/households/households.model.js
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:

src/modules/auth/auth.middleware.js
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:

src/common/middleware/error-middleware.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:

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

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

Now all households APIs will be available under:

/api/households/* 

Define Routes

Create:

src/modules/households/households.routes.js
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:

src/modules/households/households.controller.js
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:

src/modules/households/households.service.js
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

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

Final Result: Testing api/households/ API Using Postman


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:

src/modules/households/households.routes.js
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:

src/modules/households/households.controller.js
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:

src/modules/households/households.service.js
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

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


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:

src/modules/households/households.model.js
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:

src/modules/households/households.routes.js
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:

src/modules/households/households.controller.js
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:

src/modules/households/households.service.js
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:

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

If we fetch data without populate:

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

This is not useful for the frontend.


Using Populate

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

This tells Mongoose:

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


Output After Populate

{
  "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

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


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:

src/modules/households/households.routes.js
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:

src/modules/households/households.controller.js
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:

src/modules/households/households.service.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

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


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:

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

Item Schema

Create:

src/modules/items/items.model.js
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:

src/modules/households/households.middleware.js
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:

src/modules/items/items.routes.js
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:

src/modules/items/items.controller.js
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:

src/modules/items/items.service.js
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

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


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:

node-cron

Cron Job Setup (Testing Mode)

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

Create:

src/modules/items/items.cron.js
import cron from "node-cron";

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

Understanding the Cron Expression

"* * * * *"

This means:

minute hour day month weekday

So the above expression runs:

  • Every minute

For production, this can later be changed to:

0 6 * * *

Which means:

  • Run every day at 6 AM

For more details checkout node-cron documentation: 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

src/modules/items/items.service.js
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:

await item.save();

We use:

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/ )


Collecting Expiring Items

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

These items are stored separately:

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:

src/modules/items/items.cron.js
import cron from "node-cron";
import { updateItemStatuses } from "./items.service.js";

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

  await updateItemStatuses();
});

Final Cron Flow

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:

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:

nodemailer

Checkout nodemailer documentation: https://nodemailer.com/


Email Configuration

Create:

src/common/config/email.js
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

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

Turn ON:

  • 2-Step Verification

2. Generate App Password

After enabling 2FA:

Go to: https://myaccount.google.com/apppasswords

You will get something like:

abcd efgh ijkl mnop

3. Use it in Nodemailer

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:

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:

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.

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.

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

Final Email Template

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.

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

Final Cron Implementation

Update:

src/modules/items/items.cron.js
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

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

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

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

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:

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:

src/modules/items/items.routes.js
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:

src/modules/items/items.controller.js
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:

src/modules/items/items.service.js
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,
  };
};