Loop Methods

JavaScript gives us a set of built-in array methods that are cleaner than writing a traditional for loop. Rather than manually managing an index and iterating over an array, these methods let us describe what we want to do with the data, rather than how to loop over it.

Rules

  • Use loop methods over for loops.
  • Use map() over .forEach() if you need data back.
  • Only use for loops for finger counting.

map()

map() transforms every element in an array 1:1, returning a new array of the same length. Here we double every number. Notice how the commented-out for loop achieves the same thing but requires far more code.

const numbers = [1, 2, 3, 4, 5];

// The for loop way (avoid this)
// for(let i = 0; i < numbers.length; i++){
//     const multipliedNumbers = []
//     multipliedNumbers.push(numbers[i] * 2)
// }

const doubledNumbers = numbers.map((number) => {
  return number * 2;
});
// [2, 4, 6, 8, 10]

map() also works on arrays of objects. Here we update the difficulty of every balloon animal except the Koala, first with a full function, then as a one-liner using a ternary operator.

const balloonAnimals = [
  { type: 'Koala', difficulty: 5.0 },
  { type: 'Dog', difficulty: 2.5 },
  { type: 'Giraffe', difficulty: 1.0, isTall: true },
];

// Full version
const updatedBalloonAnimals = balloonAnimals.map((balloonAnimal) => {
  if (balloonAnimal.type !== 'Koala') {
    balloonAnimal.difficulty = 3.0;
  }
  return balloonAnimal;
});

// One-liner using ternary
const oneLinerUpdatedBalloonAnimals = balloonAnimals.map((balloonAnimal) => ({
  difficulty: balloonAnimal.type !== 'Koala' ? 3.0 : balloonAnimal.difficulty,
  ...balloonAnimal,
}));

Each callback in map() has access to three arguments: the current element, its index, and the original array.

numbers.map((element, index, originalArray) =>
  console.log(element, index, originalArray)
);

URL Structure

A URL is made up of several distinct parts, each serving a specific purpose. Understanding the structure helps us build better routes and query strings in our API.

https://www.google.com/search?q=cool+search

PartValue
Protocolhttps
Protocol Identifier://
Subdomainwww
Domaingoogle
Top Level Domain.com
Path/search
Query String?q=cool+search

JSON & Express

Setup

The tagline for Express is "Fast, unopinionated, minimalist web framework for Node.js". This is quite literal: the default Express configuration has no built-in way to parse JSON from a request body.

Because of that, we need to register express.json() as middleware, which sits between the incoming request and our route handlers and parses the JSON body for us.

const express = require('express');
const app = express();

// Required to parse JSON bodies.
app.use(express.json());

Complete CRUD

CRUD stands for Create, Read, Update and Delete. Together they map directly to HTTP methods: POST creates, GET reads, PATCH and PUT update, and DELETE removes.

Below is a full implementation built in class, with ID encryption added independently.

GET all

app.get('/movies', (req, res) => {
  console.log('Fetching all movie ressources');

  // we do not use res.json, ever
  const encryptedMovies = movies.map(movie => ({
    ...movie,
    id: encryptId(movie.id)
  }));

  res.send({ data: encryptedMovies });
});

GET by ID

app.get('/movies/:id', (req, res) => {
  const providedMovieId = Number(
    decryptId(req.params.id)
  );
  const foundMovie = movies.find(
    (movie) => movie.id === providedMovieId
  );

  if (!foundMovie) {
    return res.status(404).send({
      errorMessage:
        `No movie found with id ${req.params.id}`
    });
  }

  res.send({
    data: {
      ...foundMovie,
      id: encryptId(foundMovie.id)
    }
  });
});

POST

app.post('/movies', (req, res) => {
  if (!req.body.title) {
    return res.status(400).send({
      errorMessage: 'JSON body must be provided'
    });
  }

  const providedMovie = req.body;
  providedMovie.id = nextId++;
  movies.push(providedMovie);

  res.send({ data: providedMovie });
});

PATCH

// TODO: Rewrite to Anders' version
app.patch('/movies/:id', (req, res) => {
  const providedMovie = movies.find(
    (movie) => movie.id ===
      Number(decryptId(req.params.id))
  );

  if (!providedMovie) {
    return res.status(404).send({
      errorMessage:
        `No movie found with id ${req.params.id}`
    });
  }

  if (req.body.title)
    providedMovie.title = req.body.title;
  if (req.body.description)
    providedMovie.description = req.body.description;

  res.send({
    data: {
      ...providedMovie,
      id: encryptId(providedMovie.id)
    }
  });
});

PUT

