Web Development (COMP COMP1021)
<h1>Welcome to COMP1021!</h1>
About Your Professor
👋 Hi, I'm Graham! I'm a St. Lawrence College graduate, and I work in the industry as an Software Engineer. I have two cats and two dogs, and I really enjoy teaching!
I got drawn to programming as the logical thinking and problem-solving required really works well for my brain, and I find it incredibly satisfying to finally crack a sneaky bug.
About This Playbook
Web Programming (COMP1021) Playbook - "The Playbook", is a tool provided to you in order to assist with the course material. It is structured in approximately the same way the course is, and will be a valuable resource throughout the course.
The Playbook is a supplement only!
This Playbook is an evolving book, and it may update periodically with clarifications, so the content may change slightly. The ultimate source of truth for the course material is Blackboard, and this Playbook is provided as a supplement only. Please refer to Blackboard directly for important handouts such as the Course Outline, Learning Plan, and due dates.
Reference
This Playbook can be considered a reference guide for the course. We will refer to this Playbook regularly for informational content and explanations of the course material, including both during lectures and labs.
Exercises
Throughout this book, there will be exercises noted which will help solidify the content within the course. You are highly encouraged to complete these exercises, and they will often be part of the lecture with time provided to discuss them.
Code Blocks
This book will share code snippets in Code Blocks, which you've already seen at the top of this page. This allows the code to be read easily as it will have proper syntax highlighting, but also allows you to copy code snippets directly out of this book. To copy the code snippets, hover over the snippet and there is a Copy button at the top right.
<!-- Hover over this code block to see a copy button! -->
<h1>Welcome to COMP1021!</h1>
<p>This code is contained in a code block!</p>
Extra Resources
In this course we will use a combination of resources provided directly to you, as well as external resources such as MDN Web Docs. Please refer to the Extra Resources section of the playbook for an up-to-date list of resources.
COMP 205: Web Development
COMP 205 (Web Development) and COMP 1021 (Web Programming) are co-requisites. The two courses are designed to complement each other, and the material in each supports the other.
COMP 205 focuses on HTML, CSS, the visual structure of the web page, and front-end development. COMP 1021 focuses on server-side programming, and building dynamic web applications.
The COMP 205 Playbook is available at comp205.grahamcorcoran.ca. You may be referred to it throughout this course when topics overlap, particularly around HTML and CSS.
Module 1: Client & Server-side Scripting
This module covers JavaScript and the two main environments where it runs: the server (using Node.js) and the browser.
We start with the JavaScript language itself, covering variables, data types, operators, and functions. From there, we move into Node.js, JavaScript runtimes, and then into HTML, the DOM, and how JavaScript interacts with web pages in the browser.
Client-side and Server-side
In web development, client-side refers to code that runs in the user's browser. When someone visits a web page, the browser downloads the HTML, CSS, and JavaScript, and runs it locally on their machine. This is the client.
Server-side refers to code that runs on a remote machine (the server) before anything is sent to the browser. The server handles things like processing data, connecting to databases, and deciding what content to send back to the client.
JavaScript is one of the few languages used on both sides. In this module, we cover both.
By the end of this module, you should be comfortable writing JavaScript, running it in both Node.js and the browser, and understanding the distinction between the two environments.
Introduction to JavaScript
JavaScript is the programming language we will use throughout this course. If you are coming from a language like C++, many of the core concepts will feel familiar. Variables, operators, and data types all exist in JavaScript, but the syntax and behaviour differ in some important ways.
JavaScript does not require you to declare types when creating variables. It uses dynamic typing, meaning the type of a variable is determined by the value it holds, and can change during execution.
Variables
Variables in JavaScript are declared using one of three keywords: let, const, or var.
let
let is the standard way to declare a variable in JavaScript. It creates a variable that can be reassigned.
let score = 0;
score = 10;
Variables declared with let are block-scoped, meaning they only exist within the block (such as a function or an if statement) where they are declared.
const
const declares a variable that cannot be reassigned after it is set. Use const when you have a value that should not change.
const pi = 3.14159;
pi = 3; // This will cause an error
If a value should stay the same throughout your program, prefer const over let. It makes your code easier to read because anyone looking at it knows that value will not change.
const prevents reassignment of the variable, but it does not make the value itself immutable. If the value is an array or an object, you can still modify its contents.
const colours = ["red", "blue"];
colours.push("green"); // This works
colours = ["yellow"]; // This causes an error
The variable colours still points to the same array. You are changing what is inside the array, not replacing the array itself. This is why you will often see const used with arrays and objects in real-world code.
var
var is the original way to declare variables in JavaScript. You will see it in older code and in many online examples.
var name = "Fred";
In this course, we use let and const instead of var. The main reason is scoping: var is function-scoped rather than block-scoped, which can lead to unexpected behaviour. You do not need to understand the details of this difference right now, but if you see var in examples online, know that let is the modern replacement.
Data Types
JavaScript has several built-in data types. The most common ones you will work with are:
| Type | Example | Description |
|---|---|---|
| String | "Hello" | Text, wrapped in quotes |
| Number | 42, 3.14 | Integers and decimals (no separate types) |
| Boolean | true, false | Logical values |
| Null | null | An intentionally empty value |
| Undefined | undefined | A variable that has been declared but not assigned a value |
Unlike C++, JavaScript does not distinguish between integers and floating-point numbers. Both 42 and 3.14 are just Number.
You can check the type of a value using typeof:
let age = 25;
console.log(typeof age); // "number"
let name = "Jane";
console.log(typeof name); // "string"
Operators
Most operators in JavaScript work the same way as in C++. Arithmetic operators (+, -, *, /, %) and assignment operators (=, +=, -=) behave as you would expect.
The key difference is with comparison operators. JavaScript has two types of equality check:
| Operator | Name | Description |
|---|---|---|
== | Loose equality | Compares values after converting types |
=== | Strict equality | Compares values and types without conversion |
let a = 5;
let b = "5";
console.log(a == b); // true (string "5" is converted to number 5)
console.log(a === b); // false (number and string are different types)
Use === (strict equality) by default. Loose equality can produce unexpected results because of the type conversion. The same applies to !== (strict not equal) over != (loose not equal).
In C++, every variable has a type declared at compile time. If you compare an int to a string, the compiler will either reject it or you have to explicitly convert the type yourself. JavaScript is dynamically typed, so a variable can hold any type of value at any time. This means two values of completely different types can end up being compared, and JavaScript will try to convert them automatically with loose equality (==). Strict equality (===) avoids this by checking the type first and returning false immediately if the types do not match.
Console Output
console.log() is the primary way to output information in JavaScript. It prints to the terminal when running with Node, or to the browser's developer console when running in a browser.
console.log("Hello, world!");
You can output multiple values by separating them with commas:
let course = "COMP1021";
let year = 2026;
console.log("Welcome to", course, year);
// Welcome to COMP1021 2026
You can also join strings together using the + operator:
let name = "Graham";
console.log("Hello, " + name + "!");
// Hello, Graham!
The preferred way to include variables in strings is with template literals. Template literals use backticks (`) instead of quotes, and variables are inserted using ${}:
let name = "Graham";
let course = "COMP1021";
console.log(`Hello, ${name}! Welcome to ${course}.`);
// Hello, Graham! Welcome to COMP1021.
Template literals are easier to read than string concatenation, especially when you have multiple variables. Basic concatenation with + still works and you will see it in plenty of code, but template literals are the modern standard.
Comments
Comments in JavaScript work similarly to C++.
Single-line comments use //:
// This is a comment
let x = 10; // This is also a comment
Multi-line comments use /* */:
/*
This comment spans
multiple lines.
*/
Comments are ignored when the code runs. Use them to explain parts of your code that are not obvious from reading the code itself.
Functions in JavaScript
Functions are a way to group reusable code together. Instead of writing the same logic multiple times, you write it once inside a function and call it whenever you need it.
If you have written functions in C++, the concept is the same. The syntax is different, but the idea of defining a block of code that accepts input and produces output carries over directly.
Declaring Functions
A function in JavaScript is declared using the function keyword, followed by a name, parentheses, and a block of code inside curly braces.
function greet() {
console.log("Hello!");
}
To run the code inside the function, you call it by name with parentheses:
greet(); // prints "Hello!"
A function will not run until it is called. The declaration on its own does nothing.
Here is a more complete example with the components labeled:
// "add" is the function name
function add(num1, num2) { // num1 and num2 are parameters
// everything in here is the function body
return num1 + num2; // the return statement sends a value back
}
let result = add(3, 4); // 3 and 4 are arguments
console.log(result); // 7
Parameters and Arguments
Parameters are the variables listed in a function's declaration. Arguments are the actual values you pass in when calling the function.
function multiply(a, b) { // a and b are parameters
return a * b;
}
multiply(5, 3); // 5 and 3 are arguments
You can have as many parameters as you need, or none at all. If a function is called with fewer arguments than it has parameters, the missing ones will be undefined.
function introduce(name, age) {
console.log(`My name is ${name} and I am ${age} years old.`);
}
introduce("Priya"); // My name is Priya and I am undefined years old.
It's worth highlighting the above code snippet. Unlike most other languages, JavaScript will not stop you from calling a function with the wrong number of arguments. Missing parameters silently become undefined, which can lead to unexpected results like NaN (Not a Number) when used in calculations. If something isn't working and you can't figure out why, check that you're passing all the expected arguments.
Return Values
The return keyword sends a value back from a function to wherever it was called. You can store this value in a variable or use it directly.
function square(n) {
return n * n;
}
let result = square(6);
console.log(result); // 36
console.log(square(4)); // 16
A function can only return once. When a return statement is reached, the function stops executing immediately. Any code after the return will not run.
function check(value) {
if (value > 10) {
return "big";
}
return "small";
console.log("this will never run");
}
If a function does not have a return statement, it returns undefined by default.
Scope
Scope determines where a variable is accessible in your code. In the Introduction to JavaScript chapter, we mentioned that let and const are block-scoped. Functions create their own scope.
Variables declared inside a function are only accessible within that function:
function calculateTotal(price, tax) {
let total = price + (price * tax);
return total;
}
calculateTotal(50, 0.13);
console.log(total); // Error: total is not defined
The variable total exists inside calculateTotal and cannot be accessed outside of it.
Variables declared outside of a function are accessible inside it:
let taxRate = 0.13;
function calculateTotal(price) {
return price + (price * taxRate);
}
console.log(calculateTotal(50)); // 56.5
This works, but relying on variables from outside a function can make your code harder to follow. Where possible, pass values in as parameters so the function is self-contained.
Introduction to Node.js
Node.js is a tool that lets you run JavaScript outside of a web browser. Instead of needing an HTML page to execute your code, you can run JavaScript directly from your terminal.
Installing Node.js
To check if Node.js is installed, open a terminal and run:
node --version
If you see a version number, you're good to go. If not, follow the official installation guide at https://nodejs.org/. The LTS (Long Term Support) version is recommended.
Installing Node.js also installs npm, which is covered below.
Running JavaScript with Node
To run a JavaScript file with Node, use the node command followed by the file name:
node myFile.js
Node reads the file, executes the JavaScript inside it, and outputs any results to the terminal. If your code includes console.log(), the output will appear in the terminal where you ran the command.
let greeting = "Hello from Node";
console.log(greeting);
node myFile.js
Hello from Node
Unlike running JavaScript in a browser, there is no web page involved. Node executes the code and exits. This makes it useful for writing scripts, building tools, and running server-side applications.
npm
npm (Node Package Manager) is a command-line tool that comes bundled with Node.js. It is used to manage packages, which are third-party libraries written by other developers that you can use in your own projects.
Most real-world projects rely on external packages rather than building everything from scratch. For example, if your application needs to connect to a database or run a web server, there are well-tested packages available that handle this for you. npm is how you find, install, and keep track of those packages.
Initializing a Project
To start using npm in a project, you first initialize it:
npm init
This walks you through a series of prompts and creates a package.json file in your project folder. If you want to skip the prompts and accept the defaults, you can use:
npm init -y
The package.json file keeps track of your project's metadata and its dependencies. When your project uses external packages, they are recorded here so that anyone else working on the project knows what is required.
Installing Packages
To install a package, use npm install followed by the package name:
npm install express
This does two things:
- Downloads the package into a folder called
node_modulesin your project directory. - Adds the package to the
dependencieslist in yourpackage.json.
You can then use the package in your code with require(), which loads the package so you can use it in your file:
const express = require("express");
The require() function looks for the package by name inside your node_modules folder and returns it. You store the result in a variable (in this case express) and use that variable to access the package's functionality.
The node_modules folder can get large. You should not include it when submitting work or committing to git. Since your dependencies are listed in package.json, anyone can recreate the node_modules folder by running:
npm install
This reads package.json and downloads everything listed in it.
JavaScript and Runtimes
In this course we will be using JavaScript heavily, and while doing so we need to understand the difference between JavaScript the programming language, a JavaScript runtime, and the tools we use surrounding these concepts.
JavaScript
JavaScript is a programming language that was created to further expand the functionality of HTML and CSS in a web context. It was originally built to run inside web browsers, allowing developers to make web pages interactive.
JavaScript is a scripting language, meaning it is interpreted at the time it runs rather than compiled ahead of time like C++. You write the code, and a runtime reads and executes it directly.
An important distinction to keep in mind is that JavaScript is just the language. Where and how it runs depends on the runtime.
Runtimes
A JavaScript runtime is the process that executes the JavaScript code, and handles the result. Since JavaScript is interpreted, it always needs a runtime to execute. The runtime is responsible for reading your code, understanding it, and producing the output.
There are two main runtimes we will use in this course: Node.js and web browsers. Both run JavaScript, but they provide different capabilities depending on the environment they operate in.
Node
Node.js is a JavaScript runtime that runs outside of the browser. When you run node lab3.js in your terminal, Node is the program reading and executing your JavaScript code.
Under the hood, Node uses Google's V8 engine, which is the same JavaScript engine used in Google Chrome. V8 handles the actual interpretation and execution of your code. Node wraps V8 and adds functionality that would not be available in a browser, such as reading and writing files, accessing the operating system, and running a web server.
This is a key point: JavaScript in a browser is sandboxed. It can only interact with the web page it is running on. Node does not have this restriction. It runs on your machine with access to your file system, network, and other system resources. This is what makes it suitable for server-side development.
Node also comes with npm (Node Package Manager), which is a tool for installing and managing third-party libraries. When you need functionality that is not built into JavaScript or Node itself, npm allows you to pull in packages written by other developers. We will use npm regularly throughout this course.
Node takes JavaScript out of the browser and gives it the tools needed to build backend applications, scripts, and tooling.
Web Browsers
Web browsers (Chrome, Firefox, Edge, Safari) are the other major JavaScript runtime. When a browser loads a web page that contains JavaScript, the browser's built-in engine executes that code. Chrome uses V8 (the same engine as Node), Firefox uses SpiderMonkey, and Safari uses JavaScriptCore.
Where Node provides access to the file system and operating system, browsers provide access to the web page itself. JavaScript running in a browser can read and modify the page content, respond to user interactions like clicks and key presses, and make network requests to load additional data.
As mentioned earlier, browser JavaScript is sandboxed. It cannot access files on the user's computer or interact with the operating system directly. This is intentional. When you visit a website, the JavaScript on that page should not be able to read your documents or install software. The browser enforces these restrictions for security.
HTML DOM
The HTML DOM and HTML itself are regularly confused with each other but are not exactly the same. HTML is the markup you write in your .html files. The DOM is the live, in-memory structure the browser creates from that HTML. The browser can also modify the DOM after the page loads (and so can JavaScript), meaning the DOM may not always match the original HTML source.
The DOM (Document Object Model) is the browser's representation of an HTML page. When a browser loads an HTML file, it parses the HTML and builds a structured model of the page in memory. This model is the DOM.
JavaScript in the browser interacts with the page through the DOM. Every HTML element on the page becomes an object in the DOM that JavaScript can access, modify, or remove. For example, JavaScript can change the text inside a <p> tag, add a new <li> to a list, or hide an element when a button is clicked.
The DOM is not part of the JavaScript language. It is an API provided by the browser. This is why DOM-related code like document.getElementById() works in a browser but not in Node. Node does not have a web page, so it has no DOM.
The DOM & JavaScript in the Browser
Up to this point, we have been running JavaScript with Node in the terminal. In this chapter, we shift to running JavaScript in the browser, where it can interact with web pages directly.
HTML Recap
HTML (HyperText Markup Language) is the language used to structure web pages. A basic HTML page looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Page</title>
</head>
<body>
<h1>Hello</h1>
<p>This is a paragraph.</p>
</body>
</html>
HTML is covered in depth in COMP 205, which is the co-requisite for this course. If you need a refresher on HTML elements, attributes, or page structure, refer to the COMP 205 Playbook.
For this chapter, the key thing to understand is that HTML defines the structure of a page using elements, and those elements can be nested inside each other.
The DOM
When a browser loads an HTML file, it does not work with the raw text directly. Instead, it parses the HTML and builds a tree structure in memory called the DOM (Document Object Model).
Each HTML element becomes a node in this tree. Elements nested inside other elements become child nodes. For example, given this HTML:
<body>
<h1>Welcome</h1>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</body>
The browser builds a tree that looks roughly like this:
body
├── h1
│ └── "Welcome"
├── ul
│ ├── li
│ │ └── "Item 1"
│ └── li
│ └── "Item 2"
JavaScript in the browser interacts with this tree. When you change an element's text, add a new element, or remove one, you are modifying the DOM. The browser then updates the page to reflect those changes.
Adding JavaScript to a Page
To run JavaScript in a browser, you include it in your HTML file using the <script> tag.
Inline Scripts
It is possible to write JavaScript directly inside a <script> tag in your HTML:
<body>
<h1>Hello</h1>
<script>
console.log("This runs in the browser");
</script>
</body>
You may see this in online tutorials and examples. In this course, we do not use inline scripts. JavaScript should be kept in separate .js files. Inline scripts mix your logic into your HTML, which makes both harder to read and maintain.
External Scripts
Instead of inline scripts, the standard approach is to keep your JavaScript in a separate file and reference it:
<body>
<h1>Hello</h1>
<script src="app.js"></script>
</body>
// app.js
console.log("This runs in the browser");
Script Placement
Place your <script> tag at the bottom of the <body>, just before the closing </body> tag. This ensures the HTML elements on the page have been loaded before your JavaScript tries to access them. If your script runs before the elements exist, it will not be able to find them.
<body>
<h1>Hello</h1>
<p id="greeting">Welcome to the site.</p>
<!-- Scripts go here, after the content -->
<script src="app.js"></script>
</body>
You may see scripts placed in the <head> of a page with a defer attribute:
<head>
<script src="app.js" defer></script>
</head>
The defer attribute tells the browser to download the script immediately but wait to execute it until the HTML has finished loading. This achieves the same result as placing the script at the bottom of the body. Either approach is acceptable in this course.
Selecting Elements
Before you can do anything with an element on the page, you need to select it. JavaScript provides several methods for this through the document object, which represents the entire DOM. The most common is getElementById, which selects a single element by its id attribute:
<p id="message">Hello there.</p>
let element = document.getElementById("message");
console.log(element.textContent); // "Hello there."
Each id should be unique on the page, so this always returns one element. Other selection methods like querySelector and querySelectorAll exist for more flexible lookups using CSS selectors, but getElementById is sufficient for most of what we will do in this course.
Modifying Elements
Once you have selected an element, you can change its content. The textContent property gets or sets the text inside an element:
<p id="status">Waiting...</p>
let status = document.getElementById("status");
status.textContent = "Ready!";
The page will now display "Ready!" instead of "Waiting...". JavaScript can also modify an element's styles, attributes, and inner HTML, but for now textContent is the main one to be aware of.
Event Handling
Events are things that happen on the page: a user clicks a button, types in a field, hovers over an element, or submits a form. JavaScript lets you listen for these events and run code in response.
addEventListener
The standard way to handle events is with addEventListener. You call it on an element, pass in the event type and a function to run when the event occurs:
<button id="myButton">Click me</button>
let button = document.getElementById("myButton");
button.addEventListener("click", function() {
console.log("Button was clicked!");
});
When the button is clicked, the function runs. This function is called an event handler.
Common Events
| Event | Fires when... |
|---|---|
click | The element is clicked |
input | The value of an input field changes |
submit | A form is submitted |
keydown | A key is pressed |
mouseover | The cursor moves over the element |
Using Event Data
Event handlers receive an event object with information about what happened. You access it by adding a parameter to your handler function:
<input type="text" id="nameField" placeholder="Enter your name">
<p id="preview"></p>
let input = document.getElementById("nameField");
let preview = document.getElementById("preview");
input.addEventListener("input", function(event) {
preview.textContent = `You typed: ${event.target.value}`;
});
The event.target refers to the element that triggered the event. In this case, event.target.value gives you the current contents of the input field.
Putting It Together
Here is a complete example that combines selecting, modifying, and event handling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counter</title>
</head>
<body>
<h1>Counter</h1>
<p id="count">0</p>
<button id="increment">Add 1</button>
<script src="counter.js"></script>
</body>
</html>
// counter.js
let count = 0;
let display = document.getElementById("count");
let button = document.getElementById("increment");
button.addEventListener("click", function() {
count = count + 1;
display.textContent = count;
});
This page displays a number and a button. Each time the button is clicked, the number increases by one. The JavaScript selects the elements, listens for a click, updates the variable, and modifies the DOM to reflect the new value.
Module 2: Working with Databases
Most web applications need to store data somewhere. Whether it is user accounts or product listings, that data lives in a database. In this module, we cover how to work with databases using MongoDB, a NoSQL document database that stores data in a format that maps naturally to JavaScript objects.
We start with what NoSQL means and how it compares to relational databases, then move into MongoDB itself: installing it, working with the shell, performing CRUD operations, and connecting to it from Node.js.
SQL vs NoSQL
There are two broad categories of databases you will encounter: SQL (relational) and NoSQL (non-relational).
SQL databases like MySQL and PostgreSQL store data in tables with fixed columns and rows. You define a schema up front that determines exactly what fields each record must have. Data is queried using SQL (Structured Query Language), and relationships between tables are handled through joins.
NoSQL databases take a different approach. Instead of rigid tables and schemas, they use flexible structures like documents, key-value pairs, or graphs. MongoDB is a document database, meaning each record is stored as a document (a JSON-like object) inside a collection. Documents in the same collection do not need to have the same fields.
SQL databases enforce structure and are well-suited for data with clear relationships between tables. NoSQL databases are more flexible. If your data does not fit neatly into rows and columns, or if the shape of your data changes over time, a document database can be easier to work with.
Why MongoDB?
MongoDB pairs well with JavaScript and Node.js. Documents in MongoDB are stored in a format called BSON (Binary JSON), which maps directly to JavaScript objects. This means the data you read from the database looks like the objects you are already working with in your code.
MongoDB is also widely used in industry and has a mature Node.js driver, making it a practical choice for learning how databases fit into web applications.
What We Will Cover
By the end of this module, you should be able to design a simple document schema, perform CRUD operations (Create, Read, Update, Delete), and connect to MongoDB from a Node.js application.
Introduction to NoSQL
NoSQL stands for "Not Only SQL". It refers to a broad category of databases that do not use the traditional table-based structure of relational databases. There are several types of NoSQL databases (document, key-value, graph, column-family), but in this course we focus on document databases.
What is NoSQL?
In a relational (SQL) database, data is organized into tables. Each table has a fixed set of columns, and each row represents one record. If you want to store student information, you define a table with columns like name, program, and year, and every student record must follow that structure.
| name | program | year |
|----------|---------|------|
| Amara | COMP | 1 |
| Jin | BUSN | 2 |
| Fatima | COMP | 3 |
In a NoSQL document database, there are no tables or fixed columns. Instead, each record is a document, which is a self-contained object that holds its own fields and values. Documents are grouped into collections, which are roughly equivalent to tables.
{ name: "Amara", program: "COMP", year: 1 }
{ name: "Jin", program: "BUSN", year: 2, gpa: 3.8 }
{ name: "Fatima", program: "COMP", year: 3 }
Notice that the second document has a gpa field that the others do not. This is valid. Documents in the same collection do not need to have identical fields.
Document Databases
A document database stores data as individual documents. Each document is a structured object containing field-value pairs. You can think of a document as a single JavaScript object.
MongoDB is the document database we use in this course. It organizes data into three levels: a database contains collections, and each collection contains documents. A single MongoDB server can host multiple databases.
For example, a school database might have a students collection and a courses collection. Each student would be one document in the students collection.
JSON and BSON
MongoDB documents look like JSON (JavaScript Object Notation), the data format you have likely seen when working with JavaScript objects:
{
"name": "Tariq",
"program": "COMP",
"year": 2,
"courses": ["COMP1021", "COMP205"]
}
Internally, MongoDB stores documents in BSON (Binary JSON). BSON is a binary representation of JSON that supports additional data types like dates, ObjectIds, and binary data. You do not need to work with BSON directly. When you read and write documents using the MongoDB driver or shell, you work with regular JavaScript objects. MongoDB handles the conversion to and from BSON behind the scenes.
One BSON type worth knowing about is ObjectId. Every document in MongoDB has a unique _id field. If you do not provide one when inserting a document, MongoDB generates an ObjectId automatically:
{
_id: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
name: "Tariq",
program: "COMP"
}
The _id field acts as the primary key for each document.
When to Use NoSQL vs SQL
The choice depends on what you are building.
SQL databases work well when your data has a consistent structure and you need to enforce relationships between tables, like orders that reference customers. If data integrity and strict validation matter, SQL gives you those guarantees.
NoSQL document databases are a better fit when the shape of your data varies between records, or when you are working with nested data like a blog post with embedded comments. They also let you iterate without defining a rigid schema up front.
Many applications use both. For this course, we focus on MongoDB to learn how document databases work and how they integrate with Node.js.
Introduction to MongoDB
MongoDB is the database we will use for the rest of this course. This chapter covers getting it running, understanding its core terminology, and using the Mongo shell to interact with a database directly.
Installing and Running MongoDB
There are two ways to run MongoDB: locally on your machine, or through a cloud service called MongoDB Atlas. In this course we run it locally.
Installing MongoDB Community Edition
Follow the official installation guide for your operating system at https://www.mongodb.com/docs/manual/installation/. Install the Community Edition.
The installation includes:
mongod: the MongoDB server process (the database itself)mongosh: the MongoDB shell (a command-line tool for interacting with the database)
Starting the Server
On most systems, MongoDB runs as a background service after installation. You can verify it is running by opening a terminal and typing:
mongosh
If it connects successfully, MongoDB is running. If you get a connection error, you may need to start the server manually:
mongod
Keep this terminal open while you work. The server needs to be running for anything to connect to it.
Connection String
By default, MongoDB listens on localhost at port 27017. The connection string for a local instance is:
mongodb://localhost:27017
You will use this string when connecting from the shell or from Node.js.
Core Terminology
MongoDB uses different terminology than relational databases, but the concepts map closely:
| Relational (SQL) | MongoDB |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
A database holds one or more collections. A collection holds documents. A document is a single record made up of fields and values, stored as a JSON-like object.
MongoDB creates databases and collections automatically when you first write data to them. You do not need to create them in advance.
The Mongo Shell
mongosh is an interactive command-line tool for working with MongoDB. It lets you run queries, insert data, and manage databases without writing a full program.
Selecting a Database
To switch to a database (or create one by using it for the first time):
use myDatabase
Viewing Databases and Collections
show dbs
show collections
show dbs only lists databases that contain data. An empty database will not appear.
Inserting Documents
To add a document to a collection, use insertOne:
db.students.insertOne({
name: "Linh",
program: "COMP",
year: 1
})
If the students collection does not exist yet, MongoDB creates it automatically. The result includes the generated _id for the new document.
To insert multiple documents at once:
db.students.insertMany([
{ name: "Ravi", program: "BUSN", year: 2 },
{ name: "Sophie", program: "COMP", year: 1 },
{ name: "Marcus", program: "COMP", year: 3 }
])
Querying Documents
To retrieve documents from a collection, use find or findOne.
findOne returns a single document matching the query:
db.students.findOne({ name: "Linh" })
find returns all matching documents:
db.students.find({ program: "COMP" })
Calling find with no arguments returns every document in the collection:
db.students.find()
These are the basics for getting data in and out of MongoDB from the shell. The following chapters cover CRUD operations in more detail, and then how to do all of this from Node.js.
CRUD Operations
CRUD stands for Create, Read, Update, Delete. These are the four fundamental operations for working with data in any database. MongoDB provides methods for each one, and they all follow a similar pattern: you call a method on a collection and pass in a document or query.
The examples in this chapter use the Mongo shell (mongosh), but the method names and syntax are the same when using the Node.js driver.
Create
insertOne
insertOne adds a single document to a collection:
db.employees.insertOne({
name: "Kenji",
department: "Engineering",
salary: 72000
})
The result object includes an insertedId field containing the _id that MongoDB assigned to the new document.
insertMany
insertMany adds multiple documents at once. Pass in an array of documents:
db.employees.insertMany([
{ name: "Dana", department: "Marketing", salary: 65000 },
{ name: "Olga", department: "Engineering", salary: 78000 },
{ name: "Carlos", department: "Marketing", salary: 61000 }
])
The result includes an insertedIds object mapping the index of each document to its generated _id.
Read
findOne
findOne returns the first document that matches a query:
db.employees.findOne({ name: "Kenji" })
If no document matches, findOne returns null.
find
find returns all documents that match a query:
db.employees.find({ department: "Engineering" })
With no arguments, find returns every document in the collection:
db.employees.find()
Query Filters
The object you pass to find or findOne is a query filter. It specifies which documents to match. A filter with multiple fields requires all of them to match:
db.employees.find({ department: "Engineering", salary: 72000 })
This returns only documents where the department is "Engineering" and the salary is 72000.
MongoDB also supports comparison operators for more flexible queries. These are covered in the Data Modelling and Queries chapter.
Update
updateOne
updateOne takes two arguments: a filter (which document to find) and an update (what to change):
db.employees.updateOne(
{ name: "Kenji" },
{ $set: { salary: 75000 } }
)
The result includes matchedCount (how many documents matched the filter) and modifiedCount (how many were actually changed).
updateMany
updateMany modifies all documents that match a filter:
db.employees.updateMany(
{ department: "Marketing" },
{ $set: { department: "Growth" } }
)
This renames the department from "Marketing" to "Growth" for every matching employee.
Update Operators
The $set operator replaces the value of a field, or adds the field if it does not exist. MongoDB has several other update operators:
| Operator | Description |
|---|---|
$set | Sets the value of a field |
$unset | Removes a field from the document |
$inc | Increments a numeric field by a specified amount |
$push | Adds an element to an array field |
$pull | Removes an element from an array field |
// Give Kenji a raise of 5000
db.employees.updateOne(
{ name: "Kenji" },
{ $inc: { salary: 5000 } }
)
// Add a skill to an employee's skills array
db.employees.updateOne(
{ name: "Olga" },
{ $push: { skills: "Python" } }
)
When updating, always use an operator like $set. If you pass a plain object without an operator, MongoDB will replace the entire document with that object, removing all other fields. This is almost never what you want.
Delete
deleteOne
deleteOne removes the first document that matches a filter:
db.employees.deleteOne({ name: "Carlos" })
The result includes deletedCount, which tells you how many documents were removed (either 0 or 1).
deleteMany
deleteMany removes all documents that match a filter:
db.employees.deleteMany({ department: "Growth" })
To delete every document in a collection, pass an empty filter:
db.employees.deleteMany({})
deleteMany({}) removes every document in the collection. There is no undo. Always double-check your filter before running a delete operation.
MongoDB with Node.js
So far we have been working with MongoDB through the shell. In a real application, your code needs to connect to the database, run operations, and handle the results programmatically. The MongoDB Node.js driver lets you do this from JavaScript.
The MongoDB Node.js Driver
The official MongoDB driver is an npm package called mongodb. To use it in a project, initialize a project with npm and install the package:
npm init -y
npm install mongodb
This adds the mongodb package to your node_modules folder and records it in your package.json.
Async and Await
Before we connect to MongoDB, we need to cover a JavaScript concept that has not come up yet in this course: asynchronous code.
Most of the code you have written so far runs line by line, top to bottom. Each line finishes before the next one starts. But some operations take time. Connecting to a database, reading a file, or making a network request all involve waiting for something external to respond. JavaScript does not stop and wait by default. If you call a function that takes time, JavaScript moves on to the next line immediately, even if the result is not ready yet.
async and await give you a way to handle this. When you put await in front of a function call, JavaScript pauses that function until the operation finishes and gives you the result. Without await, you would get back a Promise object (a placeholder for a future value) instead of the actual data.
// Without await: result is a Promise, not the actual document
const result = collection.findOne({ name: "Anika" });
console.log(result); // Promise { <pending> }
// With await: JavaScript waits for the query to finish
const result = await collection.findOne({ name: "Anika" });
console.log(result); // { _id: ..., name: "Anika", ... }
There is one rule: await can only be used inside a function marked with async. This is how JavaScript knows the function contains asynchronous code.
async function main() {
const result = await collection.findOne({ name: "Anika" });
console.log(result);
}
main();
If you try to use await outside an async function, you will get a syntax error.
You saw this pattern in Lab 5. The entire lab was wrapped in an async function main() with await on every database call. That is the standard structure for scripts that talk to a database, and we will keep using it throughout this module.
Connecting to a Database
To connect to MongoDB from Node.js, you create a MongoClient and call connect():
const { MongoClient } = require("mongodb");
async function main() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);
await client.connect();
const db = client.db("myDatabase");
const collection = db.collection("myCollection");
// ... do work here ...
await client.close();
}
main();
MongoClient is imported from the mongodb package using destructuring. The uri is the connection string for your local MongoDB instance. After connecting, you select a database with client.db() and a collection with db.collection(). Both are created automatically if they do not exist yet.
Running CRUD Operations
The CRUD methods you used in the shell work the same way through the driver. The only difference is that each method returns a Promise, so you need await.
// Insert
const result = await collection.insertOne({
name: "Yuki",
role: "developer",
experience: 3
});
console.log("Inserted:", result.insertedId);
// Find
const person = await collection.findOne({ name: "Yuki" });
console.log(person);
// Update
await collection.updateOne(
{ name: "Yuki" },
{ $set: { experience: 4 } }
);
// Delete
await collection.deleteOne({ name: "Yuki" });
The method names, filters, and update operators are all identical to what you have been using in mongosh.
In the shell, find() prints results directly. In Node.js, find() returns a cursor, which is a pointer to the result set. To get the documents as an array, call .toArray() on the cursor:
const developers = await collection.find({ role: "developer" }).toArray();
Error Handling
Database operations can fail. The server might not be running, a query might be malformed, or the connection might drop. Wrap your database code in a try/catch/finally block to handle these cases:
const { MongoClient } = require("mongodb");
async function main() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);
try {
await client.connect();
const db = client.db("shop");
const products = db.collection("products");
await products.insertOne({
name: "Notebook",
price: 4.99,
inStock: true
});
const item = await products.findOne({ name: "Notebook" });
console.log(item);
} catch (err) {
console.log("Error:", err);
} finally {
await client.close();
}
}
main();
The try block contains your database operations. If anything goes wrong (connection failure, invalid query, server timeout), execution jumps to the catch block. The finally block runs regardless of success or failure, making it the right place to close the connection. If you forget to close connections, your application may run out of available connections or hang when exiting.
Data Modelling and Queries
Up to this point, we have worked with simple documents and basic filters. Real applications have more complex data and need more flexible ways to query it. This chapter covers how to design your document structure and how to write queries that go beyond exact matches.
Designing Document Schemas
MongoDB does not enforce a schema, but that does not mean you should store data without any structure in mind. How you organize your documents affects how easy they are to query and how well your application performs.
The main decision when designing a schema is whether to embed related data inside a document or reference it from a separate collection.
Embedding vs Referencing
Embedding means storing related data directly inside a document:
{
name: "Nadia",
email: "nadia@example.com",
address: {
street: "42 Oak Avenue",
city: "Toronto",
province: "ON"
}
}
The address is part of the person document. You get everything in a single read.
Referencing means storing related data in a separate collection and linking to it by _id:
// In the "orders" collection
{
item: "Laptop",
quantity: 1,
customerId: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1")
}
// In the "customers" collection
{
_id: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
name: "Kofi",
email: "kofi@example.com"
}
The order does not contain the customer's details. It stores a reference to the customer document.
Embedding works well when the related data belongs to the parent and you almost always need it together. An address on a person document is a good example. You would rarely look up an address without also wanting the person.
Referencing makes more sense when the related data is shared. A product might be referenced by hundreds of orders, and you would not want a copy of the full product details embedded in every order document. Referencing is also better when the related data grows over time, like comments on a post.
The right choice depends on how your application reads and writes data. When in doubt, start with embedding and pull things into separate collections if it becomes a problem.
Query Operators
Basic queries match exact values. Query operators let you match based on comparisons, ranges, and patterns.
Comparison Operators
| Operator | Meaning |
|---|---|
$eq | Equal to (same as a plain value match) |
$ne | Not equal to |
$gt | Greater than |
$gte | Greater than or equal to |
$lt | Less than |
$lte | Less than or equal to |
$in | Matches any value in an array |
$nin | Matches none of the values in an array |
// Find employees earning more than 70000
db.employees.find({ salary: { $gt: 70000 } })
// Find students in year 1 or 2
db.students.find({ year: { $in: [1, 2] } })
You can combine multiple operators on the same field. This finds products priced between 10 and 50:
db.products.find({ price: { $gte: 10, $lte: 50 } })
Logical Operators
You can combine conditions using logical operators:
| Operator | Meaning |
|---|---|
$and | All conditions must match |
$or | At least one condition must match |
// Find Engineering employees earning over 70000
db.employees.find({
$and: [
{ department: "Engineering" },
{ salary: { $gt: 70000 } }
]
})
// Find employees in Engineering or Marketing
db.employees.find({
$or: [
{ department: "Engineering" },
{ department: "Marketing" }
]
})
When you include multiple fields in a query filter, MongoDB treats it as an implicit $and. These two queries are equivalent:
db.employees.find({ department: "Engineering", salary: { $gt: 70000 } })
db.employees.find({ $and: [{ department: "Engineering" }, { salary: { $gt: 70000 } }] })
You only need explicit $and when you have multiple conditions on the same field.
Sorting and Limiting
When querying, you can control the order and number of results.
sort
sort orders the results by a field. Use 1 for ascending and -1 for descending:
// Sort by salary, highest first
db.employees.find().sort({ salary: -1 })
// Sort by name alphabetically
db.employees.find().sort({ name: 1 })
limit
limit restricts the number of documents returned:
// Get the top 3 highest-paid employees
db.employees.find().sort({ salary: -1 }).limit(3)
You can chain sort and limit together. The sort is applied first, then the limit.
skip
skip skips a number of documents before returning results. Combined with limit, it enables pagination:
// Skip the first 10 results and return the next 5
db.employees.find().skip(10).limit(5)
Projection
By default, queries return every field in the matching documents. Projection lets you specify which fields to include or exclude.
Pass a second argument to find or findOne with 1 to include a field or 0 to exclude it:
// Return only name and salary (and _id, which is included by default)
db.employees.find({}, { name: 1, salary: 1 })
// Return everything except the salary field
db.employees.find({}, { salary: 0 })
You cannot mix inclusion and exclusion in the same projection, with one exception: you can always explicitly exclude _id alongside included fields (e.g., { name: 1, _id: 0 }).
Projection is useful when documents are large and you only need a few fields.
Module 3: Full-Stack Application Development
In this module, we bring together the JavaScript, Node.js, and MongoDB skills from the previous two modules to build a backend web API.
The tool for this is Express, a lightweight web framework for Node.js. Express handles the routing layer of your server: it listens for HTTP requests, matches them to handler functions, and sends back responses. Pair that with a MongoDB connection and you have a working backend that a frontend application can talk to.
REST APIs
A REST API is a server that communicates over HTTP using a standard set of conventions. Clients make requests to specific URLs (called endpoints) using HTTP methods like GET and POST. The server processes each request and responds with JSON.
Your COMP 205 frontend will call your API using fetch. This is the backend it connects to.
What We Will Cover
By the end of this module, you should be able to build an Express server with multiple endpoints, use middleware to parse request bodies, connect Express to a MongoDB database, and handle common error cases with appropriate HTTP status codes.
Introduction to Express
Express is a web framework for Node.js. It gives you a structured way to handle HTTP requests: define a URL, specify an HTTP method, and write a function that runs when that request comes in.
Installing Express
Express is installed through npm like any other package:
npm install express
Once installed, you load it in your file with require:
const express = require("express");
Creating a Server
To create an Express application, call express(). Then tell it which port to listen on:
const express = require("express");
const app = express();
app.listen(3000, () => {
console.log("Server running on port 3000");
});
Running this with node server.js starts a server on port 3000. It does not do anything yet because no routes are defined.
Defining Routes
A route tells Express what to do when a request comes in for a specific URL and HTTP method. The two you will use most are app.get() and app.post().
app.get("/hello", (req, res) => {
res.send("Hello!");
});
The first argument is the path. The second is a callback function that receives the request (req) and response (res) objects. When a GET request comes in for /hello, Express calls that function.
res.send() sends a plain text response. For APIs, you will typically use res.json() instead, which sets the correct Content-Type header and serializes a JavaScript value as JSON:
app.get("/status", (req, res) => {
res.json({ ok: true });
});
The req and res Objects
req (request) contains information about the incoming request: the URL, any parameters, query strings, headers, and the body.
res (response) is how you send something back. A few methods you will use:
| Method | Description |
|---|---|
res.json(value) | Send a JSON response |
res.send(text) | Send a plain text response |
res.status(code) | Set the HTTP status code (chain with .json()) |
Setting a status code and sending JSON together looks like this:
res.status(404).json({ error: "Not found" });
The default status code is 200 OK. You only need to set it explicitly when it should be something else.
Route Parameters
Routes can include dynamic segments called route parameters. You define them with a colon:
app.get("/users/:id", (req, res) => {
const id = req.params.id;
res.json({ id: id });
});
A request to /users/42 would set req.params.id to "42". Parameters are always strings, so convert them if you need a different type.
Route Order
Express matches routes in the order they are defined. The first route that matches the request gets called. Define more specific routes before less specific ones to avoid unexpected matches.
A Complete Example
const express = require("express");
const app = express();
app.get("/greet/:name", (req, res) => {
res.json({ message: "Hello, " + req.params.name });
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
A GET request to /greet/Priya returns:
{ "message": "Hello, Priya" }
Middleware
Middleware are functions that run between an incoming request and your route handler. They can inspect or modify the request, attach data to it, or send a response early. You register middleware with app.use().
express.json()
By default, Express does not parse the body of incoming requests. If a client sends a POST request with a JSON body, req.body will be undefined unless you tell Express to parse it.
express.json() is built-in middleware that reads the request body, parses it as JSON, and attaches the result to req.body:
app.use(express.json());
This line goes near the top of your file, before your routes. With it in place, any POST request with a JSON body will have that data available on req.body:
app.post("/echo", (req, res) => {
console.log(req.body); // { name: "Tariq", age: 25 }
res.json(req.body);
});
Without app.use(express.json()), req.body is undefined and reading from it will cause errors.
CORS
Browsers block JavaScript from making requests to a different origin (domain, port, or protocol) than the page the script is loaded from. This is called the same-origin policy.
When a frontend application running at one origin (for example, http://localhost:5173) tries to call your Express server at a different origin (http://localhost:3000), the browser will block the request unless your server explicitly allows it.
CORS (Cross-Origin Resource Sharing) is the mechanism that allows this. You configure it on the server by sending specific response headers that tell the browser which origins are permitted.
The cors npm package handles this for you:
npm install cors
const cors = require("cors");
app.use(cors());
With no arguments, cors() allows requests from any origin.
The COMP 205 frontend runs on a different port than your Express server. Without CORS enabled, the browser will block every request the frontend makes to your API.
Middleware Order
Middleware runs in the order you register it. Register express.json() and cors() before your routes so they apply to every incoming request:
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors());
app.use(express.json());
app.post("/orders", (req, res) => {
// req.body is parsed, CORS headers are set
res.json({ received: req.body });
});
app.listen(3000);
Express with MongoDB
Connecting Express to MongoDB follows a pattern you have seen before: create a MongoClient, connect it, and then use it inside your route handlers. The difference from standalone scripts is that you connect once when the server starts and keep that connection open for the lifetime of the server.
Setup
Install both packages if you have not already:
npm install express mongodb cors
Connecting at Startup
Connect to MongoDB before starting the Express server. This way the connection is ready before any requests come in.
Declare the collection variables at the top level so your routes can reference them. Assign them inside start() after the connection resolves, then call app.listen():
const express = require("express");
const cors = require("cors");
const { MongoClient } = require("mongodb");
const app = express();
app.use(cors());
app.use(express.json());
const client = new MongoClient("mongodb://localhost:27017");
let menu, orders;
// routes go here
async function start() {
await client.connect();
const db = client.db("pizza_shop");
menu = db.collection("menu");
orders = db.collection("orders");
app.listen(3000, () => {
console.log("Server running on port 3000");
});
}
start();
Routes are defined at the top level and reference menu and orders by name. Because app.listen() is called after the connection resolves, the server does not accept requests until the collections are assigned. By the time any request arrives, menu and orders are ready.
Async Route Handlers
Database operations are asynchronous, so route handler callbacks need to be async:
app.get("/menu", async (req, res) => {
const items = await menu.find().toArray();
res.json(items);
});
Always wrap database calls in try/catch so that errors do not crash the server:
app.get("/menu", async (req, res) => {
try {
const items = await menu.find().toArray();
res.json(items);
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
Without it, an unhandled error leaves the request hanging with no response sent.
ObjectId
MongoDB assigns a unique _id to each document when it is inserted. The type is ObjectId, not a plain string, which is why a direct string comparison does not work.
When a client sends an ID as a URL parameter (like /orders/abc123), it arrives as a string. To query MongoDB by _id, you need to convert it to an ObjectId first. Add ObjectId to your existing require line at the top of the file:
const { MongoClient, ObjectId } = require("mongodb");
Before constructing the ObjectId, check that the string is valid. If the string is malformed, new ObjectId() throws an exception. That exception would be caught by the catch block and return a 500, but a bad ID string is a client error, not a server error.
ObjectId.isValid() handles this check:
app.get("/orders/:id", async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(400).json({ error: "Invalid order ID" });
}
try {
const order = await orders.findOne({ _id: new ObjectId(req.params.id) });
if (!order) {
return res.status(404).json({ error: "Order not found" });
}
res.json(order);
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
return on each early response is important. Without it, Express continues executing the function and will try to send a second response, which causes an error.
Inserting Documents
For POST endpoints, read from req.body, build the document, and use insertOne:
app.post("/orders", async (req, res) => {
const { customer, items } = req.body;
if (!customer || !items || items.length === 0) {
return res.status(400).json({ error: "customer and items are required" });
}
try {
const order = {
customer: customer,
items: items,
status: "pending"
};
const result = await orders.insertOne(order);
res.status(201).json({ ...order, _id: result.insertedId });
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
201 Created is the appropriate status code when a resource is successfully created. The client gets back the full order document including the _id that was assigned.
HTTP Status Codes
| Code | Meaning | When to use |
|---|---|---|
200 | OK | Successful GET or default success |
201 | Created | Resource was inserted |
400 | Bad Request | Invalid input from the client |
404 | Not Found | Requested resource does not exist |
500 | Server Error | Unexpected failure, usually a database error |
Put your input validation before the try/catch block. Validation errors (400) are not database errors and should not be lumped together with 500 responses.
Putting It Together
The previous pages covered each concept in isolation. This page shows what a complete Express + MongoDB server looks like when everything is assembled.
The server below handles three endpoints for a small bookstore: fetching all books, looking up a single book by ID, and creating a new book.
const express = require("express");
const cors = require("cors");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
app.use(cors());
app.use(express.json());
const client = new MongoClient("mongodb://localhost:27017");
let books;
app.get("/books", async (req, res) => {
try {
const all = await books.find().toArray();
res.json(all);
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
app.get("/books/:id", async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(400).json({ error: "Invalid book ID" });
}
try {
const book = await books.findOne({ _id: new ObjectId(req.params.id) });
if (!book) {
return res.status(404).json({ error: "Book not found" });
}
res.json(book);
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
app.post("/books", async (req, res) => {
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
try {
const book = { title, author };
const result = await books.insertOne(book);
res.status(201).json({ ...book, _id: result.insertedId });
} catch (err) {
res.status(500).json({ error: "Database error" });
}
});
async function start() {
await client.connect();
const db = client.db("bookstore");
books = db.collection("books");
app.listen(3000, () => {
console.log("Server running on port 3000");
});
}
start();
A few things worth noting about the overall structure:
- Routes are registered at the top level, outside
start(). This keeps the startup logic separate from the route definitions. booksis declared at the top level withletand assigned insidestart()after the connection resolves. Becauseapp.listen()is called after that assignment, the server does not accept requests untilbooksis ready.- Validation runs before the
try/catch. A missing field is a client error (400), not a server error (500). - Each early return on an error response prevents Express from trying to send a second response after the function continues.
Test 1 Study Guide
The first test of the course covers Module 1: Client & Server-side Scripting. You can review the material on the following pages:
- Client & Server-side Scripting (Overview)
- Introduction to JavaScript
- Functions in JavaScript
- Introduction to Node.js
- JavaScript and Runtimes
- The DOM & JavaScript in the Browser
Key Concepts
In addition to reviewing the content above, you should feel comfortable answering the following questions:
- What is the difference between client-side and server-side code?
- What is a JavaScript runtime, and why does JavaScript need one?
- Name the two main JavaScript runtimes used in this course, and describe how they differ.
- What is the difference between
let,const, andvar? Which do we use in this course, and why? - Can the contents of an array declared with
constbe modified? Why or why not? - What is the difference between
==and===in JavaScript? Which should you use by default? - What happens if you call a JavaScript function with fewer arguments than it has parameters?
- What is the difference between a parameter and an argument?
- What does
npmstand for, and what is it used for? - What is the DOM, and how does it differ from the HTML source file?
- Why does
document.getElementById()work in a browser but not in Node.js? - Why should
<script>tags be placed at the bottom of the<body>? - Describe what happens when a user clicks a button that has an event listener attached to it.
Test 2 Study Guide
The second test of the course covers Module 2: Working with Databases. You can review the material on the following pages:
- Working with Databases (Overview)
- Introduction to NoSQL
- Introduction to MongoDB
- CRUD Operations
- MongoDB with Node.js
- Data Modelling and Queries
Key Concepts
In addition to reviewing the content above, you should feel comfortable answering the following questions:
- What is the difference between a SQL database and a NoSQL database?
- What are the three levels of organization in MongoDB (from largest to smallest)?
- What is an
ObjectId, and what is the_idfield used for? - What is the difference between
findandfindOne? What doesfindOnereturn if nothing matches? - What does a query filter do, and what happens when you pass multiple fields in a single filter?
- What does each of the following update operators do:
$set,$unset,$inc,$push,$pull? - What happens if you pass a plain object to
updateOnewithout using an update operator like$set? - Why do database operations in Node.js require
asyncandawait? - What is the purpose of
try/catch/finallywhen working with database code? Why isfinallya good place to close the connection? - How does
find()behave differently in Node.js compared to the Mongo shell? What method do you call to get the results as an array? - What is the difference between embedding and referencing when designing a document schema? Give an example of when each is appropriate.
- Write a query that finds all documents where a numeric field is greater than a given value.
- What is the difference between
$andand$or? When do you need explicit$andversus relying on implicit$and? - What do
sort,limit, andskipdo? How can they be combined for pagination? - What is projection, and why would you use it?
Test 3 Study Guide
The third test of the course covers Module 3: Full-Stack Application Development. You can review the material on the following pages:
- Full-Stack Application Development (Overview)
- Introduction to Express
- Middleware
- Express with MongoDB
- Putting It Together
Key Concepts
In addition to reviewing the content above, you should feel comfortable answering the following questions:
- What is Express, and what role does it play in a web application?
- What is a REST API? What does a client send, and what does the server respond with?
- What is the difference between
app.get()andapp.post()? - What is the difference between
res.json()andres.send()? Which do you use for APIs, and why? - What is a route parameter? How do you define one, and how do you access its value?
- Why does route order matter in Express? What happens if a less specific route is defined before a more specific one?
- What is middleware? What does
express.json()do, and what happens toreq.bodyif you forget to register it? - What is CORS? Why does the browser block requests from a frontend on one port to an API on a different port, and how does the
corsmiddleware fix it? - Why do you connect to MongoDB before calling
app.listen()in an Express server? - Why do route handler callbacks need to be
asyncwhen they make database calls? - Why should database calls in route handlers be wrapped in
try/catch? What happens if you omit it? - What is
ObjectId, and why can't you query MongoDB by_idusing a plain string? What should you check before constructing one from a URL parameter? - Why is it important to
returnafter sending an early error response in a route handler? - What is the difference between a success response, a client error response, and a server error response? Given a scenario (resource created, invalid input, resource not found, unexpected server failure), which category does it fall into?