Create a Fitness Tracker App Backend: A Node.js Guide
github link:https://github.com/BRYOOH/Fitness-app
Login controller
This code attempts to validate a user’s login by checking for an existing email, matching the password, and generating a JWT token if authentication is successful.
const UserLogin= async (req,res) =>{
const { email,password } = req.body;
const user = await Users.findOne({email:email});
if(!user){
res.status(400).json({success:false,errors:"User email not found"})
}
const checkPassword = await Users.findOne({password:password});
if(!checkPassword){
res.status(400).json({success:false,errors:"User password not found"})
}
const token = jwt.sign({id:user._id},"secret_token");
return res.json({token,user:{id:user._id,email:user.email}});
};
const { email,password } = req.body;
This line uses destructuring to pull out email and password values from req.body. This assumes the client has sent the login credentials in the request body as JSON.This structure is typical in Express for handling JSON payloads from POST requests.
const token = jwt.sign({id:user._id},"secret_token");
This line creates a JSON Web Token (JWT) using jwt.sign, which is part of the jsonwebtoken library.The payload { id: user._id } includes the user’s unique ID (user._id) so the token can later identify the user when decoded."secret_token" is the secret key used to sign the token, which should be stored securely.
return res.json({ token, user: { id: user._id, email: user.email } });
};
This line sends a JSON response back to the client, containing the generated token and user information (id and email).
res.json sends a JSON response, allowing the client to access the token, which they can then store (often in local storage) and use for authentication on future requests.
getDashboard Controller
The getDashboard controller provides a complete overview of a user’s daily and weekly fitness data in one response, making it perfect for building interactive dashboards that keep users engaged. It is flexible enough to allow further customization, like adding monthly data or new categories.
This function is a great example of how backend logic and database capabilities can work together to create a smooth, data-rich experience for fitness apps or any application needing similar analytics. It delivers fitness insights directly to users, making it easier for them to understand and act on their health data.
const getDashboard = async(req,res) =>{
let userId = req.user.id;
let user = await Users.findById(userId);
console.log(user);
if(!user){
return res.status(400).json({success:false,errors:"User not found"});
}
const currentDataFormatted = new Date();
const startToday = new Date(
currentDataFormatted.getFullYear(),
currentDataFormatted.getMonth(),
currentDataFormatted.getDate()
);
const endDay = new Date(
currentDataFormatted.getFullYear(),
currentDataFormatted.getMonth(),
currentDataFormatted.getDate() + 1
);
//calculate caloriesburnt
const totalCaloriesBurnt = await Workouts.aggregate([
{$match:{user:user._id,date:{$gte:startToday,$lt:endDay}}},
{
$group:{
_id:null,
totalCaloriesBurnt: {$sum:"$caloriesBurnt"},
},
},
]);
//Calculate total no of workouts
const totalWorkouts = await Workouts.countDocuments({
user: userId,
date:{ $gte:startToday, $lt:endDay},
});
//Calculate avarage calories burnt per workout
const avgCaloriesBurntPerWorkout = totalCaloriesBurnt.length > 0 ?
totalCaloriesBurnt[0].totalCaloriesBurnt/totalWorkouts:0;
//Fetch category of workouts
const categoryCalories = await Workouts.aggregate([
{
$match:{ user:user._id, date:{ $gte:startToday,$lt:endDay }}
},
{
$group:{
_id: "$category",
totalCaloriesBurnt:{ $sum: "$caloriesBurnt"}
}
},
]);
//Format category data for pie chart
const pieChartData = categoryCalories.map((category,index)=>({
id: index,
value:category.totalCaloriesBurnt,
label:category._id,
}));
const weeks=[];
const caloriesBurnt=[];
for(let i=6;i>=0;i--){
const date= new Date(
currentDataFormatted.getTime() - i * 24 * 60 * 60 * 1000
);
weeks.push(`${date.getDate()}th`);
const startofToday = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
const endofDay = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() + 1
);
const weekData = await Workouts.aggregate([
{
$match:{
user:user._id,
date:{$gte:startofToday,$lt:endofDay},
},
},
{
$group:{
_id:{$dateToString:{format:"%Y-%m-%d", date:"$date"}},
totalCaloriesBurnt:{ $sum:"$caloriesBurnt"},
},
},
{
$sort:{ _id:1}, //Sort by date in ascending order
},
]);
caloriesBurnt.push(
weekData[0]?.totalCaloriesBurnt?weekData[0]?. totalCaloriesBurnt:0
);
}
return res.status(200).json({
totalCaloriesBurnt:
totalCaloriesBurnt.length > 0
? totalCaloriesBurnt[0].totalCaloriesBurnt
: 0,
totalWorkouts: totalWorkouts,
avgCaloriesBurntPerWorkout: avgCaloriesBurntPerWorkout,
totalWeeksCaloriesBurnt: {
weeks: weeks,
caloriesBurnt: caloriesBurnt,
},
pieChartData: pieChartData,
});
}
const currentDataFormatted = new Date();
const startToday = new Date(
currentDataFormatted.getFullYear(),
currentDataFormatted.getMonth(),
currentDataFormatted.getDate()
);
const endDay = new Date(
currentDataFormatted.getFullYear(),
currentDataFormatted.getMonth(),
currentDataFormatted.getDate() + 1
);
currentDataFormatted holds the current date and time.startToday is set to the start of the day (midnight) by initializing a new Date object using the current year, month, and day.endDay is set to the start of the following day by adding 1 to the current date, creating a 24-hour time window for today’s workouts.
const categoryCalories = await Workouts.aggregate([
// Stage 1: Match Stage
{
$match: {
user: user._id, // Match documents for specific user
date: {
$gte: startToday, // Date >= startToday
$lt: endDay // Date < endDay
}
}
},
// Stage 2: Group Stage
{
$group: {
_id: "$category", // Group by category field
totalCaloriesBurnt: {
$sum: "$caloriesBurnt" // Sum calories for each category
}
}
}
]);
The
aggregatefunction in MongoDB/Node.js is a powerful method that allows you to perform complex data processing operations on collections.
$matchStage:This is like a filter operation
It selects documents that match specific conditions:
user: user._id: Matches documents for a specific userdate: { $gte: startToday, $lt: endDay }: Matches documents within a date range$gte: Greater than or equal tostartToday$lt: Less thanendDay
$groupStage:Groups the filtered documents by a specific field
_id: "$category": Groups documents by their category fieldtotalCaloriesBurnt: { $sum: "$caloriesBurnt" }: Calculates the sum ofcaloriesBurntfor each category
const totalWorkouts = await Workouts.countDocuments({
user: userId, // Match documents for this specific user
date: { // Date range criteria
$gte: startToday, // Greater than or equal to startToday
$lt: endDay // Less than endDay
}
});
This line of code uses MongoDB's countDocuments method to count the number of workout documents that match specific criteria.More efficient than fetching all documents when you only need the count. Returns a single number (the count).
const weeks = [];
const caloriesBurnt = [];
for (let i = 6; i >= 0; i--) {
const date = new Date(
currentDataFormatted.getTime() - i * 24 * 60 * 60 * 1000
);
weeks.push(`${date.getDate()}th`);
currentDataFormatted.getTime(): Gets current timestamp in millisecondsi * 24 * 60 * 60 * 1000: Calculates milliseconds for i days- 24 (hours) * 60 (minutes) * 60 (seconds) * 1000 (milliseconds)
Subtracts this from current date to get past dates
weeks.push(`${date.getDate()}th`);Adds date string to weeks array.Format: "1th", "2th", "3th", etc.
const weekData = await Workouts.aggregate([
// Stage 1: Match documents for specific day
{
$match: {
user: user._id, // Match specific user
date: {
$gte: startofToday, // Greater than or equal to start of day
$lt: endofDay // Less than end of day
}
}
},
// Stage 2: Group and format data
{
$group: {
_id: {
$dateToString: { // Convert date to string format
format: "%Y-%m-%d", // Format: YYYY-MM-DD
date: "$date" // Field to format
}
},
totalCaloriesBurnt: {
$sum: "$caloriesBurnt" // Sum calories for the day
}
}
},
// Stage 3: Sort results
{
$sort: { _id: 1 } // Sort by date ascending
}
]);
Get workoutbydate
When building a fitness app, an important feature is allowing users to view their daily workout history, including metrics like total calories burned. The following code snippet provides an efficient way to retrieve workouts for a specific date using Node.js, Express, and MongoDB. This code handles querying workouts based on date and aggregates data to provide meaningful insights for the user.
const getWorkoutByDate = async(req,res) =>{
const userId =req.user?.id;
const user= await Users.findById(userId);
let date =req.query.date ? new Date(req.query.date):new Date();
console.log(date);
if(!user){
res.status(400).json({success:false,errors:"User not found!"})
}
const startToday = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
const endDay = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() + 1
);
const todayWorkouts = await Workouts.find({
user: userId,
date:{$gte:startToday,$lt:endDay},
});
const totalCaloriesBurnt= todayWorkouts.reduce(
(total,workout) => total + workout.caloriesBurnt,
0
);
return res.json({success:true,todayWorkouts,totalCaloriesBurnt});
};
let date = req.query.date ? new Date(req.query.date) : new Date();
console.log(date);
Here, the code checks if a date parameter is present in the query string (e.g., ?date=2024-11-05).If a date is provided, it converts this date string into a JavaScript Date object.If no date is specified, it defaults to the current date (new Date()).This line also logs the date object to the console for debugging purposes.
const totalCaloriesBurnt = todayWorkouts.reduce(
(total, workout) => total + workout.caloriesBurnt,
0
);
This line calculates the total calories burned for the day: It uses the reduce method to iterate over each workout in the todayWorkouts array. The reduce method accumulates the caloriesBurnt property from each workout and sums them up.The initial value for the accumulator (total) is set to 0.
addWorkout controller
The addWorkout function, along with helper functions parseWorkoutLine and calculateCaloriesBurnt, is designed to parse and save workout details to a database in a Node.js backend. This function accepts a structured workout string, extracts and categorizes workout details, and calculates calories burned for each workout entry.
const addWorkout = async(req,res)=>{
const userId = req.user?.id;
const {workoutString} = req.body;
if(!workoutString){
return res.status(400).json({success:false, errors:"Workout string missing"});
}
const eachworkout= workoutString.split(";").map((line)=>line.trim());
const categories = eachworkout.filter((line)=>line.startsWith("#"));
if(categories.length === 0){
return res.status(400).json({success:false,errors:"No categories found in workout string "})
}
const parseWorkouts=[];
let currentCategory = "";
let count=0;
//Loop through each line to parse workout details
await eachworkout.forEach((line)=>{
count++;
if(line.startsWith("#")){
const parts = line?.split("\n").map((part)=>part.trim());
console.log(parts);
if(parts.length <5){
return res.status(400).json({success:false, errors:Workout string is missing for ${count}th workout});
}
//update current category
currentCategory = parts[0].substring(1).trim();
//extract workout details
const workoutDetails = parseWorkoutLine(parts);
if(workoutDetails == null){
return res.status(400).json({success:false, errors:"Please enter in proper format"});
}
if(workoutDetails){
//add category to workout details
workoutDetails.category = currentCategory;
workoutDetails.caloriesBurnt = calculateCaloriesBurnt(workoutDetails);
parseWorkouts.push(workoutDetails);
}else{
return res.status(400).json({success:false,errors:Workout sttring is missing for ${count}th workout});
}
}
});
const createWorkout = await Promise.all(parseWorkouts.map( async (workout)=>{
workout.caloriesBurnt = parseFloat(calculateCaloriesBurnt(workout));
return Workouts.create({...workout,user:userId,date: Date.now()});
}));
return res.json({success:true,errors:"Workout added successfully", workouts:createWorkout});
};
const parseWorkoutLine = (parts) => {
const details = {};
console.log(parts);
if (parts.length >= 5) {
details.workoutName = parts[1].trim();
const setReps = parts[2].trim();
const [repStr , setStr] = setReps.split("X").map(s=>s.trim());
details.sets = parseInt(setStr.split("sets")[0].trim());
details.reps = parseInt(
repStr.split("reps")[0].trim()
);
details.weights = parseFloat(parts[3].split("kg")[0].trim());
details.duration = parseFloat(parts[4].split("min")[0].trim());
console.log(details);
return details;
}
return null;
};
const calculateCaloriesBurnt = (workoutDetails) =>{
const durationInMinutes = parseInt(workoutDetails.duration);
const weightInKg = parseInt(workoutDetails.weights);
const caloriesBurntPerMinute= 5;
return Math.round(durationInMinutes * caloriesBurntPerMinute * weightInKg);
};
const eachworkout = workoutString.split(";").map((line) => line.trim());
const categories = eachworkout.filter((line) => line.startsWith("#"));
In this code block, the eachworkout array is created by splitting and processing the workoutString (a string containing workout details). Here's a breakdown of what each part of this code does:
workoutString.split(";"): The split(";") function divides workoutString at every semicolon (;), creating an array of strings. Each element in this array represents one line or section of the workout details. This helps break down the workout information into separate parts for easier processing.
.map((line) => line.trim()):The trim() function removes any extra whitespace from the beginning and end of each line. This ensures each workout line is clean and doesn't have unnecessary spaces.
const categories = eachworkout.filter((line) => line.startsWith("#")): After processing, the code filters the eachworkout array to identify lines that start with the # character and asign them to categories. The filter() function iterates over each line, and line.startsWith("#") checks if a line starts with #.
const parseWorkoutLine = (parts) => {
const details = {};
console.log(parts);
if (parts.length >= 5) {
details.workoutName = parts[1].trim();
const setReps = parts[2].trim();
const [repStr , setStr] = setReps.split("X").map(s=>s.trim());
details.sets = parseInt(setStr.split("sets")[0].trim());
details.reps = parseInt(
repStr.split("reps")[0].trim()
);
details.weights = parseFloat(parts[3].split("kg")[0].trim());
details.duration = parseFloat(parts[4].split("min")[0].trim());
console.log(details);
return details;
}
return null;
};
const details = {};Initializes an empty object details that will hold the parsed workout information as key-value pairs.
details.workoutName = parts[1].trim();Extracts the workout name from the second element of parts[1]and removes any extra whitespace using trim(). This value is stored in the details.workoutName property.
const [repStr , setStr] = setReps.split("X").map(s=>s.trim()); Splits setReps using the character X (often used to denote "reps X sets" format, like "10 reps X 3 sets"). map(s => s.trim()) removes any extra whitespace from each part, resulting in two cleaned strings: repStr for reps and setStr for sets.
details.sets = parseInt(setStr.split("sets")[0].trim());Extracts the number of sets by splitting setStr with the word sets (assumes that setStr ends with "sets").Takes the first part of the split result (the number), trims it, and converts it to an integer using parseInt(), storing it in details.sets.
await eachworkout.forEach((line) => {
count++;
if (line.startsWith("#")) {
const parts = line?.split("\n").map((part) => part.trim());
console.log(parts);
The forEach loop iterates through each line in eachworkout, incrementing count with each iteration.If the line starts with #, it identifies a category line: parts splits the line by newline characters (\n) and trims each part. This will be used to separate workout details if they are provided on new lines.
currentCategory = parts[0].substring(1).trim();
currentCategory is updated by extracting the category name from the first segment substring(1) of parts, trimming any whitespace.
parts[0].substring(1): The substring(1) function extracts a portion of the string starting from index 1 to the end of the string. Since strings are zero-indexed, substring(1) omits the first character (at index 0), capturing the rest of the string. For example, if parts[0] is "#Cardio", substring(1) will produce "Cardio", effectively removing the # prefix.
const createWorkout = await Promise.all(parseWorkouts.map(async (workout) => {
workout.caloriesBurnt = parseFloat(calculateCaloriesBurnt(workout));
return Workouts.create({ ...workout, user: userId, date: Date.now() });
}));
This line maps over parseWorkouts to save each workout to the database: caloriesBurnt is recalculated and converted to a floating-point number. Workouts.create creates a new document with workout details, adding user and date fields. Promise.all waits for all workout creations to complete before proceeding.