Web Application Development

CSC13008-23KTPM1

Note 05 - SSR and CSR

Lecturer: Đỗ Nguyên Kha

Semester 1/2025-2026 @ FIT-HCMUS

[Print]

Content

  1. JavaScript
  2. SSR
  3. CSR

Variable declaration

In JavaScript, we use let to declare a variable. Here’s an example:


						let persons; // declaring a variable
					

Here, the variable persons is declared, but its value is undefined.

Variable declaration

JavaScript supports the following:

  • Number: Numeric values (5, 3.0, -1, etc.)
  • String: String values (HCMUS, JavaScript, etc.)
  • Boolean: Boolean values (true, false)

						let persons = 5;
						let totalWeight = 350.6;
						let team = "A";
						let isTeamA = true;
					

Operations

The type of data also tells which operations are permissible on a specific piece of data and indicates the semantics (or meaning) of those operations. The following code demonstrates the effect of the + operation on different types of data in JavaScript:


						let a = 5;
						let b = 7;
						let sum1 = a + b; // sum of a and b
						console.log("The sum of numbers: " + sum1); // will result in whole number
					

Operations


						let c = 3.2;
						let d = 1.5;
						let sum2 = c + d; // sum of c and d
						console.log("The sum of numbers: " + sum2); // will result in decimal point number
					

						let e = 3;
						let f = 1.5;
						let sum3 = e + f; // sum of e and f
						console.log("The sum of numbers: " + sum3); // will result in decimal point number
					

						let g = "5";
						let h = "7";
						let result = g + h; // concatenates g and h
						console.log("The concatenation of strings: " + result);
					

Input

When we use a prompt() statement in JavaScript, it always returns a string of characters. Even the integer, 25, is considered a string, as shown in the following code widget:


						let a = prompt("Enter an integer?"); // 1st integer
						let b = prompt("Enter another integer?"); // 2nd integer
						let result = a + b; // adding up
						console.log("When we apply + operator it gives " + result + " which is concatenation of two strings!");
					

In the code above, integers are treated as strings and they are concatenated due to the + operator in the output.

parseInt()

We must convert the input value to the suitable data type to apply the correct operation. We should use the parseInt() to convert it into an integer, as demonstrated below:


						let a = parseInt(prompt("Enter an integer?")); // 1st integer
						let b = parseInt(prompt("Enter another integer?")); // 2nd integer
						let result = a + b; // adding up
						console.log("When we apply + operator it gives " + result + " which is addition of two numbers!");
					

Note: Even if the input value is a floating-point, parseInt() converts it into an integer. The result contains only the integer part in the output.

parseFloat()

Similarly, we can use the parseFloat() to store a floating-point value in a variable, as demonstrated below:


						let a = parseFloat(prompt("Enter a floating-point value?")); // Taking float variable input in a
						console.log("You entered the value " + a);
					

Operators

There are several other commonly used operators in JavaScript, including:

  • Arithmetic operators: These are used for arithmetic operations (addition, subtraction, multiplication, etc.).
  • Comparison operators: These are used in conditional expressions (less than, greater than, etc.).

Operators

  • Logical operators: These are used to combine conditional expressions (and, or, not).
  • Compound assignment operators: These are the combinations of arithmetic and assignment operators.

Conditional Statement

The most commonly used conditional statement in JavaScript is the if statement. It contains a conditional expression and a true branch, and it may contain a false branch. The true branch is executed when the conditional expression is true. The false branch, if present, is executed when the conditional expression is false. The following program demonstrates the use of the conditional statement.


						if (conditional statement) { // no semicolon here
							// execution starts from here if the conditional statement is true
						} else { // no semicolon here
							// if the if branch is not executed then this part of the code is executed
						}
					

Conditional Statement

We want to write a program that determines eligibility for a driver’s license based on the age that has been input by the user. The eligible age should be 18 or above.


						let age = parseInt(prompt("Enter your age?")); // taking age input
						if (age >= 18) { // if age is greater than and equal to 18
							console.log("You are eligible for a driver's license.");
						} else {
							console.log("You are not eligible for a driver's license.");
						}
					

Ternary Operators