app.put('/movies/:id', (req, res) => {
  const movieIndex = movies.findIndex(
    (movie) => movie.id ===
      Number(decryptId(req.params.id))
  );

  if (movieIndex === -1) {
    return res.status(404).send({
      errorMessage:
        `No movie found with id ${req.params.id}`
    });
  }
  if (!req.body.title) {
    return res.status(400).send({
      errorMessage: 'ID and title are required'
    });
  }

  movies[movieIndex] = {
    id: Number(decryptId(req.params.id)),
    title: req.body.title,
    description: req.body.description
  };

  res.send({
    data: {
      ...movies[movieIndex],
      id: encryptId(movies[movieIndex].id)
    }
  });
});

DELETE

app.delete('/movies/:id', (req, res) => {
  const providedMovieId = Number(
    decryptId(req.params.id)
  );
  const movieIndex = movies.findIndex(
    (movie) => movie.id === providedMovieId
  );

  if (movieIndex === -1) {
    return res.status(404).send({
      errorMessage:
        `No movie found with id ${req.params.id}`
    });
  }

  const deletedMovie = movies.splice(movieIndex, 1)[0];
  res.send({
    data: deletedMovie,
    message: "has been deleted"
  });
});

Understanding PATCH vs PUT

Confusing PATCH and PUT can be detrimental when working with REST APIs, as using the wrong one could overwrite data you didn't intend to change. There is also a bandwidth consideration: PATCH only sends the fields that need updating, whereas PUT requires the entire object to be sent with every request. On large objects or high traffic, this difference in payload size becomes significant.

But for our example, say we have a movie object with a title and a description. If we only want to update the title, we use PATCH: we only send the fields we want to change. If we use PUT, we replace the entire object, meaning all fields must be provided, otherwise they will be overwritten with empty values.

So in the end, choosing between the two comes down to intent: use PATCH when updating partially, and PUT when replacing objects entirely.


Status Codes

HTTP status codes are standardized numbers the server sends back to the client, indicating whether a request succeeded, failed, or something in between. Below are the most common ones. For a deeper dive, visit the wiki page

CodeMeaning
200OK
201Created
400Bad Request
404Not Found
500Server Error

XSS Prevention

Cross-Site Scripting, or XSS, is an attack where malicious scripts are injected into a page through user input, allowing an attacker to run malicious JavaScript code in another user's browser.

innerHTML is used to insert user-provided content, but the browser will also parse and execute any script tags within it. Using textContent instead treats the input as plain text, neutralizing the attack.

// Unsafe - can be used to execute scripts
div.innerHTML = userInput;

// Safe - treats as text only
div.textContent = userInput;

ID Encryption

Uses the crypto module to encrypt IDs with AES-256-CBC. Encrypting IDs before sending them to the client can prevent users from guessing sequential IDs and accessing unauthorized resources.

This was a misunderstanding. It was not part of the curriculum, however it taught me some lessons in encrypting IDs as a security measure. Insecure Direct Object Reference, or IDOR, is a vulnerability where an application exposes internal identifiers directly in the URL, allowing a user to increment the value and access resources they shouldn't.

In OWASP it is listed under Broken Access Control, which is ranked as the #1 vulnerability in the Top 10.

Setup

const crypto = require('crypto');
const SECRET = process.env.SECRET_KEY;

const algorithm = 'aes-256-cbc';

// Converts secret to 32 bytes for the algorithm
const key = crypto.scryptSync(SECRET, 'salt', 32);

// 16-bit initialization vector for the algorithm
const iv = Buffer.alloc(16, 0);

// To create a 32 char key:
// const SECRET =
//   crypto.randomBytes(32).toString('hex');
// console.log(SECRET);

encryptId

function encryptId(id) {
  const cipher = crypto.createCipheriv(
    algorithm, key, iv
  );
  let encrypted = cipher.update(
    String(id), 'utf-8', 'hex'
  );
  encrypted += cipher.final('hex');
  return encrypted;
}

decryptId

function decryptId(encrypted) {
  try {
    const decipher = crypto.createDecipheriv(
      algorithm, key, iv
    );
    let decrypted = decipher.update(
      encrypted, 'hex', 'utf-8'
    );
    decrypted += decipher.final('utf-8');
    return parseInt(decrypted);
  } catch (error) {
    // Return null if decryption fails
    return null;
  }
}

module.exports = { encryptId, decryptId };

Nodemon

Installation & Usage

npm install --save-dev nodemon
nodemon app.js

This npm package auto-restarts the server on file changes. This is great for development, because it means that we don't have to manually restart the server every time we want to see a change, it is very gratifying to see instant feedback like this.

We don't want to use this for deployment though, it adds unnecessary overhead because it constantly needs to monitor the code for changes and it can cause unexpected restarts, if anything touches a file, such as logging, temp files etc. Nodemon might register that as a change and abruptly restart the server, causing potential data loss.