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:
- A main page with links to the search features and a form that allows the user to select a month from a menu
- A page that lists all the people in the
people
collection - A page that lists all the people who were born in a certain month, where the month is specified in the URL
- 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.
- A page that lists all the staff (the CS304 students who helped to build the WMDB)
You can try it out:
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:

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 usesawait
- 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:
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