Ternary operators are a shortcut to an if-condition which executes expressions depending on whether the condition is true or false.


						let variable = 0;
						if (variable === 0){
							variable = 1;
						} else{
							variable = 0;
						}
						console.log('variable:', variable);
						
						variable = 0;
						variable = variable === 0 ? 1 : 0; // 1 if variable is 0 else 0
						console.log('variable:', variable);
					

Ternary Operators

Ternary operators are a shortcut to an if-condition which executes expressions depending on whether the condition is true or false.


						 ?  : ;
					

The for loop

The counter-controlled loop is used to generate a sequence of values. This loop has four parts after the for keyword as shown below:


						for (initialization; condition; update) {
							// body of the loop
						}
					

The initialization value initializes the counter and is executed only once. The condition value checks the value of the counter at the start of each iteration before entering the body of the loop. If it’s true, the body of the for loop is executed, and if false, the for loop is terminated. The update value updates the value of the counter and again checks the condition.

The for loop

The counter-controlled loop is used to generate a sequence of values. This loop has four parts after the for keyword as shown below:


						for (let a = 0; a < 5; a++) // Using for loop 
							console.log(a);
					

The while loop

We use the while loop when the termination of the loop depends on the sentinel value instead of a definite number of iterations. As a simple example, we want to display the reverse sequence of digits in a positive integer value input by the user.


						let a = parseInt(prompt("Enter a number")); // Taking input in variable a
						let result = "";
						while (a > 0) { // This loop will terminate when the value is not greater than 0
							result += (a % 10); // Storing result
							a = parseInt(a / 10); // Dividing a by 10 and assigning the result to variable a
						}
						console.log(result);
					

Types of types

Types in JavaScript group together similar kinds of values. We can further categorize these types into the following:

  • Primitive values
  • Objects and functions

Primitive values

Following are all types that fall under the primitive category:

  • Boolean
  • Number
  • Null
  • Undefined
  • String
  • Symbol

Boolean

These are values that take only the values true and false.


						let bool_true = true; // initialize variable to true
						let bool_false = new Boolean(false); // initialize variable to false
						// print values
						console.log('The values are:', bool_true, bool_false);
						// print types
						console.log('The types are:', typeof(bool_true), typeof(bool_false));
					

Number

Number data type consists of all numeric values. They include regular numbers and decimals. Not all numbers can be perfectly represented in JavaScript. Their decimal part offers more precision when closer to 0 and less precision further away from it.


						let num1 = 120; // initialize variable to 120
						let num2 = new Number(0.002); // initialize variable to 0.002
						// print values
						console.log('The values are:', num1, num2);
						// print types
						console.log('The types are:', typeof(num1), typeof(num2));
					

Number

The numbers type also consists of “special numeric values,” represented by Infinity, -Infinity, and NaN (Not a Number).

NaN === NaN is false, even though they are the same value.

0 === -0 and -0 === 0 is true, but they are different values.

Null

Null is a type that allows us to represent nothing.


						let num1 = null; // initialize variable to null
						// print values
						console.log('The values are:', num1);
						// print types
						console.log('The types are:', typeof(num1));
					

And although the typeof function shows that it is of type object, it is not. It is a primitive type. For legacy reasons, the typeof will show that null is of type object.

Undefined

Undefined is a value assigned to variables that have not been assigned a value.


						let num1; // initialize variable without assignment
						let num2 = undefined; // initialize variable to undefined
						
						// print values
						console.log('The values are:', num1, num2);
						// print types
						console.log('The types are:', typeof(num1), typeof(num2));
					

The difference between null and undefined types can be confusing. null expresses a lack of identification while undefined represents a lack of assignment of the variable.

Objects

Almost everything in JavaScript is made up of objects. These are all the non-primitive types. They include arrays, dates, and almost everything non-primitive.


						let obj = {}; // Declare our object
						//print value and type of obj
						console.log('Our object:', obj);
						console.log('Type of our object:', typeof(obj));
					

Objects

While declaring or after declaring an object, we can assign and modify the properties of an object. A property is a (key, value) pair, where a key is a string (also called a “property name”), and value is the assigned value. Let’s look at the code below.


						// Declare our object with a `name` property having value: “obj”
						let obj = {name : "obj" , 'age': 20};
						// defining properties and assigning values using . operator
						obj.number = 13;
						// defining properties and assigning values using [] operator
						obj['obj2'] = {};
						//print value 
						console.log('Our object:', obj);
					

