Month 2, Week 1

ES6+ Mastery & The Asynchronous World

The Syntax of Modern Backend Development

Module 1: Modern JavaScript Syntax

Writing Cleaner, More Powerful Code

Arrow Functions `=>`

A more concise syntax for writing functions. They are essential in modern JavaScript.

Before (Function Expression)


                        const add = function(a, b) {
                            return a + b;
                        };
                    

After (Arrow Function)


                        const add = (a, b) => {
                            return a + b;
                        };
                    

Arrow Functions: Implicit Return

For single-expression functions, you can make them even more concise with an implicit return.


                        // No curly braces, no 'return' keyword needed
                        const multiply = (a, b) => a * b;

                        // This is identical to:
                        const multiplyVerbose = (a, b) => {
                            return a * b;
                        };
                    

This is heavily used in functional methods like .map() and .filter().

Destructuring: Unpacking Values

A powerful syntax for extracting values from arrays or properties from objects into distinct variables.

Object Destructuring


                        const user = {
                            id: 1,
                            name: 'Alex',
                            role: 'admin'
                        };

                        // Extracts `name` and `role` into new constants
                        const { name, role } = user;

                        console.log(name); // 'Alex'
                        console.log(role); // 'admin'
                    

This is extremely useful for pulling data out of API responses or configuration objects.

Array Destructuring

The same concept applies to arrays, but it unpacks based on position.


                        const coordinates = [10, 20, 30];

                        const [x, y, z] = coordinates;

                        console.log(x); // 10
                        console.log(y); // 20
                        
                        // You can ignore elements with a trailing comma
                        const [first, , third] = coordinates;
                        console.log(third) // 30
                    

Destructuring with Renaming & Defaults


                        const user = {
                            id: 1,
                            name: 'Alex'
                        };

                        // Rename `name` to `userName` and provide a default for `role`
                        const { name: userName, role = 'viewer' } = user;

                        console.log(userName); // 'Alex'
                        console.log(role);     // 'viewer'
                    

This pattern is invaluable for safely extracting and renaming data from potentially incomplete objects.

Spread vs. Rest Operators

They use the same syntax (...) but do opposite things based on where they are used.

Spread Operator: Spreads Elements Out

Used for making shallow copies and merging arrays/objects.


                        // In Arrays
                        const arr1 = [1, 2];
                        const arr2 = [...arr1, 3, 4]; // Result: [1, 2, 3, 4]

                        // In Objects
                        const user = { name: 'Alex' };
                        const userWithRole = { ...user, role: 'admin' };
                        // Result: { name: 'Alex', role: 'admin' }
                    

Rest Parameter: Gathers Elements Together

Used in function arguments to collect multiple arguments into a single array.


                        // Gathers all arguments after `a` and `b` into the `rest` array
                        function sum(a, b, ...rest) {
                            let total = a + b;
                            // `rest` is an array: [3, 4, 5]
                            rest.forEach(num => total += num);
                            return total;
                        }

                        sum(1, 2, 3, 4, 5); // Returns 15
                     

Module 2: ES6 Classes

Blueprints for Objects

The class Keyword

ES6 classes are "syntactic sugar" over JavaScript's existing prototype-based inheritance. They provide a much cleaner and more familiar syntax for creating object blueprints.

This is the foundation for object-oriented programming in frameworks like NestJS.


                        class User {
                            // The constructor is a special method for creating and initializing an object
                            constructor(name, email) {
                                this.name = name;
                                this.email = email;
                                this.isActive = true;
                            }

                            // A method
                            login() {
                                console.log(`${this.name} has logged in.`);
                            }

                            logout() {
                                console.log(`${this.name} has logged out.`);
                            }
                        }

                        // Create a new object (an "instance") from the class
                        const user1 = new User('Alex', 'alex@example.com');
                        user1.login(); // "Alex has logged in."
                    

Inheritance with extends

Classes can inherit properties and methods from other classes using the `extends` keyword.


                        class Admin extends User {
                            constructor(name, email, permissions) {
                                // `super()` calls the parent class's constructor
                                super(name, email);
                                this.permissions = permissions;
                            }

                            deletePost() {
                                console.log(`${this.name} has deleted a post.`);
                            }
                        }

                        const admin1 = new Admin('Jane', 'jane@example.com', ['all']);
                        admin1.login(); // Inherited from User! -> "Jane has logged in."
                        admin1.deletePost(); // "Jane has deleted a post."
                    

