Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ea03332
initial commit
Rahul-R79 Dec 1, 2025
dc88de7
basic file structure created
Rahul-R79 Dec 2, 2025
9be01f7
UI completed
Rahul-R79 Dec 4, 2025
7a9c3c3
feat: UI refactored
Rahul-R79 Jan 3, 2026
4654c0a
chore: set up database configuration
Rahul-R79 Jan 4, 2026
7d60a5b
feat: food model added
Rahul-R79 Jan 4, 2026
193b70c
feat: gemini service and edamam food service implemented
Rahul-R79 Jan 4, 2026
428a8ef
chore: packages installed for services and add config to index file
Rahul-R79 Jan 4, 2026
7bb296f
feat: image analyze controller implemented
Rahul-R79 Jan 4, 2026
3ea5c41
feat: multer middleware implemented
Rahul-R79 Jan 4, 2026
d54a89c
feat: image analyze router implemented
Rahul-R79 Jan 4, 2026
edbcf5c
chore: added route to index file
Rahul-R79 Jan 4, 2026
0e666b7
chore: remove Backend/.env from git and update gitignore
Rahul-R79 Jan 4, 2026
50e90e9
feat: changed gemini api and refactor edamam service
Rahul-R79 Jan 4, 2026
ec207ae
feat: refactor food nutrition model
Rahul-R79 Jan 4, 2026
2b9db54
chore: cors added
Rahul-R79 Jan 4, 2026
41ec47d
added favicon
Rahul-R79 Jan 4, 2026
cbcfe19
update axios config and remove unused dependencies
Rahul-R79 Jan 4, 2026
747f2c1
update analyze service and gitignore
Rahul-R79 Jan 4, 2026
5c4509c
update Tracker page implementation
Rahul-R79 Jan 4, 2026
002fc5a
Remove unused file `Diff for :`.
Rahul-R79 Jan 4, 2026
da0a46f
docs: README.md added
Rahul-R79 Jan 4, 2026
ec5a038
fix: add build and start scripts for backend deployment
Rahul-R79 Jan 4, 2026
3f96535
fix: correct API endpoint and formatting
Rahul-R79 Jan 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
dist
2,154 changes: 2,154 additions & 0 deletions Backend/package-lock.json

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions Backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "foodsnapai",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@google/generative-ai": "^0.24.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"axios": "^1.13.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"mongoose": "^9.1.1",
"multer": "^2.0.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.9.3"
}
}
14 changes: 14 additions & 0 deletions Backend/src/config/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoose from "mongoose";
import dotenv from 'dotenv';

dotenv.config();

export const connectDB = async(): Promise<void> => {
try{
await mongoose.connect(process.env.MONGO_URL || "");
console.log('mongoDB is connected');
}catch(err){
console.error('mongoConnection Failed');
process.exit(1);
}
}
55 changes: 55 additions & 0 deletions Backend/src/controllers/analyzeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Request, Response } from "express";
import crypto from "crypto";
import { analyzeWithGemini } from "../services/geminiService";
import { getNutritionData } from "../services/edamamService";
import Food from "../models/Food";

export const analyzeImage = async (
req: Request,
res: Response
): Promise<void> => {
try {
if (!req.file) {
res.status(400).json({ message: "No image uploaded" });
return;
}

const imageBuffer = req.file.buffer;
const mimeType = req.file.mimetype;

const imageHash = crypto
.createHash("sha256")
.update(imageBuffer)
.digest("hex");

const cachedFood = await Food.findOne({ imageHash });
if (cachedFood) {
res.json(cachedFood);
return;
}

const geminiResult = await analyzeWithGemini(imageBuffer, mimeType);

const nutritionData = await getNutritionData(
geminiResult.quantity,
geminiResult.foodName
);

const newFood = new Food({
imageHash,
foodName: geminiResult.foodName,
quantity: geminiResult.quantity,
nutrition: nutritionData,
});

await newFood.save();

res.json(newFood);
} catch (error) {
console.error("Analysis Error:", error);
res.status(500).json({
message: "Failed to analyze image",
error: (error as Error).message,
});
}
};
27 changes: 27 additions & 0 deletions Backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import { connectDB } from "./config/db";
import analyzeRoute from "./routes/analyzeRoute";

dotenv.config();
connectDB();

const app = express();
const port = process.env.PORT || 8080;

app.use(cors({
origin: process.env.VITE_URL,
credentials: true
}));

app.use(express.json());
app.get("/", (_, res) => {
res.send("Hello from FoodSnap AI Backend!");
});

app.use("/api/v1", analyzeRoute);

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
17 changes: 17 additions & 0 deletions Backend/src/middlewares/uploadMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import multer from 'multer';

const storage = multer.memoryStorage();

export const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024,
},
fileFilter: (_, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only images are allowed'));
}
},
});
44 changes: 44 additions & 0 deletions Backend/src/models/Food.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import mongoose, { Document, Schema } from "mongoose";

export interface IFood extends Document {
imageHash: string;
foodName: string;
quantity: string;
nutrition: {
Calories: { quantity: number; unit: string };
Protein: { quantity: number; unit: string };
Fat: { quantity: number; unit: string };
Carbohydrates: { quantity: number; unit: string };
};
createdAt: Date;
updatedAt: Date;
}

