People App

With this reading, we put all the pieces together to create a full-stack web database app:

  • MongoDB backend
  • Express middleware

To be very concrete, we'll be enhancing a basic Express web app so that some of the handler functions will search the database to get the data that is requested by the user via the web interface.

To do this, we'll just be combining JavaScript functions that search the WMDB collections in the MongoDB database, which you did in the Queries assignment with the Express handlers we did last week.

Functionality

The functionality just has a few features:

  1. A main page with links to the search features and a form that allows the user to select a month from a menu
  2. A page that lists all the people in the people collection
  3. A page that lists all the people who were born in a certain month, where the month is specified in the URL
  4. A page that responds to submitting the form on the main page by listing all the people who were born in the month specified by the menu in the form.
  5. A page that lists all the staff (the CS304 students who helped to build the WMDB)

You can try it out:

  1. main page
  2. all people
  3. people born in March
  4. WMDB Staff

You can copy the code to your own account:

cd ~/cs304/apps/
cp -rd ~cs304node/apps/people/ people/

Feel free to play with it. We'll do some modifications in class.

Now, let's look at the code, one handler at a time.

Main Page

The main page is not dynamic at all; there's no dynamic data on it. However, I decided to practice generating HTML in a loop by generating the menu that is in the page. The page looks like this:

screenshot of main page
Screenshot of main page of the People App

Notice the purple color of the header; that shows that we successfully loaded a CSS file.

The handler function looks like this:

// this list is also used to generate the menu, so element 0 is the default value

const monthNames = ['select month', 'January', 'February', 'March',
                    'April', 'May', 'June',
                    'July', 'August', 'September',
                    'October', 'November', 'December' ];


// main page. just has links to two other pages
app.get('/', (req, res) => {
    return res.render('index.ejs', {monthNames} );
});

So, all the HTML is buried in the index.ejs file. Here's that file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>People App</title>
    <!-- load local stylesheet (css) -->
    <link rel="stylesheet" href="/styles.css" />
    <!-- loading jquery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
    <h1>Welcome!</h1>
    <p>Everything is fine.</p>

    <h2>Links</h2>

    <p><a href="/people/">all people</a></p>
    
    <p><a href="/people-born-in/6">people born in June</a></p>

    <form method="GET" action="/people-born-in-month/">
        <p><label>month: <select required name="month">
                    <% monthNames.forEach( (mn, index) => { %>
                    <option value="<%= index %>"><%= mn %></option>
                    <% }) %>
        </select> </label></p>
        <p><input type="submit" value="get"></p>
    </form>

    <!-- load local js -->
    <script src="/main.js"></script>
</body>
</html>

Notice how the elements of the menu are generated. The code iterates over the list of month names that were passed in, generating an <option> for each. The value attribute of the option is the index in the list, so the back end will get a number rather than the name of the month. One advantage of using month numbers is internationalization: it's easier to handle other languages by translating the front end.

All People

Now let's turn to a slightly more complicated route: one that searches the database. Here's the handler for that route:

app.get('/people/', async (req, res) => {
    const db = await Connection.open(mongoUri, WMDB);
    const people = db.collection(PEOPLE);
    let all = await people.find({}).toArray();
    let descs = all.map(personDescription);
    let now = new Date();
    let nowStr = now.toLocaleString();
    return res.render('list.ejs',
                      {listDescription: `all people as of ${nowStr}`,
                       list: descs});
});

This is mostly straightforward, but a few things to notice:

  • our handler function now has the async keyword because it uses await
  • the argument to the Connection.open method is a global constant rather than a string. That way, if we mis-type it or misspell it, we get an error instead of a new, empty database.
  • the argument to the collection method is a global constant, PEOPLE, rather than a string for the same reason.
  • it uses a helper function, personDescription, to create a list of description objects. The function is below.
  • the response is a rendered template with a string listDescription and a list of

The helper function is very simple:

function personDescription(person) {
    let p = person;
    let bday = (new Date(p.birthdate)).toLocaleDateString();
    return `${p.name} (${p.nm}) born on ${bday}`;
}

It takes a person document and returns a string.

The EJS template is simple and straightforward:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>People App</title>
    <!-- load local stylesheet (css) -->
    <link rel="stylesheet" href="/styles.css" />
    <!-- loading jquery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
    <h1>List of <%= listDescription %></h1>

    <ol>
        <% list.forEach( (elt) => {%>
        <li><%= elt %></li>
        <% }); %>
    </ol>
    <!-- load local js -->
    <script src="/main.js"></script>
</body>
</html>

All People 2

You might wonder why we used a helper function, rather than doing things in EJS. We could have. Here's the alternative:

app.get('/people2/', async (req, res) => {
    const db = await Connection.open(mongoUri, WMDB);
    const people = db.collection(PEOPLE);
    let all = await people.find({}).toArray();
    let now = new Date();
    let nowStr = now.toLocaleString();
    return res.render('listPeople.ejs',
                      {listDescription: `all people (v2) as of ${nowStr}`,
                       list: all});
});

This handler passes the list of people documents into a different template. Here's that template:

<body>
    <h1>List of <%= listDescription %></h1>

    <% function personDescription(person) {
    let p = person;
    let bday = (new Date(p.birthdate)).toLocaleDateString();
    return `${p.name} (${p.nm}) born on ${bday}`;
}
%>

    <ol>
        <% list.forEach( (elt) => {  %>
        <li><%= personDescription(elt) %></li>
        <% }); %>
    </ol>
    <!-- load local js -->
    <script src="/main.js"></script>
</body>

We defined the helper function in here, and call it in the list.

So, this is just a engineering decision.

  • The first template is more generic and could be more easily reused.
  • The second requires a bit more work in the server.js file.

My personal preference is for the more generic list.ejs file, but I can respect either way of doing it.

Staff List

Another handler is for the /staffList route:

app.get('/staffList/', async (req, res) => {
    const db = await Connection.open(mongoUri, WMDB);
    const staff = db.collection(STAFF);
    let all = await staff.find({}).toArray();
    let names = all.map((doc) => doc.name);
    console.log('len', all.length, 'first', all[0]);
    return res.render('list.ejs',
                      {listDescription: 'all staff',
                       list: names});
});

This is nearly the same. Notice that we are able to re-use the list.ejs template just by extracting the name from each document.

Being able to re-use a template is nice, but it's not the most important thing. Here, the list.ejs template is very generic and therefore easy to re-use. A more complex app would probably not be able to do this, because each template will be specific to a certain kind of page.

People Born in a Month

Now let's turn to a handler that uses a parameterized endpoint to do a more focused search.

app.get('/people-born-in/:monthNumber', async (req, res) => {
    const monthNumber = req.params.monthNumber;
    // need to figure out flashing better. For now, just a console.log
    if( ! ( monthNumber && monthNumber >= 1 && monthNumber <= 12 )) {
        console.log("bad monthNumber", monthNumber);
        return res.send(`<em>error</em> ${monthNumber} is not valid`);
    }
    const db = await Connection.open(mongoUri, WMDB);
    const people = db.collection(PEOPLE);
    // not the most efficient approach; better to search in the database
    let all = await people.find({}).toArray();
    let chosen = peopleBornInMonth(all, monthNumber);
    let descriptions = chosen.map(personDescription);
    let now = new Date();
    let nowStr = now.toLocaleString();
    let num = descriptions.length;
    console.log('len', descriptions.length, 'first', descriptions[0]);
    return res.render('list.ejs',
                      {listDescription: `${num} people born in ${monthNames[monthNumber]}`,
                       list: descriptions});
});

