At the end of the previous lesson we had a login that verifies Lucía's password correctly… and stops right there. The next request arrives anonymous all over again, because HTTP is stateless. Time to fix it, and we do it first with the classic technique: server sessions with a cookie.
Escena Viva will end up using JWT (lesson 08-04), but skipping sessions would be a serious pedagogical mistake. They are the conceptual foundation of everything else, they are still the best option for a huge number of web applications, and they teach first-hand the two problems that reappear later with tokens: where the state lives and how it is revoked. On top of that, this is where you meet Passport.js, the piece you will find in practically any Node project with authentication.
Contents
- How a server session works
express-sessionand the options that matter- The in-memory store is not production-grade
- Session fixation and
regenerate() - Logging out properly
- Passport.js: what it is and what it is not
- The local strategy
serializeUseranddeserializeUser- Wiring it into
createApplication()and the login route - The
requireSessionmiddleware - CSRF: the price of cookies
- Third-party strategies and a final comparison
- Common mistakes, exercises and conclusion
- How a server session works
The idea is simple and very old: the client stores only a random identifier; the data lives on the server.
sequenceDiagram
participant B as Browser
participant A as Escena Viva API
participant S as Session store
participant D as MongoDB
B->>A: POST /auth/login {email, password}
A->>D: Find user (+passwordHash)
A->>A: bcrypt.compare -> correct
A->>A: req.session.regenerate() (new id: anti fixation)
A->>S: Store session { sid: aB3x..., userId: '64f...' }
A-->>B: 200 + Set-Cookie: sid=aB3x...; HttpOnly; Secure; SameSite=Lax
B->>A: GET /api/orders (Cookie: sid=aB3x... automatic)
A->>S: Read session by sid
S-->>A: { userId: '64f...' }
A->>D: Load user -> req.user and query their orders
A-->>B: 200 [Lucia's orders]
B->>A: POST /auth/logout
A->>S: destroy(sid)
A-->>B: 204 + Set-Cookie: sid=; Max-Age=0
Three properties follow from this design. The cookie is opaque: aB3x9Kq... means nothing outside the server, and tampering with it gets you nowhere — either the identifier exists in the store or it does not. Revocation is immediate: deleting the row from the store kicks the user out on the very next request, which is exactly what JWTs cannot do. And the server holds state: with a single process that is trivial; with several, it stops being trivial (section 3).
Why the cookie must not contain user data. The temptation to put {"userId":"64f...","role":"administrator"} in the cookie, even signed, has three problems: the data is visible to anyone with access to the browser or to a proxy log; it is frozen in time (if you demote an organizer, their cookie keeps saying organizer until it expires); and it grows without control, making every request more expensive. The cookie carries an identifier. Full stop.
express-session and the options that matter
express-session and the options that matterInstallation: npm install express-session.
// src/middleware/session.js
'use strict';
const session = require('express-session');
const { configuration } = require('../config/index.js');
function createSessionMiddleware() {
return session({
// Signs the cookie so tampering is detected. An array allows ROTATING
// the secret: it signs with the first and verifies with all of them, so
// live sessions survive the change.
secret: configuration.sessionSecrets,
// The default name (connect.sid) advertises the technology in use.
name: 'ev.sid',
resave: false, // no rewrite when nothing changed: fewer races
saveUninitialized: false, // no session created for anonymous visitors
rolling: true, // renews maxAge on every request: expires on inactivity
cookie: {
httpOnly: true, // JavaScript cannot read it: anti XSS
secure: configuration.nodeEnv === 'production', // HTTPS only
sameSite: 'lax', // anti CSRF on cross-site POST
maxAge: 30 * 60 * 1000, // 30 minutes of inactivity
path: '/',
},
});
}
module.exports = { createSessionMiddleware };In src/config/index.js, the only place that reads process.env:
const sessionSecrets = (process.env.SESSION_SECRETS || '').split(',').map((v) => v.trim()).filter(Boolean);
// Validate at startup: better to fail on boot than to serve insecurely.
if (nodeEnv === 'production' && (sessionSecrets.length === 0 || sessionSecrets.some((s) => s.length < 32))) {
throw new Error('SESSION_SECRETS must exist, with each secret 32+ characters long');
}| Option | Value | Why |
|---|---|---|
secret |
Array of 32+ random characters | Signs the cookie; the array allows rotation without closing sessions |
name |
ev.sid |
Hides the tell-tale connect.sid |
resave / saveUninitialized |
false / false |
Fewer writes and races; no sessions created for anonymous visitors |
rolling |
true |
Expiry on inactivity, not absolute |
cookie.httpOnly / secure |
true / true in production |
An XSS cannot steal the session; never in the clear over the network |
cookie.sameSite / maxAge |
'lax' / 30 min |
Blocks CSRF via external POST; short window if it leaks |
A naming caution. From this lesson on, req.session belongs to express-session: it is the HTTP session of whoever is browsing. The show session that router.param loaded in Module 6 (src/routes/sessions.js) therefore moves to req.showSession, and every controller that reads it is updated accordingly. Two very different things happened to share a name, and letting them collide on the same property is a bug waiting for a Friday afternoon.
A note on secure behind a proxy: if Express sits behind Nginx or a load balancer that terminates TLS, the internal connection is plain HTTP and the Secure cookie would never be sent. You have to declare app.set('trust proxy', 1) so that Express trusts X-Forwarded-Proto. It is the same setting express-rate-limit needs in order not to see every request coming from the same IP address (we return to this in 08-06).
- The in-memory store is not production-grade
This is the big warning of the lesson. With no store configured, express-session uses MemoryStore, and the package itself advises against it in so many words. The three problems:
- It is lost on restart. Every deployment, every PM2 restart, every process crash throws all users out. In the middle of a purchase for the Festival de Jazz, that is an abandoned cart.
- It does not work with several processes. With
cluster(M10) or two containers behind a load balancer, each process has its own memory: process A serves the login, process B serves the next request, and the user appears logged out intermittently. The classic symptom is "it logs me out sometimes". - Memory leak. It does not expire entries reliably; with real traffic, memory grows until the system kills the process.
The solution is a shared store: Redis (connect-redis) is the de facto standard, and MongoDB works too (connect-mongo). The Redis session store and distributed rate limiting are Module 10 material, along with cluster. Here it is enough to carve the rule in stone: in-memory sessions = development, and development only. And notice the conceptual consequence, because it justifies the next lesson: sessions demand shared infrastructure, and that cost is precisely what self-contained tokens avoid.
- Session fixation and
regenerate()
regenerate()The attack, step by step: the attacker visits Escena Viva and gets an identifier, sid=ATTACKER123; they get the victim to use that same identifier (a link carrying the id in the URL, a cookie planted from a compromised subdomain, an XSS); the victim signs in and, if the server keeps the identifier and merely attaches a userId to it, ATTACKER123 is now an authenticated session belonging to Lucía; the attacker, who already had it, walks in as her.
The defense is one line, and it is mandatory:
// On login, ALWAYS a fresh identifier.
req.session.regenerate((error) => {
if (error) { return next(error); }
req.session.userId = String(user._id);
// Explicit save(): with an external store the write is asynchronous, and
// without awaiting it the response can outrun the persistence.
req.session.save((saveError) => {
if (saveError) { return next(saveError); }
res.json({ user: { id: user._id, name: user.name, role: user.role } });
});
});Two details that get overlooked: regenerate destroys the previous session data, so if you were storing something before the login (a cart, the URL to return to) you have to copy it over by hand after regenerating; and an explicit req.session.save() before responding avoids the classic "the first login does not work, the second one does". Passport offers keepSessionInfo in req.login() precisely for this; by default it regenerates, which is the right thing to do.
- Logging out properly
Logging out is not deleting the cookie: you have to destroy the state on the server. If you only delete the cookie, anyone holding a copy of the identifier is still inside.
// src/controllers/authentication.js (excerpt)
function logout(req, res, next) {
req.logout((logoutError) => { // 1) Passport clears req.user
if (logoutError) { return next(logoutError); }
req.session.destroy((destroyError) => { // 2) the sid ceases to exist
if (destroyError) { return next(destroyError); }
// 3) The attributes must MATCH those of the original Set-Cookie, or the
// browser treats it as a different cookie and will not delete it.
res.clearCookie('ev.sid', { httpOnly: true, sameSite: 'lax', path: '/' });
res.status(204).end();
});
});
}It is also worth offering "sign out on all devices": that means deleting from the store every session carrying that userId, which in Redis is solved by keeping a set named ev:user:<id>:sessions. It is the operation to run after a password change (08-02) or on any suspicion of compromise.
- Passport.js: what it is and what it is not
Installation: npm install passport passport-local. Passport is a strategy framework, not a complete authentication system:
| What Passport does | What Passport does not do |
|---|---|
| Normalizes over 500 mechanisms (local, Google, SAML, JWT) behind one API | It does not hash passwords: that is on you |
Converts between req.user and the session |
It does not manage users or registration |
Provides req.login(), req.logout(), req.isAuthenticated() |
It does not do authorization or roles |
| Plugs in as Express middleware | It does not protect against CSRF |
In other words: Passport is the glue. The credential verification you wrote in 08-02 is still yours; Passport merely wraps it in a uniform interface so that swapping "password login" for "Google login" does not rewrite the application. Is it worth it for a single local strategy? Honestly, if password login is all you will ever have, a 20-line middleware of your own is simpler and easier to audit. Passport wins when you expect to add providers, and because of how ubiquitous it is in legacy Node code.
- The local strategy
// src/authentication/local-strategy.js
'use strict';
const { Strategy: LocalStrategy } = require('passport-local');
const { verifyPassword } = require('../services/passwords.js');
const DECOY_HASH = '$2b$12$C6UzMDM.H6dfI/f/IKcEeO6iVQ9Lm2ZaZ8Xk1lF4a2iSg9WbLh9nK';
function createLocalStrategy() {
return new LocalStrategy(
// By default Passport expects 'username'/'password'. We rename them to the
// fields the Escena Viva zod schemas already use.
{ usernameField: 'email', passwordField: 'password', session: true },
async (email, password, done) => {
try {
const normalized = String(email).trim().toLowerCase();
const user = await User.findOne({ email: normalized }).select('+passwordHash');
// Decoy: the same time cost whether or not the account exists (08-02).
const hash = user ? user.passwordHash : DECOY_HASH;
const matches = await verifyPassword(password, hash);
if (!user || !matches) {
// user=false means "credential failure", not a server error.
return done(null, false, { message: 'Invalid credentials' });
}
if (!user.verified) {
return done(null, false, { message: 'The account is not verified yet' });
}
// A domain object, not the Mongoose document (M7).
return done(null, { id: String(user._id), email: user.email,
name: user.name, role: user.role, assignedVenue: user.assignedVenue });
} catch (error) {
return done(error); // a genuine error: 500, not 401
}
},
);
}
module.exports = { createLocalStrategy };The done(error, user, info) signature distinguishes three outcomes, and confusing them is a frequent mistake: done(error) is a server failure (500), done(null, false, info) is invalid credentials (401), and done(null, user) is success and sets req.user.
serializeUser and deserializeUser
serializeUser and deserializeUser// src/authentication/passport.js
'use strict';
const passport = require('passport');
const { User } = require('../models/user.js');
const { createLocalStrategy } = require('./local-strategy.js');
function configurePassport() {
passport.use('local', createLocalStrategy());
// What gets stored IN the session: the id and nothing else. Never the whole object.
passport.serializeUser((user, done) => done(null, user.id));
// How req.user is rebuilt on every request, starting from the id.
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id).lean();
// Deleted account: the session invalidates itself.
if (!user) { return done(null, false); }
done(null, { id: String(user._id), email: user.email, name: user.name,
role: user.role, assignedVenue: user.assignedVenue });
} catch (error) { done(error); }
});
}
module.exports = { configurePassport, passport };Why only the id. If you serialize the complete user, you are storing a frozen snapshot: demote an organizer and their session keeps saying organizer until it expires. By storing the id and re-reading on every request, the role is always fresh. It is exactly the problem JWTs have by design (08-04) and that we avoid here in exchange for one query per request — a query that gets cached with Redis (M10) when traffic demands it.
- Wiring it into
createApplication() and the login route
createApplication() and the login routeOrder matters and it is an inexhaustible source of bugs:
// src/app.js (canonical order, extended in module 8)
function createApplication(options = {}) {
const app = express();
app.set('trust proxy', 1); // behind a proxy: real IP and protocol
app.use(requestId); // M6: traceability
app.use(helmet());
app.use(compression());
app.use(httpLogger); // morgan
app.use(corsAllowlist); // never '*'
app.use(express.json({ limit: '100kb' }));
app.use(cookieParser()); // required for CSRF and cookies
app.use(createSessionMiddleware()); // 1) the session exists
app.use(passport.initialize()); // 2) Passport hooks in
app.use(passport.session()); // 3) fills req.user from the session
app.use(csrfProtection); // 4) after session and cookies
app.use('/auth', createAuthRoutes());
app.use('/api', createApiRoutes());
app.use(routeNotFound);
app.use(errorHandler); // always last
return app;
}The three ordering rules to memorize: session() before passport.session() (Passport reads from req.session, which would not exist yet); express.json() before the login routes (the local strategy needs req.body); and CSRF after the session and the cookie parser, before the routes that change state.
// src/routes/authentication.js (excerpt)
router.post('/login', loginLimit, validate(loginSchema, VALID_SOURCES.BODY), (req, res, next) => {
// Custom callback: we control the response instead of letting Passport
// redirect (successRedirect/failureRedirect are meant for HTML).
passport.authenticate('local', (error, user, info) => {
if (error) { return next(error); }
if (!user) { return next(new AuthenticationError(info?.message)); }
// req.login establishes the session and regenerates the id (anti fixation).
req.login(user, (loginError) => {
if (loginError) { return next(loginError); }
res.json({ user: { id: user.id, name: user.name, role: user.role } });
});
})(req, res, next); // authenticate returns a middleware: it has to be invoked
});
router.post('/logout', requireSession, logout);
- The
requireSession middleware
requireSession middleware// src/middleware/require-session.js
'use strict';
const { AuthenticationError } = require('../errors.js');
function requireSession(req, res, next) {
if (req.isAuthenticated && req.isAuthenticated() && req.user) {
// req.user is our canonical property: Passport fills it here, and the JWT
// middleware of 08-04 will fill exactly the same one, so the rest of the
// application never learns which mechanism authenticated the request.
return next();
}
// 401: not authenticated. Module 6 error format.
next(new AuthenticationError('You must sign in for this operation'));
}
module.exports = { requireSession };And with that, the hole left by Module 7 finally disappears:
// src/routes/orders.js (excerpt)
router.post('/purchases', requireSession, purchaseLimit, validate(purchaseSchema), async (req, res, next) => {
const { sessionId, quantity } = req.validatedData.body;
// The userId does NOT come from the client any more: it comes from the verified session.
const result = await buyTickets({ userId: req.user.id, sessionId, quantity });
res.status(201).json(result);
});That line — userId: req.user.id — is the goal of the whole module. The identifier is no longer an input: it is a conclusion drawn by the server.
- CSRF: the price of cookies
Cookies are sent automatically on every request to the domain, no matter who originated it. That convenience is also the vulnerability. The attack against Escena Viva: Marc, with an open session, visits cheap-deals.test, which contains a hidden form pointing at https://api.escenaviva.test/api/purchases with a sessionId and a quantity, plus a script that submits it on its own. The browser attaches Marc's session cookie: ten tickets bought without him lifting a finger.
Why tokens in a header do not suffer from this: Authorization: Bearer ... is not sent on its own. Another site would have to read the token and add it by hand, and the browser stops it from doing so. That is a weighty argument in favor of tokens in an API.
| Scenario | Is the cookie sent with SameSite=Lax? |
|---|---|
POST form from another site |
No — CSRF blocked |
fetch/XHR from another site |
No |
<img src> pointing at a state-changing GET |
No |
Link navigation (GET) from another site |
Yes — which is why no GET may change state |
| Request from a subdomain of the same site | Yes — a compromised subdomain is still a risk |
Lax covers most cases, but it is not enough on its own: it does not protect between subdomains and old browsers ignore it. For sensitive operations we add the synchronizer token pattern: the server issues a CSRF token that the client must send back in a header; since another site cannot read it, it cannot reproduce it.
// src/middleware/csrf.js -> npm install csrf-csrf
'use strict';
const { doubleCsrf } = require('csrf-csrf');
const { configuration } = require('../config/index.js');
const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
getSecret: () => configuration.csrfSecret,
// The token is bound to the session: one from another user will not do.
getSessionIdentifier: (req) => req.sessionID,
cookieName: '__Host-ev.csrf',
cookieOptions: { httpOnly: true, sameSite: 'lax', secure: true, path: '/' },
ignoredMethods: ['GET', 'HEAD', 'OPTIONS'], // these do not change state
getCsrfTokenFromRequest: (req) => req.headers['x-csrf-token'],
});
module.exports = { doubleCsrfProtection, generateCsrfToken };Its place in the chain: after session() and cookieParser(), before the routes. The client obtains the token with a GET /auth/csrf and sends it in X-CSRF-Token on every POST, PATCH or DELETE. Important for the next lesson: if Escena Viva authenticates with Authorization: Bearer, those routes need no CSRF; but the refresh token will live in a cookie, so POST /auth/refresh does stay exposed, and that is why we will protect it with SameSite=Strict and a restricted Path.
- Third-party strategies and a final comparison
Passport shines when you add providers: passport-google-oauth20 or passport-github2 are wired up in the same shape as the local strategy, receiving clientID, clientSecret and callbackURL, and returning the User matching the profile. The authorization code flow with PKCE, in brief: the user clicks "Sign in with Google" and the API redirects to the provider with client_id, redirect_uri, scope and a random state; the user authenticates at Google and consents; Google redirects back to /auth/google/callback?code=...&state=...; the API checks that state matches (that is OAuth's CSRF defense) and exchanges the code for tokens server to server; and finally it creates or locates the User and opens its own session. The two places where people go wrong most often: not validating state and accepting tokens sent by the client instead of exchanging the code on the server. Escena Viva does not implement OAuth, but the slot is ready: deserializeUser does not care where the user came from.
| Criterion | Server session | Self-contained token (JWT) |
|---|---|---|
| Server state | Yes (needs Redis with several processes) | No (except for a revocation list) |
| Revocation | Immediate | Hard: you have to wait for it to expire |
| Role always up to date | Yes (it is re-read) | No (frozen in the token) |
| Cross-domain / mobile | Awkward | Natural |
| Vulnerable to CSRF | Yes (mitigable) | No, if it travels in a header |
| Vulnerable to XSS | An HttpOnly cookie cannot be stolen |
Serious if stored in localStorage |
| Size per request | ~30 bytes | 300–800 bytes |
When to use which. Sessions: a single-domain web application, an admin panel, any case where instant revocation is a requirement. Tokens: a public API, a mobile client, several front-ends on different domains, service to service. Both: the most common arrangement in real systems, and what Escena Viva will do — the short access token plus refresh cookie pattern is, deep down, a session under another name.
Common Mistakes and Tips
- Setting
saveUninitialized: truewithout thinking. It creates a session and a cookie for every bot that visits the catalog: it fills the store and complicates cookie-regulation compliance. - Forgetting
req.session.regenerate()at login. It leaves session fixation wide open. - Responding before the session has been saved. With an external store this produces the classic "the first login does not work, the second one does".
res.clearCookiewith attributes different from those of the originalSet-Cookie: the browser does not delete it and the user believes they have signed out.- Believing
SameSite=Laxreplaces the CSRF token. It helps a great deal; it is not enough between subdomains. - Using
successRedirectin a JSON API. It returns a 302 the client is not expecting; use the custom callback. - Tip: always normalize onto
req.user— when you bring in JWT in 08-04, the controllers and the authorization layer will not even notice the change — and in development start two processes on different ports so you can watch the in-memory store fail: learning it the hard way is worth more than reading about it ten times.
Exercises
Exercise 1: audit a configuration
Point out the problems in this configuration and fix them:
app.use(session({
secret: 'secret',
resave: true,
saveUninitialized: true,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 },
}));
app.use(passport.session());
app.use(passport.initialize());Exercise 2: an inconsistent session
Escena Viva is deployed with two processes behind a load balancer. Users report that "sometimes" they appear signed out, and that deploying signs them out. Explain the cause and describe the solution, stating which module of the course develops it.
Exercise 3: the requireVerified middleware
Write a requireVerified middleware that runs after requireSession and returns a 403 in the module 6 error format if req.user.verified is false. Justify why it is 403 and not 401.
Solutions
Exercise 1. Six problems: secret: 'secret' is short, guessable and hard-coded (it must come from configuration, with 32+ random characters and in an array so it can be rotated); resave: true rewrites the session on every request even when nothing changed, with pointless load and races; saveUninitialized: true creates sessions for anonymous visitors; the cookie without httpOnly is stolen by any XSS; without secure or sameSite it travels in the clear and is exposed to CSRF; and passport.session() sits before passport.initialize(), an inverted order that simply does not work. On top of that, a 30-day maxAge is excessive for a purchasing session.
app.use(session({
secret: configuration.sessionSecrets, name: 'ev.sid',
resave: false, saveUninitialized: false, rolling: true,
cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 60 * 1000, path: '/' },
store: sharedStore,
}));
app.use(passport.initialize());
app.use(passport.session());Exercise 2. The cause is the default MemoryStore: each process keeps sessions in its own memory, so if process A serves the login and process B serves the next request, the identifier does not exist there and the user appears anonymous; on deployment the memory is lost entirely and every session falls. The solution is a shared, persistent store: Redis with connect-redis (or connect-mongo on the database we already have). The Redis session store and distributed rate limiting are developed in Module 10, along with cluster and worker threads.
Exercise 3
// src/middleware/require-verified.js
'use strict';
const { AuthorizationError } = require('../errors.js');
function requireVerified(req, res, next) {
if (req.user && req.user.verified) { return next(); }
next(new AuthorizationError('You must verify your e-mail before buying tickets', {
action: 'resend-verification',
}));
}
module.exports = { requireVerified };It is 403 and not 401 because the identity is perfectly established: we know who they are and we verified their credential. What is missing is a permission, not an authentication. Returning 401 would make the client send the user back to the login form, where they would sign in correctly and fail all over again: a frustrating loop born of confusing the two concepts from lesson 08-01.
Conclusion
We now know how to carry identity between requests the classic way. A server session is a random identifier in an HttpOnly cookie plus state stored on the server; the cookie is opaque on purpose, and that is why revocation is immediate and the role is always fresh. We configured express-session with the options that genuinely decide security, learned why the in-memory store only serves in development, closed session fixation with regenerate(), and logged out properly with destroy() plus deletion of the cookie. Passport gave us a uniform interface — local strategy, serializeUser with only the id, req.user — and we paid the price of cookies with CSRF protection. And above all: buyTickets now receives a userId the server deduces rather than one the client declares.
But Escena Viva is an API with a static front-end and a mobile client on the horizon, and we have just seen the cost of sessions: shared state and an awkward fit outside the browser. In the next lesson, Authentication with JWT, we change approach: we will see what is inside a token, why signing is not encrypting, how the alg: none and algorithm confusion attacks are avoided, and — most importantly — how the real problem with JWTs is solved, namely that they cannot be revoked, with a short-lived access token and a rotating refresh token stored in an HttpOnly cookie.
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