const FoodSchema = new Schema(
{
imageHash: {
type: String,
required: true,
unique: true,
trim: true,
},
foodName: {
type: String,
required: true,
trim: true,
},
quantity: {
type: String,
required: true,
},
nutrition: {
type: Object,
required: true,
},
},
{
timestamps: true,
}
);

export default mongoose.model<IFood>("Food", FoodSchema);
9 changes: 9 additions & 0 deletions Backend/src/routes/analyzeRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from 'express';
import { analyzeImage } from '../controllers/analyzeController';
import { upload } from '../middlewares/uploadMiddleware';

const router = express.Router();

router.post('/analyze', upload.single('image'), analyzeImage);

export default router;
69 changes: 69 additions & 0 deletions Backend/src/services/edamamService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import axios from 'axios';
import dotenv from 'dotenv';

dotenv.config();

const EDAMAM_APP_ID = process.env.EDAMAM_APP_ID;
const EDAMAM_APP_KEY = process.env.EDAMAM_APP_KEY;

export interface NutritionData {
Calories: { quantity: number; unit: string };
Protein: { quantity: number; unit: string };
Fat: { quantity: number; unit: string };
Carbohydrates: { quantity: number; unit: string };
}

export const getNutritionData = async (quantity: string, foodName: string): Promise<NutritionData> => {
try {
const query = `${quantity} ${foodName}`;

const response = await axios.get("https://api.edamam.com/api/nutrition-data", {
params: {
app_id: EDAMAM_APP_ID,
app_key: EDAMAM_APP_KEY,
ingr: query,
},
});

const data = response.data;

let finalNutrition: NutritionData = {
Calories: { quantity: 0, unit: 'kcal' },
Protein: { quantity: 0, unit: 'g' },
Fat: { quantity: 0, unit: 'g' },
Carbohydrates: { quantity: 0, unit: 'g' }
};

const hasTopLevelData = data.totalNutrients && Object.keys(data.totalNutrients).length > 0;

if (hasTopLevelData) {
if (data.totalNutrients.ENERC_KCAL) {
finalNutrition.Calories = { quantity: data.totalNutrients.ENERC_KCAL.quantity, unit: data.totalNutrients.ENERC_KCAL.unit };
}
if (data.totalNutrients.PROCNT) {
finalNutrition.Protein = { quantity: data.totalNutrients.PROCNT.quantity, unit: data.totalNutrients.PROCNT.unit };
}
if (data.totalNutrients.FAT) {
finalNutrition.Fat = { quantity: data.totalNutrients.FAT.quantity, unit: data.totalNutrients.FAT.unit };
}
if (data.totalNutrients.CHOCDF) {
finalNutrition.Carbohydrates = { quantity: data.totalNutrients.CHOCDF.quantity, unit: data.totalNutrients.CHOCDF.unit };
}

} else if (data.ingredients && data.ingredients.length > 0) {
const parsed = data.ingredients[0].parsed;
if (parsed && parsed.length > 0 && parsed[0].nutrients) {
const n = parsed[0].nutrients;

if (n.ENERC_KCAL) finalNutrition.Calories = { quantity: n.ENERC_KCAL.quantity, unit: n.ENERC_KCAL.unit };
if (n.PROCNT) finalNutrition.Protein = { quantity: n.PROCNT.quantity, unit: n.PROCNT.unit };
if (n.FAT) finalNutrition.Fat = { quantity: n.FAT.quantity, unit: n.FAT.unit };
if (n.CHOCDF) finalNutrition.Carbohydrates = { quantity: n.CHOCDF.quantity, unit: n.CHOCDF.unit };
}
}

return finalNutrition;
} catch (error) {
throw new Error('Failed to fetch nutrition data from Edamam');
}
};
44 changes: 44 additions & 0 deletions Backend/src/services/geminiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
import dotenv from "dotenv";

dotenv.config();

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });

interface GeminiResponse {
foodName: string;
quantity: string;
}

export const analyzeWithGemini = async (
imageBuffer: Buffer,
mimeType: string
): Promise<GeminiResponse> => {
try {
const prompt = `
Analyze this food image.
Identify the food item and estimate the portion size/quantity.
Return ONLY a valid JSON object in the following format, with no markdown code blocks:
{
"foodName": "name of food",
"quantity": "estimated quantity (e.g., 100gm, 1kg)"
}
`;

const imagePart = {
inlineData: {
data: imageBuffer.toString("base64"),
mimeType: mimeType,
},
};

const result = await model.generateContent([prompt, imagePart]);
const response = await result.response;
const text = response.text();
const jsonResponse: GeminiResponse = JSON.parse(text);
return jsonResponse;
} catch (error) {
throw new Error("Failed to analyze image with Gemini");
}
};
62 changes: 62 additions & 0 deletions Backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"esnext",
"dom"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist/" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": false /* Do not emit outputs. */,
// "importHelpers": true /* Import emit helpers from 'tslib'. */,
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"exclude": ["node_modules"]
}
Loading