Skip to main content

NodeJS-Express

  • The “front end” denotes the interface that a web user interacts with - what they see (and hear) when using the web.
  • The “back end”, meanwhile, denotes all that goes on “behind the scenes” on web servers to make the user experience possible.

I. NodeJS

Node is an asynchronous event driven JavaScript runtime.

  • When JavaScript was first created, it was designed to run in the browser.
  • Node lets it run anywhere (your machine, a server)

Node adds capabilities JS didn't have in the browser:

  • Read/write local files
  • Create HTTP connections
  • Listen to network requests
Example
  • Browser JS:
// Change a button's text when clicked
document.querySelector("button").addEventListener("click", () => {
document.querySelector("h1").textContent = "Hello!";
});
  • Node.js — things only possible outside the browser
// Create a server that listens for requests
const http = require("http");
http.createServer((req, res) => {
res.end("Hello from the server!");
}).listen(3000);

Async / Event-driven

Node doesn't wait for slow tasks (file I/O, DB queries) — it starts them and moves on

  • When a task finishes, it fires an event and runs the next function (callback)
  • Same idea as addEventListener in frontend JS, but for server-side events
  • Result: can handle many things at once without blocking
- Synchronous: Read file → wait → done → query DB → wait → done
- Node (async): Read file + query DB at the same time → handle whichever finishes first

III. Core Node Modules

1. Running Scripts

  • Node lets you run any JS file directly in the terminal (no browser needed)
  • node myfile.js → executes the file and prints output to your terminal
  • This is the most basic thing Node adds: run JS like a program, not a webpage

2. HTTP Module — creating a local server

  • When you write frontend code, you're used to just opening an HTML file in the browser
  • In backend dev, you need an actual server running that "listens" for requests and sends back responses
    • http.createServer() sets up that server
    • .listen(3000) tells it which port to watch — port 3000 is just a common convention for local dev
  • While the script is running, anyone who visits http://localhost:3000 triggers your callback
    • "localhost" means your own machine — this server is only accessible to you locally, not the internet
    • In production, you'd deploy this to a real server with a real domain
