The Syntax of Modern Backend Development
Writing Cleaner, More Powerful Code
A more concise syntax for writing functions. They are essential in modern JavaScript.
const add = function(a, b) {
return a + b;
};
const add = (a, b) => {
return a + b;
};
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().
A powerful syntax for extracting values from arrays or properties from objects into distinct variables.
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.
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
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.
They use the same syntax (...) but do opposite things based on where they are used.
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' }
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
Blueprints for Objects
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."
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."
Mastering the Asynchronous Contract
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
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));
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...');
});
Writing Asynchronous Code Synchronously
`async/await` is syntactic sugar over Promises that allows us to write asynchronous code that looks and feels synchronous.
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.
Your task is to create a function that simulates fetching user data and then transforms it using the modern ES6+ syntax we've learned.
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' }, ... ]
});