Objects

You can access a property of an object using . and [] operators. Let’s see an example.


						let obj = {name : "obj", number: 13, obj2: {} }; // Declare our object
						let num_obj = obj['number'];
						let name = obj.name;
						console.log(`obj['number']: ${obj['number']} , ${num_obj}`);
						//print name of the obj
						console.log(`obj.name: ${obj.name} , ${name}`);
						// print the types of properties
						console.log(`types: ${typeof(obj['number'])}, ${typeof(num_obj)}`);
						console.log(`types: ${typeof(obj['name'])}, ${typeof(name)}`);
					

Date

Date is an object with an interface to store date and time. It also allows you to format or manage them.


						let obj = new Date() // create a Date obj and assign to obj
						console.log(obj); // print obj
						console.log(obj.getFullYear()); // print getFullYear method of date
					

String

A string is a collection of characters, mostly treated as a single unit. It is used to store text values like a name, address, message, etc. There are various ways to initialize a string in JavaScript. The following program demonstrates these methods:


						let s1 = 'String in Single Quotes';
						console.log(s1);
						
						let s2 = "String in Double Quotes";
						console.log(s2);
						
						let s3 = `String in Backticks`;
						console.log(s3);
					

String

JavaScript allows us to access individual characters inside a string through an integer index number. To do this, we enclose the index number in square brackets after the string variable name, as shown in the following program:


						let s = "HCMUS";
						console.log(s)
						console.log(s[0]); // Displaying first element of string
						console.log(s[1]); // Displaying second element of string
						console.log(s[2]); // Displaying fourth element of string
						console.log(s[3]); // Displaying fifth element of string
						console.log(s[4]); // Displaying sixth element of string
					

String

The same task can also be performed using loops as well. The previous program can be written in the following manner.


						let s = "HCMUS";
						let length = s.length; // Storing the length of the array
						for (let i = 0; i < length; i++) { 
							console.log(s[i]);
						}
					

Array

In JavaScript, an array is a collection of values. It is a comma-separated list of values enclosed in a pair of square brackets. An array can be stored as a single variable. For example, ["apple", "mango", "banana"] is an array of strings. It can be stored as a variable, as shown in the code below:


						let arrayOfFruits = ["apple", "mango", "banana"]; // Creating an array of strings
						console.log(arrayOfFruits); // Listing the values of array
					

Array

The following program demonstrates the various types of arrays in JavaScript:


						let a1 = []; // Empty Array
						let a2 = [100]; // Single-valued array
						let a3 = [7, 7, 4, 4, 3, 3, 3, 6, 5]; // Array having duplicate Numbers
						let a4 = [50, 'Learn', 10, 'To', 2.5, -1, 'Code']; // Array with the use of mixed values
					

Array

The individual values in an array are accessed through an index. An index is an integer representing the position of an individual value in the array. We enclose the index in square brackets after the array variable name. The first value is at index 0, and the index moves forward in the array by linear increments, as demonstrated in the following code:


						let a = [-5, 'JavaScript', 3.8];
						console.log("Array items in the forward order: ");
						console.log("The value at index 0: " + a[0]);
						console.log("The value at index 1: " + a[1]);
						console.log("The value at index 2: " + a[2]);
					

The for loop with arrays

The individual values in an array are accessed through an index number. The for loop variable can be used as an index number.


						let vals = [-5, 'JavaScript', 3.8];
						console.log("Using loop variable as a list index");
						for (let i of [0, 1, 2]) {
							console.log(vals[i]);
						}
						console.log("Directly accessing the elements");
						for (let v of vals) { // string v is read only
							console.log(v);
						}
						let mix = ['a', 1, 2.5, 'i', 50, 6, 'm', 4.4, 6.7, 's', 'fit@hcmus'];
						console.log('List index using "length"');
						for (let v = 0; v < mix.length; v++) { console.log(mix[v]); }
					

Function

We can divide a program into procedural components or modules called functions in JavaScript. Functions can be copied and reused in a programming technique called a modular or procedural approach, also known as the divide and conquer approach.


						// Defining a function named sayHello
						function sayHello() {
							console.log("Hello");
						}
						// Calling the function
						sayHello();
					

Function

