The Escena Viva User model has been sitting there since lesson 07-02 with its email, its name and its role — attendee, organizer, administrator — and no password field at all. That was not an oversight: it was a reserved gap. In this lesson we fill it with the rigor it deserves, because password storage is the one place where a mistake goes unnoticed until it becomes a newspaper headline.
We will see why a fast hash is useless, what bcrypt actually does, how to choose its cost factor by measuring, and how to write POST /auth/register and POST /auth/login without leaking which e-mails exist or how long each branch of the code takes. We will finish with what is most often done badly: verification and recovery tokens.
Contents
- Why passwords are never stored in the clear
- What a password hash is
- bcrypt, scrypt and Argon2
- bcrypt in practice
- The native alternative:
crypto.scrypt - Completing the
Usermodel - Password policy and the zod schema
POST /auth/registerand user enumerationPOST /auth/loginand constant time- Attempt limiting and logging
- E-mail verification and recovery
- Common mistakes, exercises and conclusion
- Why passwords are never stored in the clear
Imagine the escena_viva database leaks: a badly permissioned backup, a SQL injection, a disgruntled employee. If there is a password column holding the raw text, three things happen: every account is compromised, including those of organizers and administrators; accounts on other services are compromised too, because people reuse passwords (Lucía's e-mail and her password get tried automatically on dozens of sites: that is credential stuffing); and the passwords remain valid until each person changes them one by one.
Encrypt them with a key? No good either. Encryption is reversible by design: there is a key that returns the original, that key lives on the server, and whoever steals the database can usually steal the server too. Besides, there is no legitimate reason to recover a password: the server only needs to check whether the one just supplied is the right one. What about a hash like MD5 or SHA-256? It is irreversible, yes, but far too fast:
| Algorithm | Attempts/second (GPU) | Dictionary of 10^10 candidates |
|---|---|---|
| MD5 / SHA-256 | ~10^11 / ~10^10 | seconds / minutes |
| bcrypt cost 12 | ~10^3 | thousands of years |
Speed is a virtue for verifying files and a disaster for passwords: what makes the server fast makes the attacker fast, and the attacker has GPUs. Add that an unsalted hash of a common password is precomputed in public rainbow tables (5f4dcc3b5aa765d61d8327deb882cf99 is password in any search engine), and that two users with the same password would produce identical hashes, which is a leak in itself.
- What a password hash is
A password hash (or key derivation function) is designed with three deliberate properties:
- It is slow on purpose: on the order of 100–500 ms. The legitimate user does not care; the attacker trying millions of combinations is ruined by it.
- It uses a unique salt per user: a random string stored alongside the hash. It defeats rainbow tables and makes identical passwords produce different hashes. The salt is not secret: its job is uniqueness.
- It has an adjustable cost factor: hardware improves every year, the parameter is raised, and the algorithm keeps serving for a decade.
- The best ones add memory cost, so that throwing lots of GPU cores at it in parallel is not enough.
- bcrypt, scrypt and Argon2
| Algorithm | CPU cost | Memory cost | Parameters | Notes |
|---|---|---|---|---|
| bcrypt (1999) | Yes | Low and fixed (4 KB) | logarithmic cost |
Very mature. 72-byte limit on the input |
| scrypt (2009) | Yes | Yes, adjustable | N, r, p |
In node:crypto, no dependencies |
| Argon2id (2015) | Yes | Yes, adjustable | time, memory, parallelism | Winner of the Password Hashing Competition; the best option today |
A reasoned recommendation: if you are starting today and can add the argon2 dependency, use Argon2id, which resists specialized hardware better thanks to its memory cost; if you need zero native dependencies, crypto.scrypt already ships with Node and is perfectly defensible. In Escena Viva we use bcrypt because it is what you will find in the vast majority of existing code, its API explains itself without noise, and its security is adequate with a well-chosen cost. What matters is the model; switching algorithms will be a change confined to a single module, and that is how we will write it. Never acceptable: MD5, SHA-1, "salted SHA-256" or any homemade invention based on concatenating and rehashing.
- bcrypt in practice
bcrypt compiles a native module; if that complicates your deployment (minimal containers, serverless functions), bcryptjs is the pure JavaScript implementation, API-compatible and about three times slower.
// src/services/passwords.js
'use strict';
const bcrypt = require('bcrypt');
const { configuration } = require('../config/index.js');
const { ValidationError } = require('../errors.js');
// bcrypt's usable limit: 72 bytes. We reject before hashing so we never
// truncate silently (two long passwords sharing their first 72 bytes
// would be considered identical).
const MAX_BYTES = 72;
async function hashPassword(plainPassword) {
if (Buffer.byteLength(plainPassword, 'utf8') > MAX_BYTES) {
throw new ValidationError('The password exceeds the maximum allowed length');
}
// The cost lives in configuration: it is raised without touching this file.
return bcrypt.hash(plainPassword, configuration.bcryptCost);
}
async function verifyPassword(plainPassword, storedHash) {
if (!storedHash) { return false; }
// compare extracts the salt and the cost from the hash itself, and
// compares in constant time: it does not stop at the first differing byte.
return bcrypt.compare(plainPassword, storedHash);
}
module.exports = { hashPassword, verifyPassword, MAX_BYTES };A bcrypt hash explains itself:
$2b$12$N9qo8uLOickgx2ZMRZoMye.IjZAgcfl7p92ldGxad68LJZdL17lhWy │ │ └──────── salt (22 chars) ─────┘└──── hash (31 chars) ───┘ │ └── cost factor: 12 └── algorithm variant
That is why bcrypt.compare does not need you to pass the salt or the cost: they are inside the hash. And that is why raising the cost does not invalidate old hashes: each one is verified with its own.
The cost factor is logarithmic: 12 means 2^12 = 4096 iterations and each point doubles the time. Rule of thumb: the largest value that keeps login below roughly 250 ms on your production hardware. Measure it, do not copy it:
// scripts/measure-bcrypt.js
'use strict';
const bcrypt = require('bcrypt');
(async () => {
for (let cost = 10; cost <= 15; cost += 1) {
const start = process.hrtime.bigint();
await bcrypt.hash('escena-viva-test-password', cost);
console.log(`cost ${cost}: ${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(0)} ms`);
}
})();
// node scripts/measure-bcrypt.js
// cost 10: 62 ms | cost 11: 124 ms | cost 12: 248 ms (chosen) | cost 13: 495 msWhy constant-time comparison matters. In Module 3, with Buffers, we met crypto.timingSafeEqual: an ordinary comparison (===) stops at the first differing byte, so the elapsed time reveals how many bytes matched, and by repeating the measurement thousands of times a secret is reconstructed byte by byte. bcrypt.compare always compares the full 60 characters; the same logic will apply to the tokens in section 11.
- The native alternative:
crypto.scrypt
crypto.scrypt// src/services/passwords-scrypt.js (dependency-free alternative)
'use strict';
const { randomBytes, scrypt, timingSafeEqual } = require('node:crypto');
const scryptAsync = require('node:util').promisify(scrypt);
// N=2^15 demands about 32 MB per hash: that makes a GPU attack expensive,
// since GPUs have plenty of compute and little memory.
const P = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
async function hashPassword(plainPassword) {
const salt = randomBytes(16);
const derived = await scryptAsync(plainPassword, salt, 64, P);
// We store parameters + salt + hash: the format must describe itself,
// so old hashes keep verifying if we raise N tomorrow.
return `scrypt$${P.N}$${P.r}$${P.p}$${salt.toString('base64')}$${derived.toString('base64')}`;
}
async function verifyPassword(plainPassword, storedHash) {
const [label, n, r, p, saltB64, hashB64] = String(storedHash).split('$');
if (label !== 'scrypt') { return false; }
const expected = Buffer.from(hashB64, 'base64');
const derived = await scryptAsync(plainPassword, Buffer.from(saltB64, 'base64'),
expected.length, { N: Number(n), r: Number(r), p: Number(p), maxmem: P.maxmem });
return timingSafeEqual(derived, expected); // mandatory: we compare by hand (M3)
}
module.exports = { hashPassword, verifyPassword };Notice the detail almost nobody includes: storing the parameters next to the hash, which is exactly what bcrypt does for you. Without them, raising N would invalidate every previous hash.
- Completing the
User model
User model// src/models/user.js
'use strict';
const mongoose = require('mongoose');
const ROLES = Object.freeze(['attendee', 'organizer', 'administrator']);
const userSchema = new mongoose.Schema({
// Always normalize: '[email protected]' and '[email protected]'
// must be the SAME account, or the unique index is worthless.
email: { type: String, required: true, unique: true, lowercase: true, trim: true, index: true },
name: { type: String, required: true, trim: true, maxlength: 120 },
// select: false => NEVER comes back from a query unless asked for with
// .select('+passwordHash'): it shields us from "we returned the whole user".
passwordHash: { type: String, required: true, select: false },
role: { type: String, enum: ROLES, default: 'attendee', required: true },
verified: { type: Boolean, default: false },
assignedVenue: { type: String, default: null }, // organizers only
createdAt: { type: Date, default: () => new Date() },
});
// Belt and braces: even if somebody asks for +passwordHash, it disappears on serialization.
userSchema.set('toJSON', {
transform: (doc, plain) => { delete plain.passwordHash; delete plain.__v; return plain; },
});
const User = mongoose.model('User', userSchema);
module.exports = { User, ROLES };select: false turns "do not leak the hash" into the default behavior instead of a discipline you have to remember; the only place that will ask for it explicitly is the login. lowercase: true avoids the subtle bug of ending up with two accounts for the same e-mail: technically the local part is case-sensitive, but no real provider exploits that, and normalizing is what users expect.
- Password policy and the zod schema
For twenty years the rule was "8 characters with an uppercase letter, a digit and a symbol". NIST changed its mind (SP 800-63B) because the data showed those rules produce Summer2024! everywhere: short, predictable passwords that are hard to remember and end up written on a sticky note.
| Modern rule | Discarded old rule |
|---|---|
| Minimum 12 characters, generous maximum; spaces and Unicode allowed (passphrases) | Minimum 8 with composition rules; ban "odd" characters |
| Check against lists of breached passwords | Mandatory expiry every 90 days |
| Forced change only on suspicion | Ban reusing the last 5 |
// src/schemas/authentication.js
'use strict';
const { z } = require('zod');
const { MAX_BYTES } = require('../services/passwords.js');
const emailSchema = z.string().trim().toLowerCase().email().max(254);
// The maximum protects against two things: bcrypt's 72-byte limit and
// denial of service by hashing enormous inputs.
const passwordSchema = z.string().min(12, 'At least 12 characters').max(MAX_BYTES);
// .strict() rejects extra fields: nobody assigns themselves a role.
const registerSchema = z.object({ email: emailSchema, password: passwordSchema,
name: z.string().trim().min(2).max(120) }).strict();
// At login the password is validated with min(1), NOT with the policy:
// an old user with 9 characters would be locked out of their own account.
const loginSchema = z.object({ email: emailSchema,
password: z.string().min(1).max(MAX_BYTES) }).strict();
module.exports = { registerSchema, loginSchema, emailSchema, passwordSchema };Without .strict(), a body containing "role":"administrator" could end up creating an administrator if somebody writes new User(req.body): that is mass assignment, and we come back to it in 08-06. The check against breached-password lists (Have I Been Pwned) is done with k-anonymity — you send the first 5 characters of the SHA-1 and filter locally, never revealing the password — and although we do not implement it here, in production it has one of the best cost/benefit ratios around.
POST /auth/register and user enumeration
POST /auth/register and user enumeration// src/controllers/authentication.js (excerpt: registration)
'use strict';
const { User } = require('../models/user.js');
const { hashPassword } = require('../services/passwords.js');
async function register(req, res, next) {
// req.validatedData is left by the validate() middleware from module 6.
const { email, password, name } = req.validatedData.body;
if (!await User.exists({ email })) {
const passwordHash = await hashPassword(password);
// Explicit fields, NEVER ...req.body: the server sets the role.
await User.create({ email, name, passwordHash, role: 'attendee' });
// The verification e-mail would be sent here (section 11).
}
// Same response whether or not the account exists: we do not leak which e-mails are registered.
res.status(202).json({ message: 'If the address is valid, you will receive a message to activate the account' });
}Go over what it does not do: it does not return the created user, it does not return the hash, it does not say whether the e-mail already existed, and it does not accept the whole body. Four deliberate omissions. The routes, in src/routes/authentication.js → { createAuthRoutes }, chain the rate limit and module 6's validate(schema, VALID_SOURCES.BODY) before each controller.
Answering "that e-mail is already registered" looks friendly and is an information leak: it lets an attacker confirm which addresses have accounts before launching credential stuffing, and in sensitive services it reveals membership. It leaks through four channels and all four have to be closed:
| Channel | Leak and fix |
|---|---|
| Error message | "E-mail already registered" → always an identical message |
| HTTP code | 409 if it exists, 201 if not → always the same code (202) |
| Response time | Fast when it does not exist → always hash, even in vain |
| Recovery | "No account with that address" → "if it exists, we have sent you a message" |
The honest trade-off: this hurts usability, and somebody who signed up a year ago gets no clear warning. The correct solution is to let the e-mail clear it up: if the account already exists, send a message saying "somebody tried to register with your address; if that was you, here is the link to recover it". An attacker who does not control the mailbox sees nothing; the legitimate owner does. Deciding is a judgment call: on a recipe forum, a clear 409 is defensible; on a platform holding purchase data, it is not.
POST /auth/login and constant time
POST /auth/login and constant time// src/controllers/authentication.js (excerpt: login)
// Decoy hash: a valid bcrypt hash of a password nobody knows.
const DECOY_HASH = '$2b$12$C6UzMDM.H6dfI/f/IKcEeO6iVQ9Lm2ZaZ8Xk1lF4a2iSg9WbLh9nK';
async function login(req, res, next) {
const { email, password } = req.validatedData.body;
// +passwordHash: the only place in the codebase that asks for it.
const user = await User.findOne({ email }).select('+passwordHash');
// We ALWAYS verify, whether or not the user exists. Without the decoy, a
// "nonexistent user" would answer in 2 ms and a "bad password" in 250 ms:
// timing the response would reveal which addresses have an account.
const matches = await verifyPassword(password, user ? user.passwordHash : DECOY_HASH);
if (!user || !matches) { // IDENTICAL message and code in both cases
return next(new AuthenticationError('Invalid credentials'));
}
if (!user.verified) {
return next(new AuthenticationError('The account is not verified yet'));
}
// From here on, issue the session (08-03) or the tokens (08-04).
req.authenticatedUser = { id: String(user._id), name: user.name, role: user.role };
next();
}We add the errors to src/errors.js, following the module 6 pattern:
class AuthenticationError extends ApplicationError {
constructor(message = 'Invalid credentials', details) {
super(message, { appCode: 'NOT_AUTHENTICATED', isOperational: true, details });
}
}
class AuthorizationError extends ApplicationError {
constructor(message = 'You do not have permission for this operation', details) {
super(message, { appCode: 'NO_PERMISSION', isOperational: true, details });
}
}We extend the STATUS_BY_CODE table in src/middleware/errors.js with NOT_AUTHENTICATED: 401, NO_PERMISSION: 403 and TOO_MANY_REQUESTS: 429. That way a failed login returns the usual format:
- Attempt limiting and logging
Slow hashing makes the attack expensive per password tried; rate limiting makes it expensive per attempt. You need both:
// src/middleware/limits.js (additions)
const loginLimit = rateLimit({
windowMs: 15 * 60 * 1000, limit: 10,
standardHeaders: 'draft-7', legacyHeaders: false, // includes Retry-After
// Key by IP + e-mail: this also slows the distributed attack that spreads
// attempts against ONE account across many IP addresses.
keyGenerator: (req) => `${req.ip}:${String(req.body?.email || '').toLowerCase()}`,
});
const registerLimit = rateLimit({ windowMs: 60 * 60 * 1000, limit: 5 });
module.exports = { generalLimit, purchaseLimit, loginLimit, registerLimit };Beware of account lockout: if you lock an account after N failures, you have created a denial of service against any user whose address is known; an increasing delay, a CAPTCHA past a threshold and an e-mail warning are preferable. In 08-06 we tune the limits per route type and explain why an in-memory store is no good with several processes.
What gets logged: timestamp, requestId (M6), e-mail or user id, outcome and a generic reason, IP address and user agent. What never does: the password — not even hashed — the stored hash, session, access or recovery tokens, and the full request body. The most common real-world mistake is not writing console.log(password), it is dumping the entire request in a debugging middleware or an error reporter; that is why a centralized list of redacted fields pays off: password, currentPassword, token, authorization, cookie.
- E-mail verification and recovery
Both flows use the same primitive: a single-use token sent by e-mail. Four rules, no exceptions:
- Cryptographically random, not
Math.random()and not a version 1 UUID:randomBytes(32). - Stored hashed (SHA-256 is enough: the token already has 256 bits of entropy, no slowness required), so whoever steals the database cannot use the pending tokens.
- Expires soon: 1 hour for recovery, 24 h for verification.
- Single use: deleted when consumed. And on use, existing sessions and refresh tokens are invalidated: if somebody had signed in with the stolen password, the change must throw them out.
// src/services/email-tokens.js
'use strict';
const { randomBytes, createHash, timingSafeEqual } = require('node:crypto');
const { EmailToken } = require('../models/email-token.js');
const EXPIRY = Object.freeze({ verification: 24 * 60 * 60 * 1000, recovery: 60 * 60 * 1000 });
// SHA-256 is correct HERE (and not for passwords) because the token has
// 256 bits of entropy: no dictionary can ever reach it.
const hashToken = (token) => createHash('sha256').update(token).digest('hex');
async function issueToken(userId, type) {
// 32 bytes = 256 bits. base64url so it travels cleanly inside a URL (M3).
const token = randomBytes(32).toString('base64url');
await EmailToken.deleteMany({ userId, type }); // invalidates the previous ones
await EmailToken.create({ userId, type, tokenHash: hashToken(token),
expiresAt: new Date(Date.now() + EXPIRY[type]) });
return token; // in the clear ONCE, to build the link in the e-mail
}
async function consumeToken(token, type) {
const hash = hashToken(token);
const record = await EmailToken.findOne({ tokenHash: hash, type });
if (!record || record.expiresAt.getTime() < Date.now()) { return null; }
// Constant-time comparison over the hash (M3).
if (!timingSafeEqual(Buffer.from(record.tokenHash), Buffer.from(hash))) { return null; }
await record.deleteOne(); // single use: it is consumed here
return String(record.userId);
}
module.exports = { issueToken, consumeToken, EXPIRY };The EmailToken model stores userId, type (verification or recovery), tokenHash (unique) and expiresAt with an { expireAfterSeconds: 0 } index, so that MongoDB deletes expired documents on its own with no cleanup job needed.
| Common mistake | How we avoid it |
|---|---|
| Token = an encoded id or e-mail: guessable, and any account is taken over | 256 random bits |
| Token stored in the clear: leaking the database = taking over any account | The hash is what gets stored |
| No expiry: an old e-mail still works years later | expiresAt + TTL index |
| Reusable: the link in the browser history works again | Deleted on consumption |
| Not closing sessions when the password changes: the attacker stays in | Revoke sessions and refresh tokens |
And changing the password while signed in, which always requires the current one:
async function changePassword(req, res, next) {
const { currentPassword, newPassword } = req.validatedData.body;
const user = await User.findById(req.user.id).select('+passwordHash');
// Requiring the current password blocks a takeover from a session left
// open and forgotten on a shared computer.
if (!await verifyPassword(currentPassword, user.passwordHash)) {
return next(new AuthenticationError('Invalid credentials'));
}
user.passwordHash = await hashPassword(newPassword);
await user.save();
await revokeAllRefreshTokens(user._id); // only the current session survives
res.status(204).end();
}Common Mistakes and Tips
- Hashing on the client and sending the hash. It does not help: the hash becomes the password, and whoever steals the database authenticates with it directly. The password travels in the clear inside TLS and is hashed on the server.
- A single shared global salt. The salt is unique per user. An extra application-wide secret (pepper) is a valid layer, but it never replaces the salt and it complicates rotation.
- Storing the password "temporarily" in a field, which ends up in a backup and in a log; or forgetting
select: falseand returning the whole user in a JSON response, the quietest way there is to leak hashes. - Truncating silently because of the 72-byte limit. Two long passphrases sharing a beginning would become equivalent.
- Tip: make
hashPassword/verifyPasswordthe only place that knows about the algorithm — migrating to Argon2 will mean changing one file — and when you raise the cost, take advantage of the login to rehash whenever the password verifies successfully: a transparent migration, with nothing asked of the user.
Exercises
Exercise 1: spot the flaws
This controller has five security flaws. Find them and explain each one.
async function register(req, res) {
const user = await User.create(req.body);
const hash = require('node:crypto').createHash('sha256').update(req.body.password).digest('hex');
user.passwordHash = hash;
await user.save();
console.log('Registered', req.body.email, req.body.password);
res.status(201).json(user);
}Exercise 2: transparent rehashing
Write verifyAndUpgrade(user, plainPassword): it verifies the password and, if the password is correct but the hash uses a cost lower than configuration.bcryptCost, regenerates and saves it. Hint: the cost is inside the hash itself, between the second and third $.
Exercise 3: password recovery
List, in order, the steps of POST /auth/recover/:token, which sets a new password, stating for each one which attack it prevents.
Solutions
Exercise 1
User.create(req.body): mass assignment; a body containing"role":"administrator"creates an administrator. The fields must be pulled out one by one fromreq.validatedData.- SHA-256 as a password hash: fast and unsalted, vulnerable to GPUs and to rainbow tables. It must be bcrypt/scrypt/Argon2.
console.logof the plaintext password: it stays in the logs forever.res.json(user): returns the complete document; with neitherselect: falsenortoJSON, it exposes the hash and internal fields.- No prior validation: no length policy, no e-mail normalization, no duplicate control; on top of that it answers 201, revealing whether the address already existed (enumeration). Bonus flaw: the user is created without a
passwordHashand saved in two steps; if the secondsave()fails, an account is left with no credential.
Exercise 2. The login is the only moment when the server legitimately knows the plaintext password, so it is the only possible moment to rehash.
async function verifyAndUpgrade(user, plainPassword) {
if (!await verifyPassword(plainPassword, user.passwordHash)) { return false; }
// Format $2b$12$salt+hash: the cost is the third segment.
if (Number(user.passwordHash.split('$')[2]) < configuration.bcryptCost) {
user.passwordHash = await hashPassword(plainPassword);
await user.save();
}
return true;
}Exercise 3
| Step | Attack it prevents |
|---|---|
| 1. Validate the body with zod | Enormous inputs that trigger expensive hashing |
2. consumeToken(token, 'recovery'): checks hash and expiry, and deletes it |
Token reuse and guessing |
| 3. Generic error if it is invalid | Distinguishing "nonexistent" from "expired" helps an attacker calibrate |
4. Hash with bcrypt and save, setting verified: true |
Cleartext storage; whoever controls the mailbox has already proven ownership of the address |
| 5. Revoke every session and refresh token | An attacker already inside would stay inside |
| 6. Send an e-mail warning and log the event (no token, no password) | Silent account takeover; missing audit trail (08-05, 08-06) |
Conclusion
Escena Viva now has a real credential. We know why a password is never stored in the clear, encrypted or hashed with SHA-256; what a password hash does — slow, salted, with an adjustable cost — and why bcrypt keeps the variant, the cost and the salt inside the hash itself. We measured the cost factor instead of copying it, completed the User model with passwordHash and select: false, written a .strict() zod schema with a modern policy, and built a POST /auth/register and a POST /auth/login that leak which e-mails exist neither through the message, nor through the code, nor through the timing. And we got right the part that most often goes wrong: single-use tokens, random, hashed in the database and with an expiry.
But look at how login ends today: we know it is Lucía… and we do nothing with that information. The next request arrives anonymous all over again. In the next lesson, Sessions and Cookies with Passport.js, we solve it the classic way: a random session identifier in an HttpOnly cookie, the state on the server, express-session with the options that really matter, the regeneration that prevents session fixation, Passport with its local strategy, and the CSRF protection that cookies demand in exchange for their convenience.
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
