Escena Viva already has routers, controllers and its own middleware. If you deployed it today it would work, and so would anyone who wanted to hurt it: it sends no security headers, it does not control which origin calls it, it does not log requests in a standard format, it compresses nothing and it does not stop a script from buying the catalog's 1,189 available tickets in a ten-second loop. In this lesson we assemble the minimum kit: five packages, one more mentioned, and for each one the same triad —what problem it solves, how it is configured and what happens if you leave it out—, all of them put through the Module 5 audit filter first.
Contents
- The Module 5 criteria applied to each package
- helmet: security headers
- cors: requests from another origin
- morgan: HTTP logging
- compression: bandwidth
- express-rate-limit: usage limits
- cookie-parser: getting ready for Module 8
- The correct order: the complete
createApplication()
- The Module 5 criteria applied to each package
Before typing npm install, the check you already know how to run:
npm view helmet version license repository.url maintainers # basic metadata
npm view helmet time.modified # a security package unpublished for three years is alarming
npm view helmet dependencies # every dependency is attack surface
npm audit # after installing: known vulnerabilitiesThe result of the analysis for this lesson's packages:
| Package | Own dependencies | Maintenance | Verdict |
|---|---|---|---|
helmet |
None | Active, recent major version | Accept |
cors |
2 (object-assign, vary) |
Stable, infrequent changes | Accept, keep an eye on it |
morgan and compression |
4 and 5, all from the Express team | Stable and active | Accept |
express-rate-limit |
None | Very active, good documentation | Accept |
cookie-parser |
2 | Stable | Accept (Module 8) |
That helmet and express-rate-limit have zero dependencies is exactly the kind of data point you were looking for in Module 5: less third-party code, less surface, less risk of a compromised maintainer ending up in your node_modules. And as always, watch out for typosquatting: the packages are called helmet (not helmetjs), cors (not express-cors) and express-rate-limit (not express-ratelimit). Install them with npm install helmet cors morgan compression express-rate-limit and check afterwards with npm ls --all --parseable | wc -l how much the tree has grown and with npm audit whether anything came with vulnerabilities.
- helmet: security headers
What problem it solves. Browsers implement a dozen defense mechanisms that only kick in if the server asks for them with headers; without them they behave in the most permissive way possible. app.use(helmet()) is enough, and these are the headers that show up:
| Header | Default value (abridged) | What it prevents |
|---|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self'; ... |
JavaScript from unauthorized origins running (the real defense against XSS) |
X-Content-Type-Options |
nosniff |
The browser guessing a file's type and interpreting as a script something that is not one |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
The browser using HTTP after the first HTTPS visit |
X-Frame-Options |
SAMEORIGIN |
Your page being loaded inside somebody else's iframe (clickjacking) |
Referrer-Policy |
no-referrer |
Leaking the full source URL to external sites |
Cross-Origin-Opener-Policy and -Resource-Policy |
same-origin |
Isolating your window from other tabs and stopping other sites from embedding your resources |
Origin-Agent-Cluster and X-DNS-Prefetch-Control |
?1 / off |
Process isolation and DNS prefetching that leaks navigation |
X-Permitted-Cross-Domain-Policies |
none |
Legacy Flash/PDF policies |
What happens if you leave it out: all those defenses stay off. None of them is essential on its own, nor does any replace validating the input, but together they turn several possible attacks into impossible ones for one line of code.
The CSP and your static front end
Here is the practical problem. helmet's default CSP includes script-src 'self', which blocks all inline JavaScript. If public/index.html has a <button onclick="buy('ses-001-1')"> or a <script> with venue constants, both will stop working and the browser console will show Refused to execute inline script because it violates the following Content-Security-Policy directive. The instinctive reaction is to disable the CSP. Do not: it is the only header on the list that really stops an XSS. The correct ways out, in order of preference: move the inline JavaScript into public/app.js and use addEventListener instead of onclick (an hour's work and it fixes the problem for good); if some inline script is unavoidable, use a per-response nonce; and only as a last resort relax the specific directive, never the whole CSP.
// Option 2: a per-request nonce, generated before helmet.
const { randomBytes } = require('node:crypto');
app.use((req, res, next) => {
res.locals.nonce = randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
// The nonce is recomputed on every response: it is not reusable.
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"], // where the front end is allowed to fetch from
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
},
// It only makes sense if you really do serve over HTTPS.
strictTransportSecurity: configuration.isProduction
? { maxAge: 31_536_000, includeSubDomains: true }
: false,
}));A pure API with no front end: if Escena Viva only served JSON, the CSP would be almost irrelevant and you could leave
helmet()at its defaults. The conflict appears because we servepublic/.
- cors: requests from another origin
What problem it solves. The browser applies the same-origin policy: a page only reads responses from its own origin (scheme + host + port).
| Page | API | Same origin? | Reason |
|---|---|---|---|
http://localhost:3000 |
http://localhost:3000 |
Yes | Identical |
http://localhost:5173 |
http://localhost:3000 |
No | Different port |
https://escenaviva.test |
http://escenaviva.test |
No | Different scheme |
https://escenaviva.test |
https://api.escenaviva.test |
No | Different host (subdomain) |
When they do not match, the browser makes the request but hides the response from the JavaScript unless the server authorizes it with Access-Control-Allow-Origin. Mind the nuance: CORS does not protect your server, it protects the browser user; a curl or a Node script ignores it entirely.
The preflight request
For "non-simple" requests —any POST with Content-Type: application/json, or with custom headers such as X-Request-Id— the browser sends an OPTIONS first, asking for permission:
OPTIONS /api/orders HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-request-id
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: content-type,x-request-id
Access-Control-Max-Age: 600And only if that response authorizes it does the browser send the real POST. Access-Control-Max-Age matters for performance: without it, the browser repeats the OPTIONS before every request.
Escena Viva's real configuration
// src/middleware/cors.js
const cors = require('cors');
const { configuration } = require('../config/index.js');
function createCors() {
return cors({
// An allowlist read from ALLOWED_ORIGINS (06-02), never '*'.
origin(origin, callback) {
// With no Origin header (curl, mobile apps, server to server) it is accepted.
if (!origin || configuration.allowedOrigins.includes(origin)) {
callback(null, true);
return;
}
const message = `Origin not allowed: ${origin}`;
callback(Object.assign(new Error(message), { appCode: 'PATH_NOT_ALLOWED' }));
},
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Accept', 'X-Request-Id', 'X-Sale-Channel'],
// exposedHeaders: the ones the browser's JavaScript will be able to READ.
exposedHeaders: ['X-Request-Id', 'RateLimit-Remaining'],
maxAge: 600,
credentials: false,
});
}origin: '*' versus an allowlist
origin: '*' |
Allowlist | |
|---|---|---|
| Who can call from a browser | Any website in the world | Only the ones you authorize |
Compatible with credentials: true |
No: the browser rejects it | Yes |
| Risk and when it is acceptable | A malicious site can read your responses using the visitor's session; only for a read-only public API with no personal data | Bounded; for everything else |
The warning about credentials. If you enable credentials: true, the browser sends cookies and authentication headers on cross-origin requests, which opens the door for another site to act on behalf of a user with an open session. Three rules if you ever need it (Module 8): credentials: true requires a specific origin, because with '*' the browser rejects the response; never dynamically echo back the received Origin without checking it against the allowlist, because that is a fake allowlist that accepts everyone; and combine it with SameSite=Lax or Strict cookies and CSRF protection. What happens if you leave it out: the public/app.js served from another port will get the classic blocked by CORS policy and will not see a single response. If in production the front end is served from the same origin as the API, CORS is not needed: that is the safest scenario.
- morgan: HTTP logging
What problem it solves. It leaves a record of every request in a standard format that analysis tools know how to read. In 06-04 you wrote your own logger; morgan does the same with established formats and custom tokens.
app.use(morgan('dev')); // development: compact and colored by status
// GET /api/events/evt-001 200 412 - 3.201 ms
app.use(morgan('combined')); // production: Combined Log Format (Apache/Nginx)
// 127.0.0.1 - - [14/Aug/2026:09:12:44 +0000] "GET /api/events HTTP/1.1" 200 812 "-" "curl/8.5.0"| Format | Content | What for |
|---|---|---|
dev |
Method, path, colored status, time | Development |
combined |
Apache format with Referer and User-Agent |
Production with classic tooling |
common / tiny |
No Referer or User-Agent / the bare minimum |
Lighter alternatives |
| Custom | Whatever tokens you define |
Structured logging (Module 11) |
Linking it to the structured logging of Module 11
In production logs are queried with tools that need one JSON object per line, not readable text; morgan allows it by defining tokens and passing your own stream:
// src/middleware/http-logger.js
const morgan = require('morgan');
const { configuration } = require('../config/index.js');
// Custom tokens: they expose data morgan knows nothing about.
morgan.token('request-id', (req) => req.requestId ?? '-');
morgan.token('channel', (req) => req.get('X-Sale-Channel') ?? '-');
// A formatter that emits one line of JSON per request.
function jsonFormat(t, req, res) {
return JSON.stringify({
timestamp: new Date().toISOString(),
requestId: t['request-id'](req, res),
method: t.method(req, res),
path: t.url(req, res),
status: Number(t.status(req, res)),
bytes: Number(t.res(req, res, 'content-length') ?? 0),
durationMs: Number(t['response-time'](req, res)),
channel: t.channel(req, res),
});
}
function createHttpLogger() {
if (!configuration.isProduction) {
return morgan('dev', { skip: (req) => req.path === '/api/health' });
}
// Diagnostics on stderr, as the course convention dictates.
return morgan(jsonFormat, { stream: { write: (line) => process.stderr.write(line) } });
}With this, morgan replaces createRequestLogger from 06-04 (keep yours if you like it better: they do the same thing, and now you understand the third-party one from the inside). The skip option keeps the logs from filling up with the orchestrator's health probes. What happens if you leave it out: when something fails in production you will have no idea what the client asked for, or when, or how long it took. Debugging blind.
- compression: bandwidth
What problem it solves. JSON compresses extraordinarily well: the full Escena Viva catalog can go from 12 KB to under 2 KB, and fewer bytes means less load time and less transit cost.
app.use(compression({
threshold: 1024, // below that it is not worth it: CPU costs more than the bytes
level: 6, // the usual balance between CPU and ratio
// Lets the client turn it off with the X-No-Compression header.
filter(req, res) {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
}));| What compresses well | What must not be compressed |
|---|---|
| JSON, HTML, CSS, JavaScript, SVG, CSV | JPEG, PNG, WebP (already compressed) |
| The occupancy reports from Module 3 | MP4, MP3, ZIP, gzip |
| Large API responses | Responses below the threshold |
Compressing what is already compressed burns CPU and sometimes increases the size. The default filter in compression already consults the MIME type table and avoids those cases.
Why it is sometimes delegated to the reverse proxy
In many deployments (Module 11) compression is done by Nginx or the CDN, not by Node. Compressing in Node works in any deployment with no external configuration and is essential if there is no proxy in front; compressing in the proxy frees CPU from the process and usually brings optimized brotli and a cache of compressed responses, but it requires control over that proxy.
A practical rule: enable it in Node by default; if later you measure that CPU is the bottleneck and there is a proxy in front, turn it off there, but never in both places at once. To check it, compare curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' localhost:3000/api/events with the same call without the header. What happens if you leave it out: nothing breaks; you simply send three or four times more bytes than necessary in every response.
- express-rate-limit: usage limits
What problem it solves. Nothing stops a script from calling POST /api/orders in a loop: with 1,189 available tickets in the catalog and one request every 20 ms, the Auditorio Ribera runs out of capacity in under half a minute. It also protects against brute force on the Module 8 login.
// src/middleware/limits.js
const { rateLimit } = require('express-rate-limit');
const body429 = (m) => ({ error: { code: 'TOO_MANY_REQUESTS', message: m, status: 429 } });
/** The general API limit: generous, it only stops obvious abuse. */
const generalLimit = rateLimit({
windowMs: 15 * 60 * 1000, // a 15-minute window
limit: 300, // 300 requests per IP and window
standardHeaders: 'draft-7', // the standard RateLimit-* headers
legacyHeaders: false, // without the old X-RateLimit-*
message: body429('You have exceeded the request limit. Try again later.'),
});
/** A strict limit for purchases: that is the operation that consumes capacity. */
const purchaseLimit = rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 5, // 5 orders per IP and minute
standardHeaders: 'draft-7',
legacyHeaders: false,
// The ones rejected for lack of capacity count too: otherwise a loop of
// failed attempts would go unlimited.
skipFailedRequests: false,
message: body429('Too many purchase attempts. Wait a minute.'),
});
// Application: general across the whole API, strict only on purchases.
api.use(generalLimit);
orderRoutes.post('/', purchaseLimit, createOrder);When it is exceeded, the client gets a 429 Too Many Requests with RateLimit-Limit: 5, RateLimit-Remaining: 0, RateLimit-Reset: 43 and Retry-After: 43. RateLimit-Remaining tells it how many requests it has left and Retry-After how many seconds to wait; a well-built client respects them instead of retrying blindly.
Two limitations you must know about now. First: the default store is the process's memory, so with several processes (the Module 10 cluster) or several instances (Module 11) each one keeps its own count and the real limit is multiplied; the solution is a shared store in Redis (Module 10). Second: it depends on req.ip, which in turn depends on trust proxy; misconfigured, either all clients share the proxy's IP and block each other, or anyone forges theirs with a header. In Module 8 we will go deeper with per-authenticated-user limits, sliding windows and progressive delays. What happens if you leave it out: a single client can exhaust the capacity, saturate the process or try passwords without limit.
- cookie-parser: getting ready for Module 8
app.use(cookieParser(configuration.gatewayKey)) parses the Cookie header and leaves the cookies on req.cookies (and the signed ones on req.signedCookies). Escena Viva does not need it yet: the API is anonymous, there is no session and no endpoint depends on who you are.
We are flagging it because in Module 8 sessions with Passport and httpOnly, secure and SameSite cookies will show up, and then this middleware will be first on the list. Installing it now "just in case" would be adding surface for nothing: the Module 5 discipline also means not installing what you do not use.
- The correct order: the complete
createApplication()
createApplication()This is the module's final code, commented line by line. The order is not arbitrary: every position has a reason.
// src/app.js — the complete module 6 version.
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const { configuration } = require('./config/index.js');
const { createCors } = require('./middleware/cors.js');
const { createHttpLogger } = require('./middleware/http-logger.js');
const { requestId } = require('./middleware/request-id.js');
const { generalLimit } = require('./middleware/limits.js');
const { createApiRoutes } = require('./routes/index.js');
// routeNotFound and errorHandler arrive in 06-07.
const { routeNotFound } = require('./middleware/not-found.js');
const { errorHandler } = require('./middleware/errors.js');
function createApplication({ serveStaticFiles = true, logging = true, rateLimiting = true } = {}) {
const app = express();
// 0. SETTINGS, before anything else: the limiter needs 'trust proxy' for req.ip.
app.disable('x-powered-by');
app.set('trust proxy', configuration.trustProxy);
app.set('case sensitive routing', true);
app.set('json spaces', configuration.isProduction ? 0 : 2);
// 1. IDENTIFIER: first, because the logger, the errors and any later
// trace are going to use it.
app.use(requestId);
// 2. HEADER SECURITY: as early as possible, so that ALL responses carry
// them, error responses and static files included.
app.use(helmet({
contentSecurityPolicy: cspPolicy(),
strictTransportSecurity: configuration.isProduction,
}));
// 3. CORS before the routes and the limits: the preflight OPTIONS must be
// answered without consuming quota.
app.use(createCors());
// 4. LOGGING: after the identifier (so it can print it) and before anything
// that might respond, so no request escapes.
if (logging) app.use(createHttpLogger());
app.use(compression({ threshold: 1024 })); // 5. COMPRESSION
// 6. LIMITS before the BODY (7): rejecting an abusive request is cheaper
// if you have not parsed 100 KB of JSON.
if (rateLimiting) app.use('/api', generalLimit);
app.use(express.json({ limit: configuration.bodyLimit }));
// 8. STATIC FILES: paths disjoint from the API's; with fallthrough, a
// nonexistent file carries on towards it.
const staticOptions = { index: 'index.html', dotfiles: 'ignore', maxAge: '1h' };
if (serveStaticFiles) app.use(express.static(configuration.publicDir, staticOptions));
app.use('/api', createApiRoutes()); // 9. THE API: the routers from 06-03.
app.use(routeNotFound); // 10. 404: matches whatever went unhandled.
app.use(errorHandler); // 11. ERRORS: ALWAYS last.
return app;
}The seven ordering rules, to memorize: identifier first (everything else references it); security early (so the headers reach errors and static files too); CORS before the limits (the preflight OPTIONS must not consume quota); limits before the body (do not spend CPU parsing what you are going to reject); body before the routes (req.body does not exist otherwise); 404 after the routes (it only catches what went unhandled); and errors last (it only sees what was registered before it). And the logging and rateLimiting flags are not a whim: in Module 9 you will set them to false so the tests do not fill the output with logs or fail when they hit the limit on request number 301.
Common Mistakes and Tips
- Disabling the whole CSP because it breaks the front end. That is throwing away the only real defense against XSS. Move the inline JavaScript into a file or use nonces.
origin: '*'withcredentials: true. The browser rejects the combination. And if you "fix it" by echoing back the receivedOriginwithout checking it, you have built an allowlist that accepts everybody.- Believing that CORS protects the server. It protects the browser user; real authorization arrives in Module 8.
- Putting the limiter after
express.json()(you parse 100 KB before rejecting) or using it with in-memory state and several processes (with four workers the real limit is quadruple; Redis in Module 10). - Compressing images or video, or compressing in Node and in the proxy at the same time. You burn CPU to gain no bytes: leave the default
filterand pick a single place. - Installing
cookie-parserwithout using cookies. Free surface: install it when you need it. morgan('dev')in production. The color codes are escape sequences that dirty the log files; usecombinedor your JSON formatter.- Tip: add
npm audit --audit-level=highto thecheckscript. Every one of these packages is third-party code that runs on every request.
Exercises
Exercise 1: audit before installing
For the five packages in this lesson, build a table with: current version, license, date of the last publish, number of direct dependencies and whether it shows up in npm audit. Decide with reasons whether you would accept each one in a real project and what alternative you would look for if any of them had gone two years without a publish.
Exercise 2: CORS with two origins
Configure cors so that it accepts http://localhost:5173 and https://escenaviva.test and rejects the rest. Prove the three cases with curl: the OPTIONS preflight request from an allowed origin (it must return the Access-Control-Allow-* headers), a GET from a disallowed origin (it must not carry Access-Control-Allow-Origin) and a GET with no Origin header (it must work normally).
Exercise 3: the limit that saves the capacity
Apply purchaseLimit to POST /api/orders with 5 requests per minute. Write a script that fires 8 back-to-back orders of 1 ticket for ses-001-1 and prints the status and the RateLimit-* headers of each one. Check that the last three return 429 with Retry-After and that the body respects the { error: { code, message, status } } format.
Solutions
Solution 1
for p in helmet cors morgan compression express-rate-limit; do
echo "== $p"; npm view "$p" version license time.modified dependencies
done && npm audit --audit-level=moderateThe criteria you should have applied: a permissive license, a publish in the last 12-18 months, few direct dependencies and recognizable maintainers. If cors had gone two years without a publish, the reasonable answer is not to abandon it —it is a small, stable package— but to keep an eye on its open issues and be ready to replace it with twenty lines of your own middleware, because CORS is ultimately four headers.
Solution 2
# 1. Preflight from an allowed origin -> 204 with
# Access-Control-Allow-Origin, -Allow-Methods and -Max-Age.
curl -si -X OPTIONS http://localhost:3000/api/orders -H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type'
# 2. GET from a DISALLOWED origin: the grep returns nothing, so the
# browser would hide the response.
curl -si http://localhost:3000/api/events -H 'Origin: https://malicious.test' \
| grep -i 'access-control-allow-origin'
# 3. With no Origin header (plain curl, server to server): 200.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/eventsThe third case proves the key point: CORS is a browser defense, not a server one.
Solution 3
// try-limit.js
const ORDER = { sessionId: 'ses-001-1', quantity: 1, email: '[email protected]', channel: 'web' };
async function main() {
for (let attempt = 1; attempt <= 8; attempt += 1) {
const r = await fetch('http://localhost:3000/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ORDER),
});
console.log(JSON.stringify({
attempt,
status: r.status,
remaining: r.headers.get('RateLimit-Remaining'),
retryAfter: r.headers.get('Retry-After'),
}));
}
}
main().catch((error) => {
console.error('[try-limit]', error.message);
process.exitCode = 1;
});Expected output: attempts 1 to 5 return 201 with RateLimit-Remaining dropping from 4 to 0; attempts 6, 7 and 8 return 429 with Retry-After: 52. Without the limiter, all eight would have sold a ticket; with ses-001-1's capacity, an unchecked loop exhausts it in seconds.
Conclusion
Escena Viva now ships with the minimum kit of a production API. helmet adds a dozen security headers for one line, with the important caveat that its CSP forces you to move the inline JavaScript out of the front end —and that is the correct solution, not disabling it. cors authorizes public/app.js to call the API from another origin via an allowlist read from the configuration, never with '*', and you now know what a preflight request is and why credentials deserves respect. morgan logs every request in a standard format and, with custom tokens, as one JSON object per line ready for Module 11. compression trims the size of responses when it pays off. And express-rate-limit stops a script from exhausting the capacity, with the warning that its in-memory store does not survive several processes.
Above all, you have seen the complete version of createApplication() with the seven ordering rules reasoned out: identifier first, security early, CORS before the limits, limits before the body, body before the routes, 404 after the routes and errors last. But one very large hole remains: POST /api/orders still does recordOrder(req.body) with whatever comes in the body —a quantity of -5, a sessionId that is an object, a 40,000-character e-mail address— and no middleware in this lesson looks at the content of the data.
In the next lesson, Input Data Validation, we close that hole: why you never trust the client even if the form validates, where validation must live (at the edge, not in the domain), the zod schemas for Escena Viva's orders, a generic validate(schema, source) middleware that leaves the result on req.validatedData —and that respects req.query being read-only—, and how to return a 400 that genuinely helps the caller without revealing what it should not.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
