Express JS – adding a route within a server to handle POST requests

I’m new to Javascript and I’m trying to learn express and create an application that will allow users to create new recipes, browse existing recipes, and view recipes.

I’ve got my server running by typing recipeserver.js in the cmd bar and then typing localhost:3000 in my address bar on google chrome. So far it loads the index.html homepage and from there, I am able to click on a link titled “Create a Recipe” which leads me to the create.html page that looks like this:

create.html page

Initially, there will be only three recipes on the server, which are included in the database object within the recipeserver.js code I’ve included below. The create.html page allows a user to enter recipe information. When the Save Recipe button is clicked, the addrecipe.js file is supposed to send the recipe data to the server using a POST request to the resource /recipes

Within the server code, all recipes will be stored in a single object called database. The keys of this object will be unique IDs and the values will be the recipes associated with those IDs. I’m stuck on a task where I’m supposed to add a route within the server code to handle POST requests to the /recipes resource. The handler for this route should:

  1. Extract the recipe object included in the POST request body
  2. Generate a unique ID for the new recipe (Etc. a basic integer that increases every time a recipe is added.)
  3. Add a new entry into the recipes object with the key being the unique ID and the value being the recipe object.

When testing my code by adding a few recipes to my server, I should be able to just log the contents of the recipes object to see that it is storing the correct data, like in the picture below (this picture isn’t mine):

load recipe in cmd

So as shown in the first picture of my screen, I filled in the contents of the recipe I want to add in create.html. When I click on the “Save Recipe” button however, instead of loading the contents of the recipe into my cmd window, I get the error:

TypeError: C:DownloadsrecipeApplicationviewsrecipes.pug:8
    6|     div#main
    7|         h1 List of Recipes:
  > 8|         each recipe in recipes
    9|             a(href="/recipes/" + recipe.id) #{recipe.name}
    10|             br
    11|

Cannot read property 'length' of undefined

I’m a little stumped on how to a route within the server code to handle POST requests to the /recipes resource. I made a function called loadRecipes() that I’m trying to do all this in. I attempted to create a new id and increment it by 1 like the task suggests. I’m having trouble extracting the recipe object included in the POST request body. I attempted this and ended up commenting it out as it created the same error. I’m just trying to get the Save Recipe button to work so that the recipe that is added prints its contents in the cmd bar like in the 2nd picture, but I’m really lost and overwhelmed with the amount of information that comes up when I try to search for a solution and would appreciate some help in getting this to work.

Here’s all my code incase anyone wants to run it but I believe my problem just lies in the recipeserver.js file. When the Save Recipe button is clicked, the addrecipe.js file sends the recipe data to the server using a POST request to the resource /recipes.

recipeserver.js:

const express = require('express');
const fs = require("fs");
const shortId = require("short-id");
const session = require('express-session');
const app = express();
const pug = require("pug");
const port = 3000;


let database = {
    "0":{
        "ingredients":
        [
            {"name":"Crab","unit":"Tsp","amount":3},
            {"name":"Peas","unit":"Cup","amount":12},
            {"name":"Basil","unit":"Tbsp","amount":10},
            {"name":"Cumin","unit":"Liter","amount":3},
            {"name":"Salt","unit":"Tbsp","amount":1}
        ],

        "name":"Boiled Crab with Peas",
        "preptime":"13",
        "cooktime":"78",
        "description":"A boring recipe using Crab and Peas",
        "id":"0"
    },
    "1":{
        "ingredients":
        [
            {"name":"Peanuts","unit":"Liter","amount":10},
            {"name":"Artichoke","unit":"Tsp","amount":3},
            {"name":"Basil","unit":"Cup","amount":11},
            {"name":"Sage","unit":"Grams","amount":13},
            {"name":"Pepper","unit":"Cup","amount":1}
        ],

        "name":"Boiled Peanuts with Artichoke",
        "preptime":"73",
        "cooktime":"74",
        "description":"A exciting recipe using Peanuts and Artichoke",
        "id":"1"
    },
    "2":{
        "ingredients":
        [
            {"name":"Lobster","unit":"Tsp","amount":14},
            {"name":"Brussel Sprouts","unit":"Liter","amount":14},
            {"name":"Sage","unit":"Tbsp","amount":3},
            {"name":"Thyme","unit":"Tbsp","amount":12},
            {"name":"Pepper","unit":"Tsp","amount":10},
            {"name":"Cumin","unit":"Tbsp","amount":11}
        ],

        "name":"Spicy Lobster with Brussel Sprouts",
        "preptime":"86",
        "cooktime":"19",
        "description":"A tasty recipe using Lobster and Brussel Sprouts",
        "id":"2"
    }
}