A function has the following properties:

  • A list of arguments which can be anything and is optional. This is the input to the function.
  • A program block is executed on the invocation of the function.
  • An output that is returned after the function finishes executing using a return statement.

						// Define function that takes the sum of two numbers
						function sum(a , b){
							console.log('Taking sum in the function'); // print to see function execute
							let ans = a + b; // assign sum of a and b to ans
							return ans; // return ans as output
						}
					

Arrow Function

Arrow function or an arrow function expression is the more concise syntax for writing regular function expressions. Also known as a fat arrow function, they utilize => token, which is shaped like a fat arrow. With their concise syntax, we avoid writing return tokens, function token, and curly brackets. Let’s look at the following syntax.


						// Define function that takes the sum of two numbers
						let func = (a, b) => { // Create arrow function for taking sum of two numbers
							let sum = a + b; // take sum of a and b and assign to sum variable
							return sum; // return sum
						}
						console.log("Sum of 1 and 3:" , func(1,3)); // print func with arguments 1 and 3
					

Arrow Function

A single-lined function can be more concise


						let func = (a, b) => a + b; // arrow function assigned to func to add a and b
						console.log("Sum of 1 and 3:" , func(1,3)); // print func with arguments 1 and 3
					

Global scope

Global scope is the outermost scope in the JavaScript program. In this scope, all variables are accessible by any other scope of the program.


						let a = 10; // variable a assigned to 10
						
						let func = function (){
							let innerFunc= function () {
								console.log("a is accessible:", a);
							}
							innerFunc();
							return;
						}
						func(); // invoke function func
					

Local Scope

Local scopes are scopes that limit their accessibility of variables to only certain parts of the program. Despite this, they have full access to all variables in the global scope. The local scope has the following types.

  • Function scope
  • Block scope

Function Scope

Function scope is the scope inside the function declaration. Any variable declared inside a function would not be accessible outside of it.


						let a = 10; // variable a assigned to 10
						
						let func = function () { // outermost function
						let b = 20;
							console.log("a and b is accessible (outer):", a, b);
							let innerFunc = function () { // innermost function
								console.log("a and b is accessible (innner):", a, b);
							}
							innerFunc();
							return;
						}
						func(); // invoke function func
						console.log("only a is accessible (global):", a);
					

Block Scope

Block scope is a local scope bounded between two curly brackets {}. The block scope lets you limit the accessibility of all variables declared inside it. This scope uses let or const token, only letting these variables be accessed inside the curly brackets {}. However, variables declared by let token are not bound by block scope and are accessible outside it.


						let a = 10; // variable a assigned to 10
						if (a === 10) {
							let b = 6; // variable b assigned to 6 using let
							let c = 7; // variable c assigned to 7 using var
							console.log("a, b and c accessible (block scope)", a, b, c); // a, b and c accessible inside block scope
						}
						console.log("a and c accessible (global scope)", a, c); // in global scope on a and c accessible
					

Regular Expressions

In programming, we frequently parse or make queries over strings. Pattern matchings become tedious when writing string manipulation or iteration code. Moreover, it is not always best to write loops to index and iterate strings for manipulation. The solution to these issues? Regular expressions.

Regular Expressions

Regular expressions are expressions containing a sequence of characters to invoke a sequence matching or pattern matching of strings. They usually carry out “search and replace” actions. These are sometimes called rational expressions or RegEx for short. RegExes are available in most programming languages including JavaScript.

Regular Expressions

There are two formats when writing a RegEx in JavaScript.


						let regex1 = /[a-b]*/; // Create a regex and assign to regex1
						let regex2 = RegExp('[a-b]*'); // Create a regex and assign to regex2
						console.log(regex1, regex2); // Print the regex variables
					

Regular Expressions

The exec method runs the regular expression on a string passed as an argument and returns the found text as an object.


						let regex = /[a-b]+/i; // Create a regex and assign to regex
						console.log('exec on "Abc123":', regex.exec("Abc123"));
						console.log('exec on "123":', regex.exec("123"));
						console.log('exec on "ABC":', regex.exec("ABC"));
					

Regular Expressions

The test method runs the regular expression on a string passed as an argument and returns a boolean value telling if a match was found or not


						let regex = /[a-b]+/i; // Create a regex and assign to regex
						console.log('test on "Abc123":', regex.test("Abc123"));
						console.log('test on "123":', regex.test("123"));
						console.log('test on "ABC":', regex.test("ABC"));
					