Example:
  • req = the incoming request (who's asking, what page they want)
  • res = your response back to them
const http = require("http");
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html" }); // 200 = OK status code
res.end("Hello World!"); // what gets sent back to the browser
}).listen(3000);`

3. File System (fs) Module — reading and writing files

  • Browser JS cannot touch your filesystem for security reasons — Node can
    • fs.readFile() reads a file and gives you the contents inside a callback (async!)
    • fs.writeFile() creates or overwrites a file on disk
    • The "utf8" argument tells Node to give you the file as readable text, not raw bytes
fs.readFile("notes.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data); // prints file contents to terminal
});

fs.writeFile("notes.txt", "new content", (err) => {
if (err) throw err;
console.log("File saved!");
});

4. URL Module — parsing a URL into readable parts

  • When a request comes into your server, you need to know what page they're asking for
  • The URL module breaks a full URL string into useful pieces you can work with
const url = new URL("https://example.com/about?name=cam");
url.pathname // "/about" → which page they want
url.searchParams.get("name") // "cam" → query string values

5. Events / EventEmitter — creating your own custom events

  • Node has a built-in EventEmitter class you can use to create, fire, and listen for your own events
  • .on("eventName", callback) → listen for an event (same idea as addEventListener in the browser)
  • .emit("eventName") → manually trigger/fire that event

In real apps you won't use EventEmitter directly that often, but understanding it helps you understand how Node itself works under the hood

Example:
const EventEmitter = require("events");
const emitter = new EventEmitter();

emitter.on("greet", () => console.log("Hello!")); // set up the listener
emitter.emit("greet"); // fire it → prints "Hello!"

Note: require() is the older Node way to import modules — same idea as import in modern JS. You'll see both in the wild.

II. Express

Express is a minimal, unopinionated backend framework built on top of Node.js

  • Writing a server with raw Node's http module gets verbose fast
  • Express wraps all of that and gives you a cleaner, simpler way to handle requests
  • "Unopinionated" = it doesn't force you to structure your app any particular way

1. Setting Up

npm init -y        # creates package.json
npm install express
// app.js
const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Hello, world!"));

const PORT = process.env.PORT || 3000; // use env variable, fallback to 3000
app.listen(PORT, (error) => {
if (error) throw error;
console.log(`Listening on port ${PORT}!`);
});
  • express() initializes your server and stores it in app
  • app.get() defines a route — more on this below
  • app.listen() opens the door on that port, same idea as Node's .listen(3000)
  • process.env.PORT is how you read environment variables — useful so you don't hardcode the port

2. How a Request Travels Through Express

When a browser visits http://localhost:3000/:

  1. Browser sends a GET request to the / path
  2. Express receives it and stores it in a request object (req)
  3. Express passes it through a chain of middleware functions
  4. The first route that matches the HTTP verb + path handles it
  5. That route sends back a response (res) and the cycle ends

Visiting any URL in a browser is always just sending a GET request to that path. https://theodinproject.com/paths = GET request to /paths at theodinproject.com

3. Routes — matching requests to handlers

  • A route matches an incoming request by HTTP verb (GET, POST, PUT, DELETE) + path
  • Order matters — Express matches the first route that fits, top to bottom
app.get("/messages", (req, res) => res.send("GET messages"));
app.post("/messages", (req, res) => res.send("POST message"));

a. Route Parameters

  • Dynamic segments in the path, prefixed with :
  • Express populates req.params automatically
// GET /odin/messages → req.params = { username: "odin" }
app.get("/:username/messages", (req, res) => {
console.log(req.params.username); // "odin"
});

b. Query Parameters

  • Optional key-value pairs after ? in the URL — not part of the path itself
  • Express populates req.query automatically
// GET /messages?sort=date&direction=asc
// req.query = { sort: "date", direction: "asc" }

Real example you've seen: youtube.com/watch?v=abc123&t=60/watch is the path, v and t are query params

c. Routers — organizing routes into files

  • In a real app with many routes, you split them into separate files by resource
  • Each file exports a Router, mounted in app.js under a base path
// routes/authorRouter.js
const { Router } = require("express");
const authorRouter = Router();

authorRouter.get("/", (req, res) => res.send("All authors"));
authorRouter.get("/:authorId", (req, res) => res.send(`Author: ${req.params.authorId}`));

module.exports = authorRouter;
// app.js
app.use("/authors", authorRouter); // all /authors/* requests go here
app.use("/books", bookRouter);

In your codebase: backend/src/app/router.ts mounts all 32 collection routers — same pattern exactly

4. Controllers & Middleware

a. Middleware

  • Any function that sits between the request coming in and the response going out
  • Signature: (req, res, next) — call next() to pass to the next function in the chain
  • If you don't call next() and don't send a response, the request just hangs
function logger(req, res, next) {
console.log(`${req.method} ${req.path}`);
next(); // pass control forward
}
app.use(logger); // runs on every request

Common middleware uses:

  • Auth checks (is this user logged in?)
  • Request validation (is the body shaped correctly?)
  • Logging, CORS, JSON parsing

b. Controllers

  • Controllers are just functions that handle a specific route — they're also middleware, but their job is to be the final handler that sends the response
  • Named by convention: getAuthorById, createStory, deleteEpisode
// controllers/authorController.js
async function getAuthorById(req, res) {
const { authorId } = req.params;
const author = await db.getAuthorById(Number(authorId));

if (!author) {
res.status(404).send("Author not found");
return; // must return — sending a response doesn't stop the function
}

res.send(`Author: ${author.name}`);
}
module.exports = { getAuthorById };
// routes/authorRouter.js — wire the controller to the route
const { getAuthorById } = require("../controllers/authorController");
authorRouter.get("/:authorId", getAuthorById);

In your codebase: every collection has a {name}.controller.ts — these are exactly this pattern

c. Response Methods

MethodUse
res.json(data)Send JSON — use this for APIs
res.send(data)General purpose, auto-detects type
res.status(404).send(...)Set status code + send — must chain
res.redirect("/path")Redirect client to another URL

d. Error Handling

  • Wrap async controllers in try/catch, or just throw — Express v5 auto-catches
  • A special error middleware with 4 params (err, req, res, next) catches all bubbled errors
  • Place it at the very end of app.js
// global error handler — must have 4 params
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).send(err.message);
});
  • You can create custom error classes to carry a status code:
class NotFoundError extends Error {
constructor(message) {
super(message);
this.statusCode = 404;
}
}
// then in a controller:
throw new NotFoundError("Author not found"); // bubbles up to error handler

e. The next function

CallWhat happens
next()Pass to next middleware
next(error)Skip to error handler middleware

f. Folder Structure

express-app/
├─ routes/ → routers (one per resource)
├─ controllers/ → handler functions
├─ errors/ → custom error classes
├─ app.js → server setup, mounts routers, error handler at bottom