let recipes = {};

for (let recipe in database) {
  recipes[recipe.id] = recipe;
};

app.set("view engine", "pug");
app.use(express.static("public"));
app.use(express.json());
app.use(express.urlencoded({extended: true}));



app.get("/addrecipe.js", getAddRecipeJS);
app.get("/recipes", loadRecipes);
app.get("/recipe", loadRecipe);
app.route("/recipes", loadRecipes);
app.post("/recipes", loadRecipes);

let id = 1; 
function loadRecipes(request, response, next){
    response.status(200).render("recipes.pug", {"session": request.session});
    /*
    console.log("Request received!", request.body);
    const newId = shortId.generate();
    recipes[newId] = {
      id: newId,
      type: "recipe",
      ...request.body,
    };
    response.sendStatus(201);
    */
    id++;
}

function loadRecipe(req, res, next){
    res.status(200).render("recipe.pug", {"session": req.session});
}

function getAddRecipeJS(req, res, next){
    fs.readFile("addrecipe.js", function(err, data){
        if(err){
            res.statusCode = 500;
            res.end("Error reading file.");
            return;
        }
        res.status(200).send(data);
        return;
    });
}

app.listen(port);
console.log(`Server listening at http://localhost:${port}`);

index.html:

<html>

    <head><title>Recipe App Home Page</title></head>
    
    <body>
        <h1>Welcome to the Recipe App</h1>
        <br>
        <a href="/create.html">Create a Recipe</a><br>
        <a href="/recipes">Browse Recipes</a><br>       
    </body>
</html>

create.html:

<html>
    <head><title>Create a Recipe</title></head>
    
    <body>
        <script src="/js/addrecipe.js"></script>
        
        <button type="button" onclick="genRandom()">Generate Random Recipe Data</button>
        <button type="button" onclick="submit()">Save Recipe</button>
        <br><br>
        
        Recipe Name: <input type="textbox" id="recipename" size="50"><br>
        
        Prep Time: <input type="textbox" id="preptime" size="50"><br>
        
        Cook Time: <input type="textbox" id="cooktime" size="50"><br>
        
        Description: <textarea rows="5" cols="50" id="description"></textarea><br><br>
        
        Add ingredients:<br>
        Unit: <select id="unit">
        <option value="Tsp">Teaspoon</option>
        <option value="Tbsp">Tbsp</option>
        <option value="Cup">Cup</option>
        <option value="Liter">Liter</option>
        <option value="Gram">Gram</option>
        </select><br>
        
        Amount: <input type="textbox" id="amount"><br>
        
        Ingredient: <input type="textbox" id="ingredient"><br>
        
        <button type="button" id="add" onclick="addIngredient()">Add Ingredient</button>
        <br><br>
        <div id="ingredients">
        
        </div><br>
        <button type="button" id="submit" onclick="submit()">Save Recipe</button>
    </body>
</html>

addrecipe.js:

let descriptors = ["Sweet", "Spicy", "BBQ", "Braised", "Deconstructed", "Broiled", "Boiled", "Flambeed", "Raw", "Smoked", "Butterflied", "Cured", "Grilled", "Poached"];
let proteins = ["Chicken", "Beef", "Lobster", "Shrimp", "Crab", "Turkey", "Duck", "Tofu", "Chickpeas", "Lentils", "Peanuts", "Kangaroo", "Human", "Goose", "Fish", "Pork", "Eggs", "Deer"];
let accompany = ["Broccoli", "Carrots", "Peas", "Potato", "Kale", "Banana", "Artichoke", "Asparagus", "Beans", "Broccoli", "Brussel Sprouts", "Celery", "Melon", "Mushrooms", "Pumpkin"];
let spices = ["Salt", "Pepper", "Basil", "Thyme", "Sage", "Cumin"];
let mealDescriptors = ["tasty", "mediocre", "very good", "boring", "exciting", "delicious", "easy", "ridiculously complex"];
let units = ["Tbsp", "Tsp", "Cup", "Liter", "Grams"]