From the above code, we can see how to use the test method on RegEx. For each of the strings passed, it returns true if a match is found or false if no match is found.

Object-oriented programming

In JavaScript, there are two approaches to implementing OOPs: prototype-based and class-based.

Prototype-based programming

Before (and even after) introducing its ES2015 version, JavaScript relied on prototype-based programming. In this programming style, the object encapsulates the properties, its methods and data, instead of a class. You can add new properties to this object whenever you want. An object can be an individual, instead of an instance of the class; if you want an object, you can create one without creating a class first.

Prototype-based programming

The class-based programming was introduced in the ES2015 version of JavaScript and allows using the class token in implementation. Furthermore, they assist in inheritance and in implementing OOPs through constructor, extend, super, and static keywords. This new syntax is clearer and easier to use.

Note: Both prototype-based and class-based programming have the same back-end implementation and only differ in the syntactic aspect.

Prototypes

All JavaScript objects have the property prototype. They go under the property name __proto__. The prototype of each object is assigned during creation and is itself an object


						let vehicle = { wheels : 4 }; // object assigned to variable named vehicle
						let car = { seats : 5 }; // object assigned to variable named car
						let driver = {} // empty object assigned to variable named driver
						
						// Print all objects and __proto__ property for each variable
						console.log(`vehicle:`, vehicle, vehicle.__proto__);
						console.log(`car:`, car, car.__proto__);
						console.log(`driver:`, driver, driver.__proto__);
					

Prototypes

Assigning vehicle to car object’s __proto__ property allows car object to access properties of the object vehicle.


						let vehicle = { wheels : 4 }; // object assigned to variable named vehicle
						let car = {
							seats : 5,
							__proto__ : vehicle // __proto__ property assigned to vehicle
						}; // object assigned to variable named car
						
						// Print all objects and __proto__ property for each variable
						console.log(`vehicle:`, vehicle, vehicle.__proto__);
						console.log(`car:`, car, car.__proto__);
						console.log(`vehicle seat:`,vehicle.seats);
						console.log(`car wheels:`, car.wheels);
					

Constructor Functions

Constructor functions, or object constructor functions, contain blueprints to a type of object that has properties and methods to generate objects. All objects created from a constructor function will have the same properties and methods.


						function Employee(_name, _age, _designation){
							// Properties assignment passed as arguments
							this.name = _name;
							this.age = _age;
							this.designation = _designation;
							// Method
							this.setAge = newage => {
								console.log(`setting age from ${this.age} to ${newage}`)
								this.age = newage;
							}
						}
					

Constructor Functions


						let employee1 = new Employee('Mark', 12, 'Manager'); // create Employee object
						// Print Employee object assigned to employee1
						console.log(`employee1 name: ${employee1.name} age: ${employee1.age}`);
						employee1.setAge(20); // call method to set age of employee1
						console.log(`employee1 name: ${employee1.name} age: ${employee1.age}`);
					

Constructor Functions

All objects created by the constructor function share its prototype object. In our earlier example, all objects created using Employee constructor function will share the same prototype object assigned to __proto__ property.


						let employee1 = new Employee('Mark', 20, 'Manager'); // create Employee object
						let employee2 = new Employee('Bob', 30, 'Accountant'); // create Employee object
						// Print Employee objects prototype objects
						console.log(employee1.name, employee2.name);
						console.log(Employee.prototype);
						console.log(`employee protoypes: ${employee1.__proto__}, ${employee2.__proto__}`);
						console.log(`protoype equalities: ${employee1.__proto__ === employee2.__proto__}`);
					

Classes

Similar to constructor functions, classes are blueprints of a type of object with the certain properties and methods to generate objects. All objects created from a class have the same properties and methods, though not necessarily the same values. Instead of using functions, they use class keyword to create classes used to implement this functionality.


						class ClassName {
							constructor(parameter1, parameter2,...) {
								//initializing class properties
							}
							//class methods defined
						}
					

Classes


						class Employee{
							constructor(_name, _age, _designation){
								this.name = _name;
								this.age = _age;
								this.designation = _designation;
							}
							// Methods defined outside the constructor
							setAge(newage){
								console.log(`setting age from ${this.age} to ${newage}`)
								this.age = newage;
							}
						}
					