Mid-Lecture Knowledge Check

Module 3: Promises Deep Dive

Mastering the Asynchronous Contract

Recap: The Problem & The Solution

Backend development is full of slow operations. We cannot block our server waiting for them. Callbacks were the old solution, but led to "Callback Hell".

A Promise is an object that represents a future value, allowing us to handle asynchronous operations in a clean, chainable way.

States: pending, fulfilled, rejected

Creating a Promise

Understanding how to create a Promise is key to "promisifying" older callback-based functions.


                        function delay(ms) {
                            // The Promise constructor takes a function (the "executor")
                            // with two arguments: resolve and reject.
                            return new Promise((resolve, reject) => {
                                if (ms < 0) {
                                    // If something is wrong, call reject.
                                    return reject(new Error('Delay cannot be negative.'));
                                }

                                setTimeout(() => {
                                    // When the async operation succeeds, call resolve.
                                    resolve(`Waited for ${ms} milliseconds.`);
                                }, ms);
                            });
                        }

                        delay(1000)
                            .then(message => console.log(message)) // "Waited for 1000 milliseconds."
                            .catch(error => console.error(error));
                    

.finally(onFinally)

The `.finally()` method schedules a function to be called when the promise is settled (either fulfilled or rejected).

It's perfect for cleanup code that must run regardless of the outcome, like closing a database connection or stopping a loading spinner.


                        connectToDatabase()
                            .then(db => {
                                return db.query('SELECT * FROM users');
                            })
                            .then(results => {
                                console.log('Users found:', results);
                            })
                            .catch(error => {
                                console.error('An error occurred:', error);
                            })
                            .finally(() => {
                                // This will always run, whether it succeeded or failed.
                                console.log('Closing database connection...');
                            });
                    

Module 4: async/await - The Modern Standard

Writing Asynchronous Code Synchronously

The Final Form

`async/await` is syntactic sugar over Promises that allows us to write asynchronous code that looks and feels synchronous.

  • `async function`: Declaring a function as `async` automatically makes it return a Promise.
  • `await`: This operator pauses the execution of the `async` function, waits for a Promise to resolve, and "unwraps" its value.
  • `try...catch`: The standard way to handle errors (rejected Promises).

                        async function displayCommentsForUser(userId) {
                            try {
                                const user = await getUser(userId);
                                const posts = await getPostsForUser(user.id);
                                const comments = await getCommentsForPost(posts[0].id);

                                console.log('Comments for first post:', comments);
                            } catch (error) {
                                // Any rejected promise in the `try` block will be caught here
                                handleError(error);
                            }
                        }

                        displayCommentsForUser(1);
                     

This is the preferred syntax in all modern backend development.

In-Class Practical Exercise

The User Data Transformer

Your task is to create a function that simulates fetching user data and then transforms it using the modern ES6+ syntax we've learned.

  1. Create a file `app.js`.
  2. Create a `mockFetchUsers` function that returns a Promise resolving with an array of user objects.
  3. Create an `async` function `processUsers`.
  4. Inside `processUsers`, `await` the user data.
  5. Use .map() with an arrow function to transform the data.
  6. Inside the map, use destructuring to pull out `name` and `company`.
  7. Return a new, simpler array of objects.
  8. Call `processUsers` and log the final result.
  9. Run your script: node app.js.
Starter Code (`app.js`):

                            const mockFetchUsers = () => {
                                return new Promise(resolve => {
                                    setTimeout(() => {
                                        resolve([
                                            { id: 1, name: 'Alex Architect', company: { name: 'BuildCo' } },
                                            { id: 2, name: 'Jane Developer', company: { name: 'CodeStream' } }
                                        ]);
                                    }, 1000);
                                });
                            };

                            async function processUsers() {
                                console.log('Fetching users...');
                                const users = await mockFetchUsers();

                                const simplifiedUsers = users.map(({ name, company: { name: companyName } }) => {
                                    // Use DESTRUCTURING here to get `name` and `company`
                                    // from the `user` object.
                                    // Then, from the `company` object, get its `name` and RENAME it to `companyName`.
                                    
                                    // Return a new object: { name, companyName }
                                    return { name, companyName };
                                });

                                return simplifiedUsers;
                            }

                            processUsers().then(result => {
                                console.log('Processed Users:', result);
                                // Expected: [ { name: 'Alex Architect', companyName: 'BuildCo' }, ... ]
                            });
                         

Final Knowledge Check