This code is a bit more daunting, but we can understand it. Some observations:

  • The endpoint has a parameter, marked with a colon /people-born-in/:monthNumber/, so working URLs will look like /people-born-in/1 or /people-born-in/12.
  • The handler extracts the value of the parameter with req.params.monthNumber
  • If the month number is bogus, the handler returns a small error message. That error message is the whole response; there's nothing else. Later in the course, we'll learn a more sophisticated way of getting error messages to the user.
  • The next few lines are very similar to the earlier handler, except that we use a helper function to filter the list of people. As the comment says, it would be better to search in the database, but this is okay.
  • Notice the use of the array of month names to generate a nice description of the list.
  • Finally, it renders the same list.ejs template we used earlier.

The helper function is straightforward:

// This function filters a list of dictionaries for those with the correct targetMonth.
// The target month is 1-based, so January = 1, etc. 

function peopleBornInMonth(dictionaryList, targetMonth) {
    function isRightMonth(p) {
        let bd = new Date(p.birthdate);
        // have to remember to add 1 to getMonth() since it's zero-based
        return bd.getMonth()+1 == targetMonth;
    }
    return dictionaryList.filter(isRightMonth);
}

The filter method of arrays is similar to map but it copies only the elements for which the callback function returns true. Here, the callback function is isRightMonth which is locally defined.

Form Processing

Next, let's turn to a handler for the form. Recall that the form looks like this:

 <form method="GET" action="/people-born-in-month/">
        <p><label>month: <select required name="month">
                    <option value="0">select month</option>
                    <option value="1">January</option>
                    <option value="2">February</option>
                    <option value="3">March</option>
                    <option value="4">April</option>
                    <option value="5">May</option>
                    <option value="6">June</option>
                    <option value="7">July</option>
                    <option value="8">August</option>
                    <option value="9">September</option>
                    <option value="10">October</option>
                    <option value="11">November</option>
                    <option value="12">December</option>
        </select> </label></p>
        <p><input type="submit" value="get"></p>
    </form>

When that form gets submitted, the URL will look like:

/people-born-in-month/?month=6

In other words, the month number will be in the query string part of the URL. That's not a parameterized endpoint. Express parses the query string and puts the information in a dictionary in the request object:

req.query

So, our handler can easily access the form data to create the reply. In this case, we've already solved the problem of looking up people born in a particular month. So we have a couple of options:

  • copy/paste the code from the other handler. You know my feelings about copy/paste, so we'll discard this option.
  • create a helper function that both handlers call. In many circumstances, this would be an excellent approach.
  • redirect the browser to the other endpoint

In this case, we'll use re-directs, which is a new concept for us. Before getting to redirects, let's look at the handler:

// This gets data from the form submission and redirects to the one above
app.get('/people-born-in-month/', (req, res) => {
    let monthNumber = req.query.month;
    if( ! ( monthNumber && monthNumber >= 1 && monthNumber <= 12 )) {
        console.log("bad monthNumber", monthNumber);
        return res.send(`<em>error</em> ${monthNumber} is not valid`);
    }
    console.log('monthNumber', monthNumber, 'redirecting');
    res.redirect('/people-born-in/'+monthNumber);
});

This should be straightforward to understand, once we understand redirects.

Redirects

A redirect is a special kind of reply from the server. It tells the browser to make another, different request. This can be done for a variety of reasons, such as when you are reorganizing a website and want to redirect a user from an old URL to the new location for the information. Here, we want to say that the authoritative page is at the other URL. We want the user to bookmark and remember the /people-born-in/6 URL, not the /people-born-in-month/?month=6 url.

Here's a way to visualize this:

redirect tells the browser to make a different request
redirect tells the browser to make a different request

Redirects are not free. There's an extra network round-trip, which can delay the user and clog the network, so we don't use redirects just to save ourselves a little coding work. (We can use helper functions for that.) We use a redirect when there's a better URL that the user should be using. In this case, the better URL is the one with the nice, clean URL.

Conclusion

Our example is now complete. You're welcome to copy and look at the complete code from the folder in the course account: ~cs304node/apps/people/. We have a web application that can:

  • display data from the database to the user
  • take inputs from the user that the app uses as search criteria
  • handle form submissions
  • redirect the browser to a better URL