Inheritance

In JavaScript, we implement inheritance through manipulating prototypes of objects. An object’s ability to inherit properties of another object through manipulating the prototype property is called prototypal inheritance. This accesses properties and methods from the object from which it inherits.

Inheritance


						let vehicle = { wheels : 4 }; // object assigned to variable named vehicle
						let car = {
							seats : 5,
							__proto__ : vehicle // __proto__ property assigned to vehicle
						}; // object assigned to variable named car
						let bmw = {
							price : 50000,
							owner : "Bob",
							__proto__ : car, // __proto__ property assigned to car (inherits car)
						};
						// Print all properties of bmw object
						console.log("BMW price:", bmw.price);
						console.log("BMW owner:", bmw.owner);
						console.log("BMW seats:", bmw.seats);
						console.log("BMW wheels:", bmw.wheels);
					

Inheritance in Constructor Functions


						function Base() {}
						function Derived() {}
						// Set the `[[Prototype]]` of `Derived.prototype`
						// to `Base.prototype`
						Object.setPrototypeOf(Derived.prototype, Base.prototype);

						const obj = new Derived();
						// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
					

Inheritance in classes


						class Base {}
						class Derived extends Base {}
						
						const obj = new Derived();
						// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
					

Request-Response Cycle

In web development, the request-response cycle is fundamental to how servers and clients communicate. Every time a user interacts with a website—whether by clicking a link, submitting a form, or making an API request—a request is sent to the server, and the server processes it before responding.

Source: https://www.geeksforgeeks.org/web-tech/request-and-response-cycle-in-express-js/

The request-response cycle in Express.js

Express.js acts as an intermediary between the client and the server, handling incoming requests and determining the appropriate response. The cycle follows the steps below:

  1. A client (browser or API consumer) requests HTTP.
  2. Express.js receives the request and processes it.
  3. The server performs necessary operations (e.g., retrieving data and executing logic).
  4. A response is generated and sent back to the client.

Request Object


						const express = require("express");
						const app = express();
						const port = 3000;
						
						app.get("/log", (req, res) => {
							console.log("Incoming request details:", req.method, req.url);
							console.log("Query Params:", req.query);
							console.log("Body:", req.body);
							res.send("Request details logged to console");
						});
						
						app.listen(port, () => {
						console.log(`Server is running on http://localhost:${port}`);
						});
					

Response Object


						const express = require("express");
						const app = express();
						const port = 3000;
						
						app.get("/greet", (req, res) => {
							res.send("Hello, welcome to our API!");
						});
						
						app.get("/json", (req, res) => {
							res.json({ message: "Hello, this is a JSON response!" });
						});
						
						app.get("/error", (req, res) => {
							res.status(404).send("Page not found!");
						});
						
						app.get("/redirect", (req, res) => {
							res.redirect("/greet"); // Redirects to the /greet endpoint
						});
						
						app.get("/file", (req, res) => {
							res.sendFile(__dirname + "/example.txt"); // Serves a file from the server
						});
						
						app.get("/end", (req, res) => {
							res.write("Processing...");
							res.end(); // Ends the response without sending additional data
						});
						
						app.listen(port, () => {
							console.log(`Server is running on http://localhost:${port}`);
						});
					

Routing

Routing determines how a web application handles incoming requests based on the requested URL and HTTP method. Think of it like a traffic system, directing users to the right destination. For example, when someone visits a website, the application must decide whether to display a web page or return data.

Routing

In Express.js, a route defines how an application processes incoming requests. A route consists of three key parts:

  • Path: This is the requested resource.
  • HTTP method: This specifies the type of request (GET, POST, PUT, or DELETE) and the intended action (retrieving, creating, updating, or deleting data).
  • Route handler: This function processes the request and sends a response.

Routing

Here is an example of a route that handles GET requests at the /welcome path and responds with 'Welcome to Express.js!'.


						app.get('/welcome', (req, res) => {
							res.send('Welcome to Express.js routing!');
						});
					

