MERN Stack App

Full Stack Calorie Counting App

Published 6th June

It's high time we built a more complex project. We have build all sorts of front and back-end applications but only small ones. We will now build a full-stack application. Our application will be a calorie-counting app, and we will be able to use it on multiple devices at the same time. We will not store our data inside of LocalStorage anymore, but this time we will use a database.

The tech-stack we will be using is the MERN stack.

So on the front-end we will have a React application and on the back-end we will build an Express service that will communicate with a MongoDB database instance.

Let's dig in! Open a terminal and generate a boiler plate react application:

      npx create-react-app front
    

Next, we will add the Material UI plugin for a cool and flexible layout.

      cd calorie-counting
      yarn add @mui/material @emotion/react @emotion/styled
    

Now, that we are set up, let's start modifying it to suit our purpose. First, delete the following files: package.lock.json from the root directory, the logo.svg, index.css and reportWebVitals.js from the 'src' directory. Next, inside index.js remove the import of reportWebVitals, the call of its function and the import of index.css. Also, get rid of the import of index.css from index.js. Finally, paste the below inside App.js

import "./App.css";

function App() {
  return (
    <div className="App">
      <h1>Calorie Tracker</h1>
    </div>
  );
}

export default App;
    

We will build our application incrementally from simple to more complex. For starters we won't even have any routes or fancy state management, it will be just a simple, dumb front-end that will communicate with our Express API. In future articles we will add more front-end stuff like routes and more complex state management for stuff such as the user authentication.

Now that we've got the basic setup ready, let's start working on some components. Paste the below in App.js:

import "./App.css";
import { MainCard } from "./components/MainCard";
function App() {
  return (
    <div className="App">
      <MainCard />
    </div>
  );
}

export default App;

    

Next, create a components directory (inside src) and a MainCard.js file inside of it. In that file, paste the below:

import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import { styled } from "@mui/material/styles";
import { BasicButton } from "./BasicButton";
import { Header } from "./Header";
import { CustomTextField } from "./CustomTextField";

const Item = styled(Paper)(() => ({
  textAlign: "center",
  backgroundColor: "#077ae6",
  color: "#fff",
  lineHeight: "60px",
  marginTop: "5rem",
}));
export const MainCard = () => {
  return (
    <Box>
      <Grid container spacing={1} justifyContent="center">
        <Grid item xs={12} md={7}>
          <Item>
            <Header />

            <CustomTextField id="mealItem" placeholder="Meal Item" />

            <CustomTextField
              id="mealQuantity"
              type="number"
              placeholder="Meal Quantity"
            />

            <CustomTextField
              id="mealCalories"
              placeholder="Meal Calories/100g"
              type="number"
            />

            <CustomTextField
              id="mealProtein"
              placeholder="Meal Protein/100g"
              type="number"
            />
            <BasicButton />
          </Item>
        </Grid>
      </Grid>
    </Box>
  );
};

    

This is a basic container component, that uses 3 other components. Let's create them now. Inside the components directory create a BasicButton.js file and paste the below inside:

import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
export const BasicButton = () => {
  return (
    <Grid>
      <Button variant="contained" color="secondary" size="large">
        Add Meal
      </Button>
    </Grid>
  );
};
    

Next, create a CustomTextField.js file and paste the below inside:

import { styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
const StyledTextField = styled(TextField)`
  .MuiInputBase-root {
    background-color: #fff;
  }
`;
export const CustomTextField = ({ id, placeholder, type = "text" }) => {
  return (
    <Grid>
      <StyledTextField
        id={id}
        variant="outlined"
        placeholder={placeholder}
        type={type}
      />
    </Grid>
  );
};

    

Finally, create a Header.js file and paste the below code inside:

import Typography from "@mui/material/Typography";
export const Header = () => {
  return (
    <Typography variant="h2" component="div">
      Calorie Tracker
    </Typography>
  );
};
    

We should now have a fully functioning interface with 4 fields and a button. if you don't recognize the styled()`` syntax, you should know it's from what we call styled components and you can read more about them here.

What we want next, is that the user inputs some values into the fields, and when they press the button, the values are computed and a list of 'meals' is displayed together with the calories and other nutrition data available.

In order to do that, we need to handle some state inside the MainCard component. Let's import the useState() hook inside the component:

      import { useState } from 'react';
    

Let's now manage some pieces of state inside the MainCard function, paste them before the return statement:

      const [mealItem, setMealItem] = useState('');
      const [mealQty, setMealQty] = useState(0);
      const [mealCals, setMealCals] = useState(0);
      const [mealProtein, setProtein] = useState(0);
    

Now we need some onChange handlers onto the CustomTextField components. In order to do so, we will pass another prop to the CustomTextField, that prop will be called changeHandler and it will be a function. Add it to the component like so:

import { styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
const StyledTextField = styled(TextField)`
  .MuiInputBase-root {
    background-color: #fff;
  }
`;
export const CustomTextField = ({
  id,
  placeholder,
  type = "text",
  changeHandler,
}) => {
  return (
    <Grid>
      <StyledTextField
        id={id}
        variant="outlined"
        placeholder={placeholder}
        type={type}
        onChange={changeHandler}
      />
    </Grid>
  );
};

    

Now we can pass the handlers to each of the CustomTextField, and the MainCard component will look like so:

import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import { styled } from "@mui/material/styles";
import { BasicButton } from "./BasicButton";
import { Header } from "./Header";
import { CustomTextField } from "./CustomTextField";
import { useState } from "react";
const Item = styled(Paper)(() => ({
  textAlign: "center",
  backgroundColor: "#077ae6",
  color: "#fff",
  lineHeight: "60px",
  marginTop: "5rem",
}));
export const MainCard = () => {
  const [mealItem, setMealItem] = useState('');
  const [mealQty, setMealQty] = useState(0);
  const [mealCals, setMealCals] = useState(0);
  const [mealProtein, setProtein] = useState(0);

  return (
    <Box>
      <Grid container spacing={1} justifyContent="center">
        <Grid item xs={12} md={7}>
          <Item>
            <Header />

            <CustomTextField
              id="mealItem"
              placeholder="Meal Item"
              changeHandler={(e) => setMealItem(e.target.value)}
            />
            <CustomTextField
              id="mealQuantity"
              type="number"
              placeholder="Meal Quantity"
              changeHandler={(e) => setMealQty(e.target.value)}
            />

            <CustomTextField
              id="mealCalories"
              placeholder="Meal Calories/100g"
              type="number"
              changeHandler={(e) => setMealCals(e.target.value)}
            />

            <CustomTextField
              id="mealProtein"
              placeholder="Meal Protein/100g"
              type="number"
              changeHandler={(e) => setProtein(e.target.value)}
            />
            <BasicButton />
          </Item>
        </Grid>
      </Grid>
    </Box>
  );
};

    

Next, we need to pass a prop to the BasicButton component too:

import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
export const BasicButton = ({ clickHandler }) => {
  return (
   <Grid>
     <Button
        onClick={clickHandler}
        variant="contained"
        color="secondary"
        size="large"
      >
        Add Meal
     </Button>
   </Grid>
  );
};

    

Finally, inside the MainCard, add the saveMeal method still above the return statement:

      const saveMeal = () => {
        if (!mealItem || !mealQty || !mealCals || !mealProtein) {
          console.log("boo! bad input! X__X");
        } else {
          console.log(mealItem, mealQty, mealCals, mealProtein);
        }
      };
    

*note that we have a primitive validation for now, we will improve that in future posts.

Pass it as a prop to the BasicButton :

      <BasicButton clickHandler={saveMeal} />
    

For now, we are good to go, let's create a MealItems component. Create the file and paste the below to it:

      import Grid from "@mui/material/Grid";
      import Typography from "@mui/material/Typography";
      
      const mealStyles = {
        border: "2px solid #fff",
        borderRadius: "15px",
        marginTop: "1rem",
        marginBottom: "1rem",
      };
      export const MealItems = ({ items = [] }) => {
        let totalCals = 0;
        let totalP = 0;
        items.map((i) => (totalCals += i.mealCals));
        items.map((i) => (totalP += i.mealProtein));

        if (items.length === 0) {
          return;
        } else {
          return (
            <Grid container spacing={1} justifyContent="center">
              <Grid item xs={12} md={7}>
                <Typography variant="h4" component="div">
                  Meal Items:
                </Typography>
              </Grid>
              {items.map((i) => {
                return (
                  <Grid style={mealStyles} item xs={12} md={7} key={Math.random()}>
                    <Typography variant="h5" component="div">
                      {i.mealItem}
                    </Typography>
                    <Typography variant="h6" component="div">
                      Qty: {i.mealQty} g
                    </Typography>
                    <Typography variant="h6" component="div">
                      Cal: {i.mealCals}
                    </Typography>
                    <Typography variant="h6" component="div">
                      Protein: {i.mealProtein}
                    </Typography>
                  </Grid>
                );
              })}
              <Grid item xs={12} md={7}>
                <Typography variant="h4" component="div">
                  Meals Total Calories: {totalCals.toFixed(2)}
                  <br />({totalP.toFixed(2)} g Protein)
                </Typography>
              </Grid>
            </Grid>
          );
        }
      };
    

Next, import the MealItems component inside the MainCard and render it below the BasicButton:

      <Item>
        <MealItems />
      </Item>
    

Surprise! You won't see a thing because we're not passing any items prop to it. Let's add 1 more piece of state in the MainCard:

  
  const [mealItems, setMealItems] = useState([]);
    

Next, pass it as prop to MealItems:

    <Item>
      <MealItems items={mealItems} />
    </Item>
    

Next, change a bit the saveMeal() method:

      const saveMeal = () => {
        if (!mealItem || !mealQty || !mealCals || !mealProtein) {
          console.log("boo! bad input! X__X");
        } else {
          const totalCalories = (mealQty * mealCals) / 100;
          const totalProtein = (mealQty * mealProtein) / 100;
          const newMealItem = {
            mealItem,
            mealQty,
            mealCals: totalCalories,
            mealProtein: totalProtein,
          };
          const newMeals = [...mealItems, newMealItem];
          setMealItems(newMeals);
          setMealItem('');
          setMealQty(0);
          setMealCals(0);
          setProtein(0);
        }
      };
    

Finally, we need to 'bind' the pieces of state defined by our CustomTextField components to the actual values in the fields. We can do that by adding a value prop to the CustomTextField and passing it from the MainCard. First the prop:

import { styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
const StyledTextField = styled(TextField)`
  .MuiInputBase-root {
    background-color: #fff;
  }
`;
export const CustomTextField = ({
  id,
  placeholder,
  type = "text",
  changeHandler,
  value,
}) => {
  return (
    <Grid>
      <StyledTextField
        id={id}
        variant="outlined"
        placeholder={placeholder}
        type={type}
        onChange={changeHandler}
        value={typeof value === "string" ? value : ""}
      />
    </Grid>
  );
};
    

Next, just pass the value prop:

      <Box>
        <Grid container spacing={1} justifyContent="center">
          <Grid item xs={12} md={7}>
            <Item>
              <Header />
  
              <CustomTextField
                id="mealItem"
                placeholder="Meal Item"
                changeHandler={(e) => setMealItem(e.target.value)}
                value={mealItem}
              />
              <CustomTextField
                id="mealQuantity"
                type="number"
                placeholder="Meal Quantity"
                changeHandler={(e) => setMealQty(e.target.value)}
                value={mealQty}
              />
  
              <CustomTextField
                id="mealCalories"
                placeholder="Meal Calories/100g"
                type="number"
                changeHandler={(e) => setMealCals(e.target.value)}
                value={mealCals}
              />
  
              <CustomTextField
                id="mealProtein"
                placeholder="Meal Protein/100g"
                type="number"
                changeHandler={(e) => setProtein(e.target.value)}
                value={mealProtein}
              />
              <BasicButton clickHandler={saveMeal} />
            </Item>
            <Item>
              <MealItems items={mealItems} />
            </Item>
          </Grid>
        </Grid>
      </Box>
    

Now everything works as expected. What we'll do as a last step (for now) is to generate an Express application and make sure we submit our form data to it as well.

In a separate directory, generate a new npm project:

      npm init --y
    

In the package.json file that was generated, paste the below:

      {
        "name": "back",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1",
          "start": "node server.js",
          "dev": "nodemon server.js"
        },
        "keywords": [],
        "author": "",
        "license": "ISC"
      }      
    

Next, in the same terminal run npm i express cors

Finally, run npm i -D nodemon --save

Next, create a server.js file and paste the below inside:

      const express = require("express");
      const cors = require("cors");
      
      const app = express();
      app.use(cors());
      app.use(express.json());
      
      app.get("/testRoute", (req, res) => {
        return res.end("Test route here...");
      });
      
      app.post("/meals", (req, res) => {
        console.log(req.body);
        return res.end(JSON.stringify({ msg: "post successfull" }));
      });
      
      app.use("*", (req, res) => {
        if (req.method === "POST") {
          res.statusCode = 405;
          return res.send("Method not allowed");
        } else {
          res.statusCode = 404;
          return res.send("Page not found..");
        }
      });
      
      app.listen(4000);
      

    

Now you can run npm run dev to start the server.

Finally, paste the below line inside the saveMeal() method under the setProtein(0) line:

      fetch("http://localhost:4000/meals", {
        method: "POST",
        headers: {
          Accept: "application/json, text/plain, */*",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(newMealItem),
      })
        .then((res) => res.json())
        .then((res) => console.log(res));
    

Finally, try adding a new meal item. You will see that it also submits it to our server (if you have it running). Now our app is starting to look more like a full-stack app. Note that the server is responding to the client and we log that response on the client and on the server, so everything works fine.

We are still missing the M part (mongoDB) from our MERN stack, but I think this is a good start for now.

We are also building this project pretty hastily, but worry not! We will improve upon it with every iteration.

You have the full code up until now in this github repo: repo Link