Lesson 2 of 5
Node.js internals and Express structure
3 min read
What makes Node different
Node runs JavaScript on a single main thread with an event loop, and hands I/O (network, disk, DNS) to the operating system or a small thread pool (libuv). While a database query is in flight, Node serves other requests. That's why one Node process handles thousands of concurrent connections, as long as you never block the thread.
Blocking things to avoid on a request path: fs.readFileSync, heavy loops, synchronous crypto (bcrypt.hashSync), giant JSON.parse. For CPU-heavy work use worker threads, a job queue, or another service.
Event loop phases (Node specific)
timers (setTimeout) → pending callbacks → poll (I/O) → check (setImmediate) → close callbacks. Between each phase, Node runs process.nextTick callbacks first, then promise microtasks. Asked often: process.nextTick runs before promise callbacks, and setImmediate runs after I/O in the check phase.
Scaling across cores
One Node process uses one core. Use the cluster module, PM2 in cluster mode, or (most commonly) run several containers behind a load balancer.
A clean Express app
import express from "express";
const app = express();
app.use(express.json({ limit: "1mb" }));
app.get("/health", (req, res) => res.json({ ok: true }));
app.use("/api/jobs", jobsRouter);
// 404 for unknown routes
app.use((req, res) => res.status(404).json({ error: { code: "NOT_FOUND" } }));
// central error handler: 4 arguments
app.use((err, req, res, next) => {
const status = err.status ?? 500;
if (status >= 500) console.error(err);
res.status(status).json({ error: { code: err.code ?? "INTERNAL", message: status >= 500 ? "Something went wrong" : err.message } });
});
app.listen(process.env.PORT ?? 3000);Middleware
Middleware are functions (req, res, next) that run in order: parse JSON, log, authenticate, rate-limit, validate. Call next() to continue or send a response to stop. Pass an error with next(err) to jump to the error handler. In Express 5, a rejected promise in an async handler is forwarded automatically; in Express 4, wrap async handlers or errors are lost.
Layered structure
routes/ URL → controller (thin)
controllers/ read request, call service, shape response
services/ business rules ("a student can apply once per job")
repositories/ database queries
middlewares/ auth, validation, errorsKeeping business logic out of route handlers makes it testable and reusable from jobs and scripts.
Validation
Never trust input. Validate type, length, format and allowed values at the edge, with a schema library:
import { z } from "zod";
const ApplySchema = z.object({ coverNote: z.string().trim().max(2000), resumeId: z.number().int().positive() });
router.post("/:id/applications", auth, async (req, res) => {
const body = ApplySchema.parse(req.body); // throws → error handler → 400
const app = await applications.create(req.user.id, Number(req.params.id), body);
res.status(201).json(app);
});Configuration and logging
- Config from environment variables (12-factor); never commit secrets.
- Structured logs (JSON with request id, user id, duration) using pino or winston; no
console.lognoise. - Graceful shutdown: on SIGTERM stop accepting requests, finish in-flight ones, close the DB pool.
Check yourself
- Why is
readFileSyncdangerous inside a request handler? - What does
next(err)do in Express? - Why split controllers and services?
Sign up free to save your progress.