View


						const express = require('express');
						const app = express();
						const port = 3000;
						
						// Set EJS as the templating engine
						app.set('view engine', 'ejs');
						app.set('views', './views');
						
						// Sample data
						const products = [
							{ id: 1, name: 'Laptop', price: 75000 },
							{ id: 2, name: 'Mobile', price: 25000 },
							{ id: 3, name: 'Tablet', price: 35000 }
						];
						
						// Route to render products with EJS
						app.get('/products', (req, res) => {
							res.render('products', {
								title: 'Product Catalog',
								products: products
							});
						});
						
						// Root route
						app.get('/', (req, res) => {
							res.render('index', { title: 'Home', message: 'Welcome to the store!' });
						});
						
						// Start server
						app.listen(port, () => {
							console.log(`Server running at http://localhost:${port}`);
						});
					

Server-side rendering

Server-side rendering (SSR) refers to the practice of generating HTML content on the server and sending it to the client. SSR is opposed to client-side rendering, where the client generates the HTML content using JavaScript. Both techniques are not mutually exclusive and can be used together in the same application.

Server-side rendering

A static site can be considered as SSR (and can be generated using SSR infrastructure), but there are nuanced differences. Content of a static site is generated at build time, not at request time. Static sites often do not need to be deployed on a server at all, and can be served from a CDN.

Server-side rendering

Benefits of SSR include:

  • Accessibility
  • Crawler-friendliness
  • Performance

Client-side rendering

Client-side rendering (CSR) refers to the practice of generating HTML content using JavaScript in the browser. CSR is opposed to server-side rendering, where the server generates the HTML content. Both techniques are not mutually exclusive and can be used together in the same application.

Client-side rendering

Benefits of CSR include:

  • Interactivity
  • Performance

AJAX

Asynchronous JavaScript and XML lets a web page communicate with a server to fetch or send data without reloading the entire page. This enables dynamic updates and smoother user experiences. It typically uses the XMLHttpRequest API or the newer Fetch API.

XMLHttpRequest


						// Simple AJAX GET request using XMLHttpRequest
						const xhr = new XMLHttpRequest();
						xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');

						xhr.onload = function() {
							if (xhr.status >= 200 && xhr.status < 300) {
								console.log('Response:', xhr.responseText);
							} else {
								console.error('Request failed with status:', xhr.status);
							}
						};

						xhr.onerror = function() {
							console.error('Network error occurred');
						};

						xhr.send();
					

fetch


						fetch('https://jsonplaceholder.typicode.com/posts')
						.then(response => {
							if (!response.ok) {
								throw new Error('Network response was not ok');
							}
							return response.json();
						})
						.then(data => {
							console.log('Data received:', data);
						})
						.catch(error => {
							console.error('Fetch error:', error);
						});
					

JSON

JavaScript Object Notation is a lightweight, text-based data-interchange format that uses human-readable named values and arrays. It’s language-agnostic but closely resembles JavaScript objects, making it easy to serialize data for sending between a server and a client. Typical usage in web apps is to store or transmit structured data (like a list of tasks) as a JSON string, then parse it back into native objects in your code. A simple example: {"task":"Buy milk","done":false} represents a single item in a larger array of tasks.

JSON

In JavaScript, JSON serialization is done using JSON.stringify, and deserialization uses JSON.parse


						const user = { name: "Alex", age: 30 };
						const jsonString = JSON.stringify(user); // '{"name":"Alex","age":30}'
					

						const jsonString = '{"name":"Alex","age":30}';
						const user = JSON.parse(jsonString); // { name: "Alex", age: 30 }
					

SSR vs CSR

Both SSR and CSR have their performance tradeoffs, and a mix of SSR and CSR can be used to combine the benefits of both techniques. For example, the server can generate a page skeleton with empty placeholders, and the client can fetch additional data and update the page as needed.

TODO app

A TODO app is a simple tool to capture, organize, and track tasks. It usually lets you add items, mark them complete, and view them in a list, sometimes with due dates, priorities, and categories. Most designs include a minimal input field, a list of tasks with checkboxes, and actions to edit or remove items. The goal is a quick, distraction-free way to stay organized and productive.

Group Assignment - GA03

  • TODO app with vanilla JavaScript (no Framework): Add new task, Mark/Unmark as done, Remove task
  • Scoring:
    • Tailwind and Responsive layout: 2.0
    • Homepage, TODO list page (static): 2.0
    • Add/Mark/Unmark/Remove: 6.0