let recipe = {ingredients: []};

function addIngredient(){
    let name = document.getElementById("ingredient").value;
    let amount = document.getElementById("amount").value;
    let unit = document.getElementById("unit").value;
    let ingredient = {name, amount, unit};
    recipe.ingredients.push(ingredient);
    updateIngredients();
}

function updateIngredients(){
    let innerHTML = "";
    recipe.ingredients.forEach(ingredient => {
        innerHTML += ingredient.amount + " " + ingredient.unit + " " + ingredient.name + "<br>";
    });
    document.getElementById("ingredients").innerHTML = innerHTML;
}

function submit(){
    recipe.name = document.getElementById("recipename").value;
    recipe.preptime = document.getElementById("preptime").value;
    recipe.cooktime = document.getElementById("cooktime").value;
    recipe.description = document.getElementById("description").value;
    
    let req = new XMLHttpRequest();
    req.onreadystatechange = function() {
        if(this.readyState==4 && this.status==200){
            alert("recipe saved");
        }
    }
    
    //Send a POST request to the server containing the recipe data
    req.open("POST", `/recipes`);
    req.setRequestHeader("Content-Type", "application/json");
    req.send(JSON.stringify(recipe));
}

recipes.pug:

html
    head
        title Recipes
body
    a(href="/create.html") add a recipe
    div#main
        h1 List of Recipes:
        each recipe in recipes
            a(href="/recipes/" + recipe.id) #{recipe.name}
            br

recipe.pug:

html
    head
        title #{recipe.name}
body

    div#main
        h1 #{recipe.name}
        br

Answer

First of all, thanks for putting in effort in explaining your issue in detail. One suggestions, you can share the repo instead of snippets of code (since this is quite long, and structure of folder do affects how we can get it up running).

Nonetheless, the error you’re getting is due to recipes in recipes.pug is actually undefined.

index.js

function loadRecipes(request, response, next) {
      response
        .status(200)
        // Here, you only pass `session` object to the template engine
        // So template engine does not know about `recipes`
        // So `recipes` is undefined, and you can't loop it
        .render('recipes.pug', { session: request.session});
     
      id++;
    }

recipes.pug

html
    head
        title Recipes
body
    a(href="/create.html") add a recipe
    div#main
        h1 List of Recipes:
        // You're having issue here, since `recipes` is not passed to the template 
        // engine, it will throw an error
        each recipe in recipes
            a(href="/recipes/" + recipe.id) #{recipe.name}
            br

Update your index.js with this

function loadRecipes(request, response, next) {
      response
        .status(200)
        .render('recipes.pug', { session: request.session, recipes: database });
     
      id++;
    }

Now, you should be able to view the /recipes page and continue to work on the project. I note there are quite number of bug in your code.

app.get('/addrecipe.js', getAddRecipeJS);
app.get('/recipes', loadRecipes);
app.get('/recipe', loadRecipe);
app.route('/recipes', loadRecipes);
app.post('/recipes', loadRecipes);

Based on this list of route, you shouldn’t be using the same function to handle POST and GET request for /recipes.

GET can be used to retrieve the list of recipes POST should be used to handle the data submitted, and save it to the database variable inside your index.js

I will give you a simple way of doing this (You should really explore yourself too)

app.post('/recipes', saveRecipes);

function saveRecipes(req, res, next) {
  // Data submitted from your page is available in `req.body`
  console.log(req.body);
  // I'm trying to get a new `key` for the database
  // This is because you're using number as `key` for each recipe
  // Can skip this and use some random uuid as well
  const dbLength = Object.keys(database).length;
  // Add this to the database variable, and you're DONE!
  database.dbLength = req.body;
  res.